Integração com o IDCamim
Guia completo para conectar qualquer sistema ao Login Único Camim. Autentique usuários @camim sem gerenciar senhas próprias.
Como funciona o fluxo
O IDCamim usa o Authorization Code Flow — o mais seguro para aplicações web com backend.
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
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/painel → Sistemas → Registrar 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âmetro | Valor | Descrição |
|---|---|---|
| client_id | camim_... | ID do seu sistema |
| redirect_uri | URL cadastrada | Exatamente igual à cadastrada no registro |
| response_type | code | Sempre code |
| scope | openid profile email | Dados que você quer acessar |
| state | string aleatória | Salve na sessão para verificar depois (anti-CSRF) |
| code_challenge | string (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"
}
| Campo | Descrição |
|---|---|
| access_token | Token opaco para acessar /me. Dura 1 hora. |
| id_token | JWT assinado ES256 com dados básicos do usuário. |
| expires_in | Segundos até expirar (3600 = 1h). |
| refresh_token | Presente 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.
| Scope | Campos retornados | Obrigatório? |
|---|---|---|
| openid | sub, iss, aud, exp, iat | Sim — sempre |
email, email_verified | Não | |
| profile | name, given_name, family_name, picture, updated_at | Não |
| phone | phone_number | Não |
| offline_access | Gera 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âmetro | Descrição |
|---|---|
| id_token_hint | O id_token recebido na etapa 3. Identifica a sessão. |
| post_logout_redirect_uri | Para 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"
| Token | Validade |
|---|---|
| access_token | 1 hora |
| refresh_token | 30 dias |
| Sessão no IDCamim | 14 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.