OID4VCI Authorization Code — emisión interactiva
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
| Escenario | Flujo recomendado |
|---|---|
| Ciudadano ya pasó un trámite previo (presencial o digital) | Pre-Auth |
| Ciudadano se loguea en el momento para reclamar credencial | Auth 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
- 1Wallet descubre el emisor. Vía deep link, QR, o input manual. Obtiene la URL del Credential Issuer Metadata.
- 2Wallet pide la autorización. Redirige al endpoint
/authorizedel emisor conresponse_type=code+ parámetros que describen qué credencial pide. - 3Emisor autentica al ciudadano. Login + consentimiento explícito (vista típica de OAuth: "¿Acepta que Sovra Wallet reciba su Constancia de Domicilio?").
- 4Emisor redirige a la wallet con un
authorization_code(one-shot, short-lived). - 5Wallet intercambia code por token.
POST /tokencon el code, recibeaccess_token+c_nonce. - 6Wallet pide la credencial.
POST /credentialcon el access_token + un proof firmado con la clave del holder. - 7Emisor 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_verifierPKCE 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
- Cómo emite credenciales un sistema con OID4VCI — el flujo alternativo
- OID4VP — protocolo de presentación de credenciales — el flujo de presentación
- W3C Verifiable Credentials Data Model 2.0 — el formato de la credencial emitida

