Volver a Explorar
PatrónTécnicoIdentidad y CVIntermedio

OID4VCI Authorization Code — emisión interactiva

8 minVerificado · 2026-05-18

OID4VCI define dos flujos principales para emitir credenciales: Pre-Authorized Code (cubierto en otra pieza) y Authorization Code. Este artículo cubre el flujo Authorization Code, que es la variante para casos donde el ciudadano se autentica en el momento de pedir la credencial.

Es el modelo equivalente al OAuth 2.0 + OIDC tradicional, adaptado para emisión de credenciales.

Cuándo usar Authorization Code vs Pre-Auth

EscenarioFlujo recomendado
Ciudadano ya pasó un trámite previo (presencial o digital)Pre-Auth
Ciudadano se loguea en el momento para reclamar credencialAuth Code
Emisor sin sistema de pre-aprobación (login directo desde wallet)Auth Code
Emisión por verificador tercero (verifier emite credenciales)Auth Code

La intuición: si la autenticación del ciudadano sucede antes del flujo de wallet, usar Pre-Auth. Si sucede durante el flujo, usar Auth Code.

El flujo end-to-end

  1. 1
    Wallet descubre el emisor. Vía deep link, QR, o input manual. Obtiene la URL del Credential Issuer Metadata.
  2. 2
    Wallet pide la autorización. Redirige al endpoint /authorize del emisor con response_type=code + parámetros que describen qué credencial pide.
  3. 3
    Emisor autentica al ciudadano. Login + consentimiento explícito (vista típica de OAuth: "¿Acepta que Sovra Wallet reciba su Constancia de Domicilio?").
  4. 4
    Emisor redirige a la wallet con un authorization_code (one-shot, short-lived).
  5. 5
    Wallet intercambia code por token. POST /token con el code, recibe access_token + c_nonce.
  6. 6
    Wallet pide la credencial. POST /credential con el access_token + un proof firmado con la clave del holder.
  7. 7
    Emisor firma y devuelve. Verifica el proof, firma la credencial, la entrega.

El authorization code

El authorization code que recibe la wallet es un string corto (típicamente 32-64 chars) que:

  • Es one-shot: se invalida al primer uso.
  • Es short-lived: expira en 60-300 segundos.
  • Está vinculado a la sesión específica: solo intercambiable con el code_verifier PKCE correspondiente.

Esto es OAuth 2.0 estándar, no nada nuevo para OID4VCI.

PKCE — obligatorio en Auth Code

Para evitar el ataque de "authorization code interception", OID4VCI Auth Code requiere PKCE (RFC 7636). Esto agrega dos parámetros al flujo:

// En el momento de iniciar el flujo
const codeVerifier = randomString(64);
const codeChallenge = base64url(sha256(codeVerifier));

// Wallet redirect a /authorize
"https://salta.gob.ar/authorize?" +
  "response_type=code" +
  "&code_challenge=" + codeChallenge +
  "&code_challenge_method=S256" +
  ...;

// Wallet intercambia code por token
fetch("https://salta.gob.ar/token", {
  body: {
    code: authorizationCode,
    code_verifier: codeVerifier,  // PKCE check
    ...
  }
});

El emisor verifica que el code_verifier matchea el code_challenge original. Sin esto, un atacante que intercepta el code no puede intercambiarlo por token.

El parámetro scope para credenciales

Una diferencia importante con OAuth tradicional: en OID4VCI Auth Code, el scope indica qué credencial se está pidiendo:

scope=openid ConstanciaDomicilio
scope=openid LicenciaConducir Diploma

El emisor sabe por el scope qué credencial preparar. Si el ciudadano no tiene autorización para la credencial pedida, el flujo aborta.

Ejemplo de Authorization Endpoint

// /authorize - endpoint que recibe la solicitud inicial
async function handleAuthorize(req: Request) {
  const params = new URL(req.url).searchParams;
  const responseType = params.get("response_type");
  const scope = params.get("scope");
  const codeChallenge = params.get("code_challenge");
  const codeChallengeMethod = params.get("code_challenge_method");
  
  if (responseType !== "code") return error("unsupported_response_type");
  if (!codeChallenge) return error("invalid_request");
  
  // Identificar al usuario - típicamente vía login + sesión
  const user = await authenticateUser(req);
  if (!user) return redirectToLogin();
  
  // Verificar que el usuario puede recibir la credencial pedida
  const requestedCredentials = parseScope(scope);
  for (const cred of requestedCredentials) {
    if (!userCanReceive(user, cred)) return error("access_denied");
  }
  
  // Crear el authorization code
  const code = randomToken(32);
  await storeAuthorizationCode(code, {
    user_id: user.id,
    requested_credentials: requestedCredentials,
    code_challenge: codeChallenge,
    code_challenge_method: codeChallengeMethod,
    expires_in: 300,
  });
  
  // Redirect back to wallet with code
  return redirect(`${redirectUri}?code=${code}&state=${state}`);
}

Ejemplo de Token Endpoint

// /token - intercambio de code por access_token
async function handleToken(req: Request) {
  const params = await req.formData();
  const grant = params.get("grant_type");
  const code = params.get("code");
  const codeVerifier = params.get("code_verifier");
  
  if (grant !== "authorization_code") return error("unsupported_grant_type");
  
  const session = await getAuthorizationCode(code);
  if (!session || session.used) return error("invalid_grant");
  
  // Verificar PKCE
  const expectedChallenge = base64url(sha256(codeVerifier));
  if (expectedChallenge !== session.code_challenge) {
    return error("invalid_grant");
  }
  
  const accessToken = randomToken(32);
  const cNonce = randomToken(16);
  
  await markCodeUsed(code);
  await storeTokenSession(accessToken, {
    user_id: session.user_id,
    credentials: session.requested_credentials,
    c_nonce: cNonce,
    expires_in: 300,
  });
  
  return json({
    access_token: accessToken,
    token_type: "Bearer",
    expires_in: 300,
    c_nonce: cNonce,
    c_nonce_expires_in: 300,
  });
}

Los desafíos específicos de Auth Code

Cuatro desafíos diferenciados:

Referencias

Relacionados

Tagsoid4vciauth-codeoauthissuance