Como funciona o fluxo

O IDCamim usa o Authorization Code Flow — o mais seguro para aplicações web com backend.

1. Usuário clica "Entrar com IDCamim" no seu sistema │ ▼ 2. Seu sistema redireciona para → https://auth.camim.com.br/auth │ ▼ 3. Usuário digita e-mail e senha na página do Camim │ ▼ 4. IDCamim redireciona de volta → https://seusistema.com.br/callback?code=XYZ │ ▼ 5. Seu backend troca o código por tokens (POST /token) │ ▼ 6. Seu backend busca os dados do usuário (GET /me) │ ▼ 7. Usuário autenticado ✓

A Discovery URL https://auth.camim.com.br/.well-known/openid-configuration contém todos os endpoints e configurações — bibliotecas OIDC se configuram automaticamente com ela.

Endpoints

GET
/auth
Inicia o login — redirecione o usuário para cá
POST
/token
Troca o código por access_token e id_token
GET
/me
Retorna os dados do usuário autenticado
GET
/session/end
Encerra a sessão do usuário (logout)
GET
/jwks
Chaves públicas para verificar JWTs (ES256)
POST
/token/introspection
Verifica se um access_token é válido
POST
/token/revocation
Revoga um token antes da expiração
GET
/.well-known/openid-configuration
Discovery — auto-configuração de bibliotecas

O endpoint de autorização é /auth, não /authorize.

Registrar seu sistema

Antes de integrar, registre seu sistema no painel para obter client_id e client_secret.

Via Painel Admin (recomendado)

Acesse auth.camim.com.br/painelSistemasRegistrar novo sistema.

O painel exibe o client_secret uma única vez. Copie e guarde com segurança.

Via cURL (com chave de admin)

curl -X POST https://auth.camim.com.br/admin/clients \
    -H "x-admin-api-key: SUA_CHAVE_ADMIN" \
    -H "Content-Type: application/json" \
    -d '{
      "client_name": "Nome do Sistema",
      "redirect_uris": ["https://seusistema.com.br/auth/callback"],
      "post_logout_redirect_uris": ["https://seusistema.com.br"],
      "description": "Descrição do sistema"
    }'

Resposta:

{
    "client_id": "camim_a1b2c3d4e5f6g7h8",
    "client_secret": "abc123def456...",
    "client_name": "Nome do Sistema",
    "redirect_uris": ["https://seusistema.com.br/auth/callback"]
  }

Não tem a chave de admin? Envie as URLs para o administrador do Camim e ele devolve as credenciais.

Passo 1 — Redirecionar o usuário

Monte a URL e redirecione o navegador do usuário:

https://auth.camim.com.br/auth
    ?client_id=camim_SEU_CLIENT_ID
    &redirect_uri=https://seusistema.com.br/auth/callback
    &response_type=code
    &scope=openid profile email
    &state=VALOR_ALEATORIO_GERADO_POR_VOCE
ParâmetroValorDescrição
client_idcamim_...ID do seu sistema
redirect_uriURL cadastradaExatamente igual à cadastrada no registro
response_typecodeSempre code
scopeopenid profile emailDados que você quer acessar
statestring aleatóriaSalve na sessão para verificar depois (anti-CSRF)
code_challengestring (opcional)Para PKCE — recomendado em SPAs

Passo 2 — Receber o código

Após o login, o IDCamim redireciona para sua redirect_uri:

https://seusistema.com.br/auth/callback
    ?code=CODIGO_AQUI
    &state=VALOR_ALEATORIO
    &iss=https://auth.camim.com.br

Sempre verifique o state — compare com o valor salvo na sessão. Se divergir, rejeite a requisição (ataque CSRF). O código expira em 10 minutos e é de uso único.

Em caso de erro:

https://seusistema.com.br/auth/callback?error=access_denied&error_description=...

Passo 3 — Obter tokens

Faça esta requisição do seu backend — nunca do frontend:

curl -X POST https://auth.camim.com.br/token \
    -u "camim_SEU_CLIENT_ID:SEU_CLIENT_SECRET" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code" \
    -d "code=CODIGO_RECEBIDO" \
    -d "redirect_uri=https://seusistema.com.br/auth/callback"

A autenticação é via HTTP Basic Auth (-u) — o curl codifica automaticamente em Base64.

Resposta:

{
    "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
    "id_token":     "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type":   "Bearer",
    "expires_in":   3600,
    "scope":        "openid profile email"
  }
CampoDescrição
access_tokenToken opaco para acessar /me. Dura 1 hora.
id_tokenJWT assinado ES256 com dados básicos do usuário.
expires_inSegundos até expirar (3600 = 1h).
refresh_tokenPresente se offline_access foi solicitado. Dura 30 dias.

Passo 4 — Dados do usuário

curl https://auth.camim.com.br/me \
    -H "Authorization: Bearer SEU_ACCESS_TOKEN"

Resposta:

{
    "sub":            "52664cd8-6976-4c76-ae81-a7c2ae9adee5",
    "email":          "cristiano@camim.com.br",
    "email_verified": true,
    "name":           "Cristiano Camim",
    "given_name":     "Cristiano",
    "family_name":    "Camim",
    "picture":        null,
    "updated_at":     1742598000
  }

Use sempre o sub como identificador do usuário no seu banco — é um UUID único e imutável. O e-mail pode mudar; o sub nunca muda.

Scopes disponíveis

Informe no parâmetro scope separados por espaço.

ScopeCampos retornadosObrigatório?
openidsub, iss, aud, exp, iatSim — sempre
emailemail, email_verifiedNão
profilename, given_name, family_name, picture, updated_atNão
phonephone_numberNão
offline_accessGera refresh_token (30 dias)Não

Exemplo completo: openidprofileemailphoneoffline_access

Exemplos por linguagem

import secrets, urllib.parse, requests
  from flask import Flask, redirect, request, session

  app = Flask(__name__)
  app.secret_key = "sua-secret-key"

  CLIENT_ID     = "camim_SEU_CLIENT_ID"
  CLIENT_SECRET = "SEU_CLIENT_SECRET"
  REDIRECT_URI  = "https://seusistema.com.br/auth/callback"
  CAMIM_BASE    = "https://auth.camim.com.br"

  @app.route("/login")
  def login():
      state = secrets.token_urlsafe(16)
      session["oauth_state"] = state
      params = urllib.parse.urlencode({
          "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI,
          "response_type": "code", "scope": "openid profile email", "state": state,
      })
      return redirect(f"{CAMIM_BASE}/auth?{params}")

  @app.route("/auth/callback")
  def callback():
      if "error" in request.args:
          return f"Erro: {request.args['error_description']}", 400
      if request.args.get("state") != session.pop("oauth_state", None):
          return "State inválido", 400

      tokens = requests.post(f"{CAMIM_BASE}/token",
          auth=(CLIENT_ID, CLIENT_SECRET),
          data={"grant_type": "authorization_code",
                "code": request.args["code"], "redirect_uri": REDIRECT_URI},
      ).json()
      user = requests.get(f"{CAMIM_BASE}/me",
          headers={"Authorization": f"Bearer {tokens['access_token']}"}
      ).json()

      session["user"] = {"id": user["sub"], "email": user["email"], "name": user.get("name")}
      return redirect("/dashboard")
const express = require('express');
  const axios   = require('axios');
  const crypto  = require('crypto');
  const session = require('express-session');

  const app = express();
  app.use(session({ secret: 'sua-secret-key', resave: false, saveUninitialized: false }));

  const CLIENT_ID     = 'camim_SEU_CLIENT_ID';
  const CLIENT_SECRET = 'SEU_CLIENT_SECRET';
  const REDIRECT_URI  = 'https://seusistema.com.br/auth/callback';
  const CAMIM_BASE    = 'https://auth.camim.com.br';

  app.get('/login', (req, res) => {
    const state = crypto.randomBytes(16).toString('hex');
    req.session.oauthState = state;
    const params = new URLSearchParams({
      client_id: CLIENT_ID, redirect_uri: REDIRECT_URI,
      response_type: 'code', scope: 'openid profile email', state,
    });
    res.redirect(`${CAMIM_BASE}/auth?${params}`);
  });

  app.get('/auth/callback', async (req, res) => {
    if (req.query.error) return res.status(400).send(req.query.error_description);
    if (req.query.state !== req.session.oauthState) return res.status(400).send('State inválido');
    delete req.session.oauthState;

    const { data: tokens } = await axios.post(`${CAMIM_BASE}/token`,
      new URLSearchParams({ grant_type: 'authorization_code',
        code: req.query.code, redirect_uri: REDIRECT_URI }),
      { auth: { username: CLIENT_ID, password: CLIENT_SECRET } }
    );
    const { data: user } = await axios.get(`${CAMIM_BASE}/me`,
      { headers: { Authorization: `Bearer ${tokens.access_token}` } }
    );
    req.session.user = { id: user.sub, email: user.email, name: user.name };
    res.redirect('/dashboard');
  });
<?php // login.php
  session_start();
  $state = bin2hex(random_bytes(16));
  $_SESSION['oauth_state'] = $state;
  header('Location: https://auth.camim.com.br/auth?' . http_build_query([
      'client_id' => 'camim_SEU_CLIENT_ID',
      'redirect_uri' => 'https://seusistema.com.br/callback.php',
      'response_type' => 'code', 'scope' => 'openid profile email', 'state' => $state,
  ]));

  // callback.php
  if ($_GET['state'] !== $_SESSION['oauth_state']) die('State inválido');
  $ch = curl_init('https://auth.camim.com.br/token');
  curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true,
      CURLOPT_USERPWD => 'camim_SEU_CLIENT_ID:SEU_CLIENT_SECRET',
      CURLOPT_POSTFIELDS => http_build_query(['grant_type' => 'authorization_code',
          'code' => $_GET['code'], 'redirect_uri' => 'https://seusistema.com.br/callback.php']),
  ]);
  $tokens = json_decode(curl_exec($ch), true);
  $ch = curl_init('https://auth.camim.com.br/me');
  curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => ["Authorization: Bearer {$tokens['access_token']}"],
  ]);
  $user = json_decode(curl_exec($ch), true);
  $_SESSION['user'] = ['id' => $user['sub'], 'email' => $user['email']];
# pip install authlib
  # settings.py
  IDCAMIM = {
      "CLIENT_ID":     "camim_SEU_CLIENT_ID",
      "CLIENT_SECRET": "SEU_CLIENT_SECRET",
      "REDIRECT_URI":  "https://seusistema.com.br/auth/callback",
      "DISCOVERY_URL": "https://auth.camim.com.br/.well-known/openid-configuration",
      "SCOPES":        "openid profile email",
  }

  # views.py
  import secrets
  from django.conf import settings
  from django.shortcuts import redirect
  from authlib.integrations.requests_client import OAuth2Session

  def camim_login(request):
      conf = settings.IDCAMIM
      state = secrets.token_urlsafe(16)
      request.session["oauth_state"] = state
      client = OAuth2Session(conf["CLIENT_ID"], redirect_uri=conf["REDIRECT_URI"], scope=conf["SCOPES"])
      meta = client.get(conf["DISCOVERY_URL"]).json()
      url, _ = client.create_authorization_url(meta["authorization_endpoint"], state=state)
      return redirect(url)

  def camim_callback(request):
      conf = settings.IDCAMIM
      client = OAuth2Session(conf["CLIENT_ID"], redirect_uri=conf["REDIRECT_URI"])
      meta = client.get(conf["DISCOVERY_URL"]).json()
      tokens = client.fetch_token(meta["token_endpoint"], code=request.GET["code"],
                                  client_secret=conf["CLIENT_SECRET"])
      user = client.get(meta["userinfo_endpoint"]).json()
      request.session["user"] = {"id": user["sub"], "email": user["email"], "name": user.get("name")}
      return redirect("/dashboard/")
// pages/api/auth/[...nextauth].js
  import NextAuth from "next-auth";

  export default NextAuth({
    providers: [{
      id: "idcamim", name: "Camim", type: "oauth",
      wellKnown: "https://auth.camim.com.br/.well-known/openid-configuration",
      clientId: process.env.IDCAMIM_CLIENT_ID,
      clientSecret: process.env.IDCAMIM_CLIENT_SECRET,
      authorization: { params: { scope: "openid profile email" } },
      idToken: true, checks: ["state"],
      profile(profile) {
        return { id: profile.sub, name: profile.name, email: profile.email };
      },
    }],
  });

  // .env.local
  // IDCAMIM_CLIENT_ID=camim_SEU_CLIENT_ID
  // IDCAMIM_CLIENT_SECRET=SEU_CLIENT_SECRET
  // NEXTAUTH_SECRET=outra-chave-aleatoria
  // NEXTAUTH_URL=https://seusistema.com.br

Logout

Destrua a sessão local e redirecione para o IDCamim:

https://auth.camim.com.br/session/end
    ?id_token_hint=ID_TOKEN_DO_USUARIO
    &post_logout_redirect_uri=https://seusistema.com.br
ParâmetroDescrição
id_token_hintO id_token recebido na etapa 3. Identifica a sessão.
post_logout_redirect_uriPara onde redirecionar após logout. Deve estar cadastrada.

Refresh Token

Solicite offline_access no scope para receber um refresh_token (válido 30 dias).

curl -X POST https://auth.camim.com.br/token \
    -u "camim_SEU_CLIENT_ID:SEU_CLIENT_SECRET" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=refresh_token" \
    -d "refresh_token=SEU_REFRESH_TOKEN"
TokenValidade
access_token1 hora
refresh_token30 dias
Sessão no IDCamim14 dias

Introspecção de token

Valide um access_token a partir do backend (útil em microsserviços):

curl -X POST https://auth.camim.com.br/token/introspection \
    -u "camim_SEU_CLIENT_ID:SEU_CLIENT_SECRET" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "token=ACCESS_TOKEN_A_VERIFICAR"

Resposta — token válido:

{ "active": true, "sub": "uuid...", "scope": "openid profile email", "exp": 1742601600 }

Resposta — token inválido ou expirado:

{ "active": false }

FAQ

O usuário precisa ter conta no meu sistema?

Não. No primeiro login, use o sub para criar o registro automaticamente.

Por que usar o sub e não o e-mail como identificador?

O sub é um UUID imutável. E-mails podem mudar. Sempre associe pelo sub.

Por que a troca de código deve ser feita no backend?

Porque exige o client_secret. Se feita no frontend, o secret fica exposto a qualquer um. Nunca envie o secret ao navegador.

O token expirou. E agora?

Se você solicitou offline_access, use o refresh_token. Caso contrário, redirecione para login — como o IDCamim mantém sessão por 14 dias, normalmente é transparente.

Posso usar PKCE?

Sim — S256 e plain são suportados. Recomendado para SPAs e apps mobile onde não é possível guardar um client_secret.

Qual Content-Type usar no /token?

application/x-www-form-urlencoded — não JSON. O curl faz isso automaticamente com -d.

Quais domínios de e-mail são aceitos?

Somente domínios autorizados pelo administrador em auth.camim.com.br/painel/dominios.