@sphereon/jarm
Version:
Sphereon JARM
107 lines (86 loc) • 4.1 kB
text/typescript
import { decodeProtectedHeader, isJwe, isJws } from '@sphereon/oid4vc-common';
import * as v from 'valibot';
import type { JarmDirectPostJwtResponseParams } from '../index.js';
import type { AuthRequestParams, JarmDirectPostJwtAuthResponseValidationContext } from './c-jarm-auth-response.js';
import { vJarmAuthResponseErrorParams } from './v-jarm-auth-response-params.js';
import { jarmAuthResponseDirectPostValidateParams, vJarmDirectPostJwtParams } from './v-jarm-direct-post-jwt-auth-response-params.js';
export interface JarmDirectPostJwtAuthResponseValidation {
/**
* The JARM response parameter conveyed either as url query param, fragment param, or application/x-www-form-urlencoded in the body of a post request
*/
response: string;
}
const parseJarmAuthResponseParams = <Schema extends v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>>(
schema: Schema,
responseParams: unknown,
) => {
if (v.is(vJarmAuthResponseErrorParams, responseParams)) {
const errorResponseJson = JSON.stringify(responseParams, undefined, 2);
throw new Error(`Received error response from authorization server. '${errorResponseJson}'`);
}
return v.parse(schema, responseParams);
};
const decryptJarmAuthResponse = async (input: { response: string }, ctx: JarmDirectPostJwtAuthResponseValidationContext) => {
const { response } = input;
const responseProtectedHeader = decodeProtectedHeader(response);
if (!responseProtectedHeader.kid) {
throw new Error(`Jarm JWE is missing the protected header field 'kid'.`);
}
const { plaintext } = await ctx.jwe.decryptCompact({
jwe: response,
jwk: { kid: responseProtectedHeader.kid },
});
return plaintext;
};
/**
* Validate a JARM direct_post.jwt compliant authentication response
* * The decryption key should be resolvable using the the protected header's 'kid' field
* * The signature verification jwk should be resolvable using the jws protected header's 'kid' field and the payload's 'iss' field.
*/
export const jarmAuthResponseDirectPostJwtValidate = async (
input: JarmDirectPostJwtAuthResponseValidation,
ctx: JarmDirectPostJwtAuthResponseValidationContext,
) => {
const { response } = input;
const responseIsEncrypted = isJwe(response);
const decryptedResponse = responseIsEncrypted ? await decryptJarmAuthResponse(input, ctx) : response;
const responseIsSigned = isJws(decryptedResponse);
if (!responseIsEncrypted && !responseIsSigned) {
throw new Error('Jarm Auth Response must be either encrypted, signed, or signed and encrypted.');
}
let authResponseParams: JarmDirectPostJwtResponseParams;
let authRequestParams: AuthRequestParams;
if (responseIsSigned) {
throw new Error('Signed JARM responses are not supported.');
//const jwsProtectedHeader = decodeProtectedHeader(decryptedResponse);
//const jwsPayload = decodeJwt(decryptedResponse);
//const schema = v.required(vJarmDirectPostJwtParams, ['iss', 'aud', 'exp']);
//const responseParams = parseJarmAuthResponseParams(schema, jwsPayload);
//({ authRequestParams } = await ctx.openid4vp.authRequest.getParams(responseParams));
//if (!jwsProtectedHeader.kid) {
//throw new Error(`Jarm JWS is missing the protected header field 'kid'.`);
//}
//await ctx.jose.jws.verifyJwt({
//jws: decryptedResponse,
//jwk: { kid: jwsProtectedHeader.kid, kty: 'auto' },
//});
//authResponseParams = responseParams;
} else {
const jsonResponse: unknown = JSON.parse(decryptedResponse);
authResponseParams = parseJarmAuthResponseParams(vJarmDirectPostJwtParams, jsonResponse);
({ authRequestParams } = await ctx.openid4vp.authRequest.getParams(authResponseParams));
}
jarmAuthResponseDirectPostValidateParams({
authRequestParams,
authResponseParams,
});
let type: 'signed encrypted' | 'encrypted' | 'signed';
if (responseIsSigned && responseIsEncrypted) type = 'signed encrypted';
else if (responseIsEncrypted) type = 'encrypted';
else type = 'signed';
return {
authRequestParams,
authResponseParams,
type,
};
};