OAuth 2.1 Authorization Server: padrões práticos para implementações enterprise
OAuth 2.1 simplifica RFCs anteriores eliminando fluxos inseguros e padronizando práticas. Implementar authorization server production-ready exige compreensão de PKCE, proof-key-for-code-exchange, refresh tokens e integrações com IDPs corporativos.
Resumo executivo
OAuth 2.1 simplifica RFCs anteriores eliminando fluxos inseguros e padronizando práticas. Implementar authorization server production-ready exige compreensão de PKCE, proof-key-for-code-exchange, refresh tokens e integrações com IDPs corporativos.
Ultima atualizacao: 12/03/2026
O que mudou do OAuth 2.0 para OAuth 2.1
OAuth 2.1 (publicado como draft RFC em 2024, consolidado em 2025) simplifica e torna mais seguro o OAuth 2.0, removendo fluxos problemáticos e padronizando práticas que antes eram "deveria ser". Para engenheiros de segurança e auth, a mudança mais importante é que OAuth 2.1 torna obrigatório o que era recomendado.
Principais mudanças:
- Authorization Code Flow com PKCE agora é obrigatório para todos os clientes (não apenas public)
- Implicit Flow foi totalmente removido (obsoleto desde 2016)
- Resource Owner Password Credentials Grant foi removido
- Refresh Token Rotation é fortemente recomendado
- Token binding via TLS certificate é suportado
Authorization Code Flow com PKCE
Por que PKCE é obrigatório
PKCE (Proof Key for Code Exchange) previne ataques de authorization code interception. Mesmo em aplicações confidential (backend-to-backend), OAuth 2.1 exige PKCE para mitigar ataques de code injection.
Fluxo completo com PKCE
typescript// 1. Cliente gera code verifier e challenge
import { createHash, randomBytes } from 'crypto';
function generateCodeVerifier(): string {
return randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier: string): string {
const hash = createHash('sha256').update(verifier).digest();
return hash.toString('base64url');
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Armazenar codeVerifier temporariamente para o exchange step
session.codeVerifier = codeVerifier;typescript// 2. Redirect usuário para authorization endpoint
const authUrl = new URL('https://auth.example.com/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'my-client-id');
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('scope', 'openid profile email offline_access');
authUrl.searchParams.set('state', generateState()); // CSRF protection
window.location.href = authUrl.toString();typescript// 3. Trocar authorization code por tokens
async function exchangeCodeForTokens(code: string) {
const response = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa('my-client-id:client-secret')}`
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://myapp.com/callback',
code_verifier: session.codeVerifier,
client_id: 'my-client-id'
})
});
const tokens = await response.json();
return tokens;
}Implementação no authorization server
typescript// Authorization endpoint (simplificado)
app.get('/oauth/authorize', async (req, res) => {
const { response_type, client_id, redirect_uri, code_challenge, code_challenge_method, state, scope } = req.query;
// Validate client
const client = await clientStore.findById(client_id);
if (!client || !client.redirectUris.includes(redirect_uri)) {
return res.status(400).json({ error: 'invalid_request' });
}
// Validate PKCE parameters
if (!code_challenge || code_challenge_method !== 'S256') {
return res.status(400).json({ error: 'invalid_request' });
}
// Store authorization code with challenge
const authCode = generateSecureRandom();
await authCodeStore.save(authCode, {
clientId: client_id,
codeChallenge: code_challenge,
codeChallengeMethod: code_challenge_method,
userId: req.user.id,
expiresAt: new Date(Date.now() + 10 * 60 * 1000) // 10 minutos
});
// Show consent page (se necessário)
return res.render('consent', {
client: client,
scope: scope,
authCode: authCode,
state: state
});
});
// Token endpoint
app.post('/oauth/token', async (req, res) => {
const { grant_type, code, redirect_uri, code_verifier, client_id } = req.body;
// Retrieve and validate auth code
const authCodeData = await authCodeStore.findAndDelete(code);
if (!authCodeData || authCodeData.expiresAt < new Date()) {
return res.status(400).json({ error: 'invalid_grant' });
}
// Validate PKCE
const expectedChallenge = createHash('sha256')
.update(code_verifier)
.digest()
.toString('base64url');
if (authCodeData.codeChallenge !== expectedChallenge) {
return res.status(400).json({ error: 'invalid_grant' });
}
// Generate tokens
const accessToken = jwt.sign(
{ sub: authCodeData.userId, scope: authCodeData.scope },
config.jwtSecret,
{ expiresIn: '1h' }
);
const refreshToken = generateRefreshToken(authCodeData.userId, client_id);
res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: refreshToken,
scope: authCodeData.scope
});
});Refresh Token Rotation
Refresh tokens são alvos de ataques de roubo. OAuth 2.1 recomenda refresh token rotation: cada novo refresh token invalida o anterior.
Implementação de refresh token rotation
typescriptinterface RefreshToken {
id: string;
userId: string;
clientId: string;
token: string;
parentToken?: string; // Para rastrear rotation chain
revoked: boolean;
expiresAt: Date;
lastUsedAt: Date;
}
// Token endpoint - refresh token grant
app.post('/oauth/token', async (req, res) => {
const { grant_type, refresh_token } = req.body;
if (grant_type !== 'refresh_token') {
return handleOtherGrantTypes(req, res);
}
// Validate refresh token
const storedToken = await refreshTokenStore.findByToken(refresh_token);
if (!storedToken || storedToken.revoked || storedToken.expiresAt < new Date()) {
return res.status(400).json({ error: 'invalid_grant' });
}
// Detectar token reuso (possível ataque)
if (storedToken.parentToken) {
// Token pai já foi usado, indicando possível reuso
await refreshTokenStore.revokeFamily(storedToken.id);
await sessionStore.revokeAllUserSessions(storedToken.userId);
return res.status(400).json({ error: 'invalid_grant' });
}
// Generate new access token
const accessToken = jwt.sign(
{ sub: storedToken.userId, scope: storedToken.scope },
config.jwtSecret,
{ expiresIn: '1h' }
);
// Generate new refresh token
const newRefreshToken = generateSecureRandom();
await refreshTokenStore.save({
id: generateId(),
userId: storedToken.userId,
clientId: storedToken.clientId,
token: newRefreshToken,
parentToken: storedToken.id, // Rastrear rotation
revoked: false,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 dias
lastUsedAt: new Date()
});
// Revoke old refresh token
await refreshTokenStore.revoke(storedToken.id);
res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: newRefreshToken,
scope: storedToken.scope
});
});
// Token store com rotation tracking
class RefreshTokenStore {
async save(token: RefreshToken): Promise<void> {
await db.refreshTokens.insert(token);
}
async findByToken(token: string): Promise<RefreshToken | null> {
return await db.refreshTokens.findOne({ token, revoked: false });
}
async revoke(tokenId: string): Promise<void> {
await db.refreshTokens.updateOne(
{ id: tokenId },
{ revoked: true }
);
}
async revokeFamily(tokenId: string): Promise<void> {
// Revoke todos os tokens na chain de rotation
const token = await db.refreshTokens.findOne({ id: tokenId });
if (token.parentToken) {
await this.revokeFamily(token.parentToken);
}
await this.revoke(tokenId);
await db.refreshTokens.updateMany(
{ parentToken: tokenId },
{ revoked: true }
);
}
async revokeAllUserSessions(userId: string): Promise<void> {
await db.refreshTokens.updateMany(
{ userId },
{ revoked: true }
);
}
}OpenID Connect (OIDC) Configuration
OIDC é uma camada de identidade sobre OAuth 2.1, adicionando endpoints de userinfo, ID tokens e discovery.
Discovery endpoint
typescript// /.well-known/openid-configuration
app.get('/.well-known/openid-configuration', (req, res) => {
const baseUrl = `${req.protocol}://${req.get('host')}`;
res.json({
issuer: baseUrl,
authorization_endpoint: `${baseUrl}/oauth/authorize`,
token_endpoint: `${baseUrl}/oauth/token`,
userinfo_endpoint: `${baseUrl}/oauth/userinfo`,
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
response_types_supported: ['code'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256', 'ES256'],
scopes_supported: ['openid', 'profile', 'email', 'offline_access'],
claims_supported: [
'sub', 'name', 'given_name', 'family_name', 'email',
'email_verified', 'picture', 'updated_at'
],
code_challenge_methods_supported: ['S256'],
grant_types_supported: ['authorization_code', 'refresh_token'],
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post']
});
});
// JWKS endpoint (public keys for token verification)
app.get('/.well-known/jwks.json', (req, res) => {
res.json({
keys: [
{
kty: 'RSA',
use: 'sig',
alg: 'RS256',
n: keyPair.publicKey.n,
e: keyPair.publicKey.e,
kid: 'key-1'
}
]
});
});ID Token generation
typescriptfunction generateIdToken(userId: string, authCodeData: AuthCode, nonce?: string): string {
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: config.issuer,
sub: userId,
aud: authCodeData.clientId,
exp: now + 3600,
iat: now,
nonce: nonce,
auth_time: Math.floor(Date.now() / 1000),
email: authCodeData.userEmail,
email_verified: authCodeData.userEmailVerified
};
return jwt.sign(payload, keyPair.privateKey, { algorithm: 'RS256', keyid: 'key-1' });
}Userinfo endpoint
typescriptapp.get('/oauth/userinfo', authenticateBearerToken, async (req, res) => {
const userId = req.token.sub;
const user = await userStore.findById(userId);
if (!user) {
return res.status(404).json({ error: 'user_not_found' });
}
res.json({
sub: user.id,
name: user.name,
given_name: user.givenName,
family_name: user.familyName,
email: user.email,
email_verified: user.emailVerified,
picture: user.picture,
updated_at: user.updatedAt
});
});Device Authorization Flow
Device Authorization Flow permite autenticação em dispositivos com input limitado (Smart TVs, IoT, CLIs).
Authorization endpoint
typescriptapp.post('/oauth/device_authorization', async (req, res) => {
const { client_id, scope } = req.body;
const userCode = generateUserCode(); // Ex: "ABCD-EFGH"
const deviceCode = generateSecureRandom();
const verificationUri = `https://auth.example.com/device`;
await deviceCodeStore.save({
deviceCode,
userCode,
clientId: client_id,
scope: scope || 'openid profile',
expiresAt: new Date(Date.now() + 15 * 60 * 1000), // 15 minutos
pollingInterval: 5, // 5 segundos
interval: 5,
status: 'pending'
});
res.json({
device_code: deviceCode,
user_code: userCode,
verification_uri: verificationUri,
verification_uri_complete: `${verification_uri}?user_code=${userCode}`,
expires_in: 900,
interval: 5
});
});Token endpoint (device grant)
typescriptapp.post('/oauth/token', async (req, res) => {
const { grant_type, device_code, client_id } = req.body;
if (grant_type === 'urn:ietf:params:oauth:grant-type:device_code') {
const deviceAuth = await deviceCodeStore.findByDeviceCode(device_code);
if (!deviceAuth || deviceAuth.expiresAt < new Date()) {
return res.status(400).json({ error: 'expired_token' });
}
if (deviceAuth.status === 'pending') {
return res.status(400).json({
error: 'authorization_pending',
interval: deviceAuth.interval
});
}
if (deviceAuth.status === 'denied') {
return res.status(400).json({ error: 'access_denied' });
}
// Generate tokens
const accessToken = jwt.sign(
{ sub: deviceAuth.userId, scope: deviceAuth.scope },
config.jwtSecret,
{ expiresIn: '1h' }
);
res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
scope: deviceAuth.scope
});
}
});Client Authentication Methods
OAuth 2.1 suporta múltiplos métodos de autenticação de cliente no token endpoint.
Client Secret Basic (HTTP Basic Auth)
typescriptfunction authenticateClientBasicAuth(authHeader: string): Client | null {
if (!authHeader || !authHeader.startsWith('Basic ')) {
return null;
}
const credentials = Buffer.from(
authHeader.substring(6),
'base64'
).toString('utf-8');
const [clientId, clientSecret] = credentials.split(':');
return clientStore.findByCredentials(clientId, clientSecret);
}
// No token endpoint
app.post('/oauth/token', (req, res) => {
const authHeader = req.headers.authorization;
const client = authenticateClientBasicAuth(authHeader);
if (!client) {
return res.status(401).json({
error: 'invalid_client',
error_description: 'Client authentication failed'
});
}
// Continue com token generation
});Client Secret Post
typescriptfunction authenticateClientPostAuth(clientId: string, clientSecret: string): Client | null {
return clientStore.findByCredentials(clientId, clientSecret);
}
app.post('/oauth/token', express.urlencoded({ extended: false }), (req, res) => {
const { client_id, client_secret } = req.body;
const client = authenticateClientPostAuth(client_id, client_secret);
if (!client) {
return res.status(401).json({ error: 'invalid_client' });
}
// Continue
});Private Key JWT (mutual TLS)
typescriptasync function authenticateClientJwt(assertion: string): Promise<Client | null> {
try {
const decoded = jwt.verify(assertion, config.jwksEndpoint, {
algorithms: ['RS256', 'ES256'],
issuer: decoded.payload.iss,
audience: decoded.payload.aud
});
const client = await clientStore.findById(decoded.payload.sub);
if (!client) return null;
// Validate JWT claims
if (decoded.payload.exp < Date.now() / 1000) return null;
if (decoded.payload.jti && await jtiStore.exists(decoded.payload.jti)) {
return null; // Replay attack
}
return client;
} catch (error) {
return null;
}
}Integração com Enterprise IDPs
Configuração de identity provider externo
typescriptinterface IdentityProvider {
id: string;
name: string;
type: 'saml2' | 'oidc' | 'ldap';
config: {
clientId?: string;
clientSecret?: string;
discoveryUrl?: string;
entityId?: string;
metadataUrl?: string;
attributeMapping: Record<string, string>;
};
}
// OIDC IDP integration
class OIDCIdentityProvider {
async discover(configUrl: string): Promise<OIDCConfig> {
const response = await fetch(`${configUrl}/.well-known/openid-configuration`);
return await response.json();
}
async exchangeCode(code: string, redirectUri: string): Promise<TokenResponse> {
const response = await fetch(`${this.config.tokenEndpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
})
});
return await response.json();
}
async getUserInfo(accessToken: string): Promise<UserProfile> {
const response = await fetch(`${this.config.userinfoEndpoint}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
return await response.json();
}
}
// SAML2 IDP integration
class SAML2IdentityProvider {
async getAuthRequest(relyingPartyId: string): Promise<string> {
const authRequest = this.samlLib.createAuthRequest({
ssoUrl: this.config.ssoUrl,
entityId: relyingPartyId,
idpEntityId: this.config.entityId
});
return authRequest;
}
async validateResponse(samlResponse: string): Promise<SAMLAttributes> {
const profile = await this.samlLib.validateResponse(
samlResponse,
this.config.metadataUrl
);
return profile.attributes;
}
}Brokering: nosso authorization server como broker
typescript// Broker endpoint que redireciona para IDP externo
app.get('/oauth/authorize', async (req, res) => {
const { idp_hint, client_id, redirect_uri, state } = req.query;
const idp = await idpStore.findById(idp_hint as string);
if (!idp) {
return res.status(400).json({ error: 'invalid_idp' });
}
// Generate state para nosso authorization server
const brokerState = generateSecureRandom();
await brokerStateStore.save(brokerState, {
clientId: client_id,
redirectUri: redirect_uri,
state: state,
idpId: idp.id
});
// Redirecionar para IDP externo
if (idp.type === 'oidc') {
const oidcIdp = new OIDCIdentityProvider(idp);
const authUrl = await oidcIdp.buildAuthUrl(brokerState);
return res.redirect(authUrl);
} else if (idp.type === 'saml2') {
const samlIdp = new SAML2IdentityProvider(idp);
const authRequest = await samlIdp.getAuthRequest(client_id);
return res.redirect(authRequest);
}
});
// Callback do IDP externo
app.get('/oauth/callback', async (req, res) => {
const { code, state: brokerState } = req.query;
const stateData = await brokerStateStore.findAndDelete(brokerState);
if (!stateData) {
return res.status(400).json({ error: 'invalid_state' });
}
const idp = await idpStore.findById(stateData.idpId);
// Exchange code com IDP externo
let externalUserProfile;
if (idp.type === 'oidc') {
const oidcIdp = new OIDCIdentityProvider(idp);
const tokens = await oidcIdp.exchangeCode(code, stateData.redirectUri);
externalUserProfile = await oidcIdp.getUserInfo(tokens.access_token);
}
// Map attributes to local user
const localUser = await userStore.findOrCreateByExternalId(
idp.id,
externalUserProfile.sub,
{
email: externalUserProfile.email,
name: externalUserProfile.name
}
);
// Generate local authorization code
const authCode = generateSecureRandom();
await authCodeStore.save(authCode, {
clientId: stateData.clientId,
userId: localUser.id,
scope: 'openid profile email',
expiresAt: new Date(Date.now() + 10 * 60 * 1000)
});
// Redirect to original callback
const callbackUrl = new URL(stateData.redirectUri);
callbackUrl.searchParams.set('code', authCode);
callbackUrl.searchParams.set('state', stateData.state);
return res.redirect(callbackUrl.toString());
});Token Introspection e Revocation
Token introspection endpoint
typescriptapp.post('/oauth/introspect', authenticateClient, async (req, res) => {
const { token, token_type_hint } = req.body;
// Check if access token
let tokenInfo;
try {
const decoded = jwt.verify(token, config.jwtSecret);
tokenInfo = {
active: true,
sub: decoded.sub,
scope: decoded.scope,
client_id: decoded.clientId,
exp: decoded.exp,
iat: decoded.iat
};
} catch (error) {
tokenInfo = { active: false };
}
res.json(tokenInfo);
});Token revocation endpoint
typescriptapp.post('/oauth/revoke', authenticateClient, async (req, res) => {
const { token, token_type_hint } = req.body;
// Revoke refresh token
await refreshTokenStore.revokeByToken(token);
// Add access token to blacklist (com TTL)
try {
const decoded = jwt.verify(token, config.jwtSecret);
await tokenBlacklist.add(token, decoded.exp);
} catch (error) {
// Token already invalid
}
res.status(200).end();
});Security Hardening Checklist
Mandatory security practices
yamloauth_21_security_checklist:
transport_security:
- Enforce HTTPS (no HTTP endpoints)
- HSTS headers configured
- TLS 1.3 minimum
- Certificate pinning for mobile apps
token_security:
- PKCE mandatory for all clients
- Refresh token rotation implemented
- Short access token expiration (5-60 minutes)
- Long refresh token expiration (30-90 days)
- Token introspection available
- Token revocation endpoint
client_security:
- Client secret hashed in storage
- Client authentication required (except public clients)
- Redirect URI whitelist enforced
- Post-logout redirect URI whitelist
session_management:
- Absolute session timeout (24h)
- Idle session timeout (30m)
- Concurrent session limits
- Session revocation on password change
- Device fingerprinting for anomaly detection
rate_limiting:
- Authorization endpoint: 10 requests/min per IP
- Token endpoint: 5 requests/min per client
- Userinfo endpoint: 100 requests/min per access token
- Introspection: 1000 requests/min per client
monitoring:
- Failed authentication attempts logged
- Suspicious token usage patterns detected
- Client credential compromise alerts
- Token leakage detection (replay attacks)Conclusão
Implementar um Authorization Server production-ready com OAuth 2.1 exige mais que conhecimento dos fluxos básicos — exige implementação correta de PKCE, refresh token rotation, OIDC integration e segurança robusta. Padrões enterprise adicionam complexidade de integrar com IDPs externos, gerenciar múltiplos identity providers e manter audit logging completo.
Organizações que implementam OAuth 2.1 corretamente reduzem significativamente a superfície de ataque de autenticação enquanto fornecem experiência de usuário moderna.
Precisa implementar Authorization Server OAuth 2.1 com integrações enterprise IDPs? Fale com especialistas em segurança da Imperialis para implementar uma arquitetura de autenticação production-ready com OAuth 2.1, OIDC e integrações corporativas.
Fontes
- OAuth 2.1 RFC draft — accessed on 2026-03
- OpenID Connect Core 1.0 — accessed on 2026-03
- OAuth 2.0 for Browser-Based Apps — accessed on 2026-03
- OAuth Token Revocation RFC 7009 — accessed on 2026-03