Seguranca e resiliencia

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.

12/03/20269 min de leituraSeguranca
OAuth 2.1 Authorization Server: padrões práticos para implementações enterprise

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

Leituras relacionadas