Skip to content

Commit

Permalink
fix: add openid federation jwtVerifier
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Jul 12, 2024
1 parent dacf629 commit a5755ff
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 60 deletions.
2 changes: 1 addition & 1 deletion packages/siop-oid4vp/lib/helpers/jwtUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { JwtHeader, JwtPayload, SIOPErrors } from '../types'

export type JwtType = 'id-token' | 'request-object' | 'verifier-attestation'

export type JwtProtectionMethod = 'did' | 'x5c' | 'jwk' | 'custom'
export type JwtProtectionMethod = 'did' | 'x5c' | 'jwk' | 'openid-federation' | 'custom'

export function parseJWT(jwt: string) {
const header = jwtDecode<JwtHeader>(jwt, { header: true })
Expand Down
11 changes: 7 additions & 4 deletions packages/siop-oid4vp/lib/types/Errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
enum SIOPErrors {
// todo: INVALID_REQUEST mapping onto response conforming to spec
INVALID_CLIENT_ID_MUST_MATCH_REDIRECT_URI = `Invalid request object payload. The redirect_uri must match the client_id with client_id_scheme 'redirect_uri'.`,
INVALID_REQUEST = 'The request contained invalid or conflicting parameters',
AUTH_REQUEST_EXPECTS_VP = 'authentication request expects a verifiable presentation in the response',
AUTH_REQUEST_DOESNT_EXPECT_VP = "authentication request doesn't expect a verifiable presentation in the response",
Expand All @@ -13,18 +14,20 @@ enum SIOPErrors {
NO_RESPONSE = 'No response (payload) provided.',
NO_PRESENTATION_SUBMISSION = 'The VP did not contain a presentation submission. Did you forget to call PresentationExchange.checkSubmissionFrom?',
BAD_VERIFIER_ATTESTATION = 'Invalid verifier attestation. Bad JWT structure.',
BAD_VERIFIER_ATTESTATION_REDIRECT_URIS = `Invalid verifier attestation. redirect_uri cannot be found in the the attestation jwts's redirect_uris.`,
CREDENTIAL_FORMATS_NOT_SUPPORTED = 'CREDENTIAL_FORMATS_NOT_SUPPORTED',
CREDENTIALS_FORMATS_NOT_PROVIDED = 'Credentials format not provided by RP/OP',
COULD_NOT_FIND_VCS_MATCHING_PD = 'Could not find VerifiableCredentials matching presentationDefinition object in the provided VC list',
DIDAUTH_REQUEST_PAYLOAD_NOT_CREATED = 'DidAuthRequestPayload not created',
DID_METHODS_NOT_SUPORTED = 'DID_METHODS_NOT_SUPPORTED',
ERROR_VERIFYING_SIGNATURE = 'Error verifying the DID Auth Token signature.',
INVALID_JWT = 'Received an invalid JWT.',
INVALID_REQUEST_OBJECT_X509_SCHEME_JWT = `Request Object uses client_id_scheme 'x509_san_dns' | 'x509_san_uri', but now x5c header is present.`,
INVALID_REQUEST_OBJECT_DID_SCHEME_JWT = `Request Object uses client_id_scheme 'did', but now kid header is present.`,
MISSING_ATTESTATION_JWT = `Request Object uses client_id_scheme 'verifier_attestation', but now jwt header is present.`,
MISSING_ATTESTATION_JWT_TYP = `Request Object uses client_id_scheme 'verifier_attestation', but the jwt is not 'verifier-attestation+jwt'.`,
MISSING_X5C_HEADER_WITH_CLIENT_ID_SCHEME_X509 = `Missing x5c header with client_id_scheme 'x509_san_dns' | 'x509_san_uri'.`,
MISSING_KID_HEADER_WITH_CLIENT_ID_SCHEME_DID = `Missing kid header with client_id_scheme 'did'.`,
MISSING_ATTESTATION_JWT_WITH_CLIENT_ID_SCHEME_ATTESTATION = `Missing jwt header jwt with client_id_scheme 'verifier_attestation'.`,
MISSING_ATTESTATION_JWT_TYP = `Attestation JWT missing typ 'verifier-attestation+jwt'.`,
INVALID_CLIENT_ID_SCHEME = 'Invalid client_id_scheme.',
INVALID_REQUEST_OBJECT_ENTITY_ID_SCHEME_CLIENT_ID = `Request Object uses client_id_scheme 'entity_id', but the client_id is not a string.`,
EXPIRED = 'The token has expired',
INVALID_AUDIENCE = 'Audience is invalid. Should be a string value.',
NO_AUDIENCE = 'No audience found in JWT payload or not configured',
Expand Down
136 changes: 81 additions & 55 deletions packages/siop-oid4vp/lib/types/JwtVerifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ interface X5cJwtVerifier extends JwtVerifierBase {
issuer: string
}

interface OpenIdFederationjwtVerifier extends JwtVerifierBase {
method: 'openid-federation'

/**
* The OpenId federation Entity
*/
entityId: string
}

type JwkJwtVerifier =
| (JwtVerifierBase & {
method: 'jwk'
Expand All @@ -52,83 +61,99 @@ interface CustomJwtVerifier extends JwtVerifierBase {
method: 'custom'
}

export type JwtVerifier = DidJwtVerifier | X5cJwtVerifier | CustomJwtVerifier | JwkJwtVerifier
export type JwtVerifier = DidJwtVerifier | X5cJwtVerifier | CustomJwtVerifier | JwkJwtVerifier | OpenIdFederationjwtVerifier

export const getJwtVerifierWithContext = async (
jwt: { header: JwtHeader; payload: JwtPayload },
options: { type: JwtType },
): Promise<JwtVerifier> => {
const type = options.type
export const getDidJwtVerifier = (jwt: { header: JwtHeader; payload: JwtPayload }, options: { type: JwtType }): DidJwtVerifier => {
const { type } = options
if (!jwt.header.kid) throw new Error(`${SIOPErrors.INVALID_JWT} Missing kid header.`)

if (jwt.header.kid?.startsWith('did:')) {
if (!jwt.header.kid.includes('#')) {
throw new Error(`${SIOPErrors.INVALID_JWT}. '${type}' contains an invalid kid header.`)
}
return { method: 'did', didUrl: jwt.header.kid, type }
} else if (jwt.header.x5c) {
if (!Array.isArray(jwt.header.x5c) || jwt.header.x5c.length === 0 || !jwt.header.x5c.every((cert) => typeof cert === 'string')) {
throw new Error(`${SIOPErrors.INVALID_JWT}. '${type}' contains an invalid x5c header.`)
}
return { method: 'x5c', x5c: jwt.header.x5c, issuer: jwt.payload.iss, type }
} else if (jwt.header.jwk) {
if (typeof jwt.header.jwk !== 'object') {
throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' contains an invalid jwk header.`)
}
if (type !== 'id-token') {
// Users need to check if the iss claim matches an entity they trust
// for type === 'verifier-attestation'
return { method: 'jwk', type, jwk: jwt.header.jwk }
}
if (!jwt.header.kid.includes('#')) {
throw new Error(`${SIOPErrors.INVALID_JWT}. '${type}' contains an invalid kid header.`)
}
return { method: 'did', didUrl: jwt.header.kid, type: type }
}

if (typeof jwt.payload.sub_jwk !== 'string') {
throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' is missing the sub_jwk claim.`)
}
export const getX5cVerifier = (jwt: { header: JwtHeader; payload: JwtPayload }, options: { type: JwtType }): X5cJwtVerifier => {
const { type } = options
if (!jwt.header.x5c) throw new Error(`${SIOPErrors.INVALID_JWT} Missing x5c header.`)

const jwkThumbPrintUri = jwt.payload.sub_jwk
const digestAlgorithm = await getDigestAlgorithmFromJwkThumbprintUri(jwkThumbPrintUri)
const selfComputedJwkThumbPrintUri = await calculateJwkThumbprintUri(jwt.header.jwk as JWK, digestAlgorithm)
if (!Array.isArray(jwt.header.x5c) || jwt.header.x5c.length === 0 || !jwt.header.x5c.every((cert) => typeof cert === 'string')) {
throw new Error(`${SIOPErrors.INVALID_JWT}. '${type}' contains an invalid x5c header.`)
}
return { method: 'x5c', x5c: jwt.header.x5c, issuer: jwt.payload.iss, type: type }
}

if (selfComputedJwkThumbPrintUri !== jwkThumbPrintUri) {
throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' contains an invalid sub_jwk claim.`)
}
export const getJwkVerifier = async (jwt: { header: JwtHeader; payload: JwtPayload }, options: { type: JwtType }): Promise<JwkJwtVerifier> => {
const { type } = options
if (!jwt.header.jwk) throw new Error(`${SIOPErrors.INVALID_JWT} Missing jwk header.`)

if (typeof jwt.header.jwk !== 'object') {
throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' contains an invalid jwk header.`)
}
if (type !== 'id-token') {
// Users need to check if the iss claim matches an entity they trust
// for type === 'verifier-attestation'
return { method: 'jwk', type, jwk: jwt.header.jwk }
}

return { method: 'jwk', type, jwk: jwt.header.jwk, jwkThumbprint: jwt.payload.sub_jwk }
if (typeof jwt.payload.sub_jwk !== 'string') {
throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' missing sub_jwk claim.`)
}

return { method: 'custom', type }
const jwkThumbPrintUri = jwt.payload.sub_jwk
const digestAlgorithm = await getDigestAlgorithmFromJwkThumbprintUri(jwkThumbPrintUri)
const selfComputedJwkThumbPrintUri = await calculateJwkThumbprintUri(jwt.header.jwk as JWK, digestAlgorithm)

if (selfComputedJwkThumbPrintUri !== jwkThumbPrintUri) {
throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' contains an invalid sub_jwk claim.`)
}

return { method: 'jwk', type, jwk: jwt.header.jwk, jwkThumbprint: jwt.payload.sub_jwk }
}

export const getJwtVerifierWithContext = async (
jwt: { header: JwtHeader; payload: JwtPayload },
options: { type: JwtType },
): Promise<JwtVerifier> => {
const { header, payload } = jwt

if (header.kid?.startsWith('did:')) return getDidJwtVerifier({ header, payload }, options)
else if (jwt.header.x5c) return getX5cVerifier({ header, payload }, options)
else if (jwt.header.jwk) return getJwkVerifier({ header, payload }, options)

return { method: 'custom', type: options.type }
}

export type VerifyJwtCallback = (jwtVerifier: JwtVerifier, jwt: { header: JwtHeader; payload: JwtPayload; raw: string }) => Promise<boolean>

export const getRequestObjectJwtVerifier = async (
jwt: { header: JwtHeader; payload: RequestObjectPayload },
options: { type: 'request-object'; raw: string },
options: { raw: string },
): Promise<JwtVerifier> => {
const type = options.type
const type = 'request-object'

const clientIdScheme = jwt.payload.client_id_scheme
const clientId = jwt.payload.client_id

if (clientIdScheme === 'did') {
if (!jwt.header.kid) {
throw new Error(SIOPErrors.INVALID_REQUEST_OBJECT_DID_SCHEME_JWT)
}
if (!clientIdScheme) {
return getJwtVerifierWithContext(jwt, { type })
}

if (clientIdScheme === 'did') {
return getDidJwtVerifier(jwt, { type })
} else if (clientIdScheme === 'pre-registered') {
// All validations must be done manually
// The Verifier metadata is obtained using [RFC7591] or through out-of-band mechanisms.
return getJwtVerifierWithContext(jwt, { type })
} else if (clientIdScheme === 'x509_san_dns' || clientIdScheme === 'x509_san_uri') {
// Make sure that the jwt is x509 protected
if (!jwt.header.x5c) {
throw new Error(SIOPErrors.INVALID_REQUEST_OBJECT_X509_SCHEME_JWT)
}
return getJwtVerifierWithContext(jwt, { type })
return getX5cVerifier(jwt, { type })
} else if (clientIdScheme === 'redirect_uri') {
if (jwt.payload.redirect_uri && jwt.payload.redirect_uri !== clientId) {
throw new Error(`Invalid request object payload. The redirect_uri must match the client_id with client_id_scheme 'redirect_uri'.`)
throw new Error(SIOPErrors.INVALID_CLIENT_ID_MUST_MATCH_REDIRECT_URI)
}
if (options.raw.split('.').length > 2) {
throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' JWT must not not be signed.`)
}
if (options.raw.split('.').length > 2) throw new Error(`${SIOPErrors.INVALID_JWT} The '${type}' Jwt must not not be signed.`)
return getJwtVerifierWithContext(jwt, { type })
} else if (clientIdScheme === 'verifier_attestation') {
const verifierAttestationSubtype = 'verifier-attestation+jwt'
Expand Down Expand Up @@ -163,19 +188,20 @@ export const getRequestObjectJwtVerifier = async (
!jwt.payload.redirect_uri ||
!attestationPayload.redirect_uris.includes(jwt.payload.redirect_uri)
) {
throw new Error(`${SIOPErrors.BAD_VERIFIER_ATTESTATION} request object redirect_uri in not included in the verifier attestation jwt.`)
throw new Error(SIOPErrors.BAD_VERIFIER_ATTESTATION_REDIRECT_URIS)
}
}

// The iss claim value of the Verifier Attestation JWT MUST identify a party the Wallet trusts for issuing Verifier Attestation JWTs.
// If the Wallet cannot establish trust, it MUST refuse the request.
return { method: 'jwk', type, jwk: attestationPayload.cnf['jwk'] as JWK }
} else if (clientIdScheme === 'entity_id') {
// TODO!
throw new Error('Not implemented yet')
} else if (clientIdScheme) {
throw new Error(SIOPErrors.INVALID_CLIENT_ID_SCHEME)
if (!clientId.startsWith('http')) {
throw new Error(SIOPErrors.INVALID_REQUEST_OBJECT_ENTITY_ID_SCHEME_CLIENT_ID)
}

return { method: 'openid-federation', type, entityId: clientId }
}

return getJwtVerifierWithContext(jwt, { type })
throw new Error(SIOPErrors.INVALID_CLIENT_ID_SCHEME)
}

0 comments on commit a5755ff

Please sign in to comment.