Skip to content

Commit

Permalink
Merge branch 'develop' into feature/SPRIND-89
Browse files Browse the repository at this point in the history
# Conflicts:
#	packages/issuer-rest/lib/oid4vci-api-functions.ts
  • Loading branch information
Brummos committed Jan 9, 2025
2 parents 9c273b9 + f61d6d1 commit f3b89fc
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 54 deletions.
4 changes: 2 additions & 2 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
getTypesFromObject,
KID_JWK_X5C_ERROR,
NotificationRequest,
NotificationResult,
NotificationResponseResult,
OID4VCICredentialFormat,
OpenId4VCIVersion,
PKCEOpts,
Expand Down Expand Up @@ -531,7 +531,7 @@ export class OpenID4VCIClient {
credentialRequestOpts: Partial<CredentialRequestOpts>,
request: NotificationRequest,
accessToken?: string,
): Promise<NotificationResult> {
): Promise<NotificationResponseResult> {
return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/OpenID4VCIClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
getTypesFromCredentialSupported,
KID_JWK_X5C_ERROR,
NotificationRequest,
NotificationResult,
NotificationResponseResult,
OID4VCICredentialFormat,
OpenId4VCIVersion,
PKCEOpts,
Expand Down Expand Up @@ -577,7 +577,7 @@ export class OpenID4VCIClientV1_0_13 {
credentialRequestOpts: Partial<CredentialRequestOpts>,
request: NotificationRequest,
accessToken?: string,
): Promise<NotificationResult> {
): Promise<NotificationResponseResult> {
return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/functions/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NotificationErrorResponse, NotificationRequest, NotificationResult, post } from '@sphereon/oid4vci-common';
import { NotificationErrorResponse, NotificationRequest, NotificationResponseResult, post } from '@sphereon/oid4vci-common';

import { CredentialRequestOpts } from '../CredentialRequestClient';
import { LOG } from '../types';
Expand All @@ -7,7 +7,7 @@ export async function sendNotification(
credentialRequestOpts: Partial<CredentialRequestOpts>,
request: NotificationRequest,
accessToken?: string,
): Promise<NotificationResult> {
): Promise<NotificationResponseResult> {
LOG.info(`Sending status notification event '${request.event}' for id ${request.notification_id}`);
if (!credentialRequestOpts.notificationEndpoint) {
throw Error(`Cannot send notification when no notification endpoint is provided`);
Expand Down
2 changes: 1 addition & 1 deletion packages/did-auth-siop-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"typescript": "5.4.5"
"typescript": "5.4.5"
},
"engines": {
"node": ">=18"
Expand Down
47 changes: 47 additions & 0 deletions packages/issuer-rest/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
export * from './OID4VCIServer'
export * from './oid4vci-api-functions'
export * from './expressUtils'


/**
* Copied from openid-client
*/
export type ResponseType = 'code' | 'id_token' | 'code id_token' | 'none' | string;
export type ClientAuthMethod =
| 'client_secret_basic'
| 'client_secret_post'
| 'client_secret_jwt'
| 'private_key_jwt'
| 'tls_client_auth'
| 'self_signed_tls_client_auth'
| 'none';
export interface ClientMetadata {
// important
client_id: string;
id_token_signed_response_alg?: string;
token_endpoint_auth_method?: ClientAuthMethod;
client_secret?: string;
redirect_uris?: string[];
response_types?: ResponseType[];
post_logout_redirect_uris?: string[];
default_max_age?: number;
require_auth_time?: boolean;
tls_client_certificate_bound_access_tokens?: boolean;
request_object_signing_alg?: string;

// less important
id_token_encrypted_response_alg?: string;
id_token_encrypted_response_enc?: string;
introspection_endpoint_auth_method?: ClientAuthMethod;
introspection_endpoint_auth_signing_alg?: string;
request_object_encryption_alg?: string;
request_object_encryption_enc?: string;
revocation_endpoint_auth_method?: ClientAuthMethod;
revocation_endpoint_auth_signing_alg?: string;
token_endpoint_auth_signing_alg?: string;
userinfo_encrypted_response_alg?: string;
userinfo_encrypted_response_enc?: string;
userinfo_signed_response_alg?: string;
authorization_encrypted_response_alg?: string;
authorization_encrypted_response_enc?: string;
authorization_signed_response_alg?: string;

[key: string]: unknown;
}
88 changes: 74 additions & 14 deletions packages/issuer-rest/lib/oid4vci-api-functions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { uuidv4 } from '@sphereon/oid4vc-common'
import { decodeJwt, decodeProtectedHeader, uuidv4 } from '@sphereon/oid4vc-common'
import {
ACCESS_TOKEN_ISSUER_REQUIRED_ERROR,
AccessTokenRequest,
adjustUrl,
AuthorizationChallengeCodeResponse,
AuthorizationChallengeError,
AuthorizationChallengeErrorResponse,
AuthorizationRequest,
CommonAuthorizationChallengeRequest,
CredentialIssuerMetadataOptsV1_0_13,
CredentialOfferRESTRequest,
CredentialRequestV1_0_13,
determineGrantTypes,
Expand All @@ -18,6 +20,9 @@ import {
Grant,
IssueStatusResponse,
JWT_SIGNER_CALLBACK_REQUIRED_ERROR,
JWTHeader,
JWTVerifyCallback,
JwtVerifyResult,
NotificationRequest,
NotificationStatusEventNames,
OpenId4VCIVersion,
Expand All @@ -33,10 +38,11 @@ import {
LOG,
VcIssuer
} from '@sphereon/oid4vci-issuer'
import { env, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi-express-support'
import { env, ISingleEndpointOpts, oidcDiscoverIssuer, oidcGetClient, sendErrorResponse } from '@sphereon/ssi-express-support'
import { InitiatorType, SubSystem, System } from '@sphereon/ssi-types'
import { NextFunction, Request, Response, Router } from 'express'


import { handleTokenRequest, verifyTokenRequest } from './IssuerTokenEndpoint'
import {
IAuthorizationChallengeEndpointOpts,
Expand All @@ -47,6 +53,8 @@ import {
} from './OID4VCIServer'
import { validateRequestBody } from './expressUtils'

import { ClientMetadata } from './index'

const expiresIn = process.env.EXPIRES_IN ? parseInt(process.env.EXPIRES_IN) : 90

export function getIssueStatusEndpoint<DIDDoc extends object>(router: Router, issuer: VcIssuer<DIDDoc>, opts: IGetIssueStatusEndpointOpts) {
Expand Down Expand Up @@ -85,6 +93,10 @@ export function getIssueStatusEndpoint<DIDDoc extends object>(router: Router, is
})
}


function isExternalAS(issuerMetadata: CredentialIssuerMetadataOptsV1_0_13) {
return issuerMetadata.authorization_servers?.some((as) => !as.includes(issuerMetadata.credential_issuer))
}
export function authorizationChallengeEndpoint<DIDDoc extends object>(
router: Router,
issuer: VcIssuer<DIDDoc>,
Expand Down Expand Up @@ -183,15 +195,19 @@ export function accessTokenEndpoint<DIDDoc extends object>(
opts: ITokenEndpointOpts & ISingleEndpointOpts & { baseUrl: string | URL },
) {
const tokenEndpoint = issuer.issuerMetadata.token_endpoint
const externalAS = issuer.issuerMetadata.authorization_servers
const externalAS = isExternalAS(issuer.issuerMetadata)
if (externalAS) {
LOG.log(`[OID4VCI] External Authorization Server ${tokenEndpoint} is being used. Not enabling issuer token endpoint`)
return
} else if (opts?.enabled === false) {
LOG.log(`[OID4VCI] Token endpoint is not enabled`)
return
}
const accessTokenIssuer = opts?.accessTokenIssuer ?? process.env.ACCESS_TOKEN_ISSUER ?? issuer.issuerMetadata.credential_issuer
const accessTokenIssuer =
opts?.accessTokenIssuer ??
process.env.ACCESS_TOKEN_ISSUER ??
issuer.issuerMetadata.authorization_servers?.[0] ??
issuer.issuerMetadata.credential_issuer

const preAuthorizedCodeExpirationDuration =
opts?.preAuthorizedCodeExpirationDuration ?? getNumberOrUndefined(process.env.PRE_AUTHORIZED_CODE_EXPIRATION_DURATION) ?? 300
Expand Down Expand Up @@ -318,23 +334,39 @@ export function notificationEndpoint<DIDDoc extends object>(
})
try {
const jwtResult = await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback })
EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, {
eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED,
id: uuidv4(),
data: notificationRequest,
initiator: jwtResult.jwt,
initiatorType: InitiatorType.EXTERNAL,
system: System.OID4VCI,
subsystem: SubSystem.API,
const accessToken = jwtResult.jwt.payload as AccessTokenRequest
const errorOrSession = await issuer.processNotification({
preAuthorizedCode: accessToken['pre-authorized_code'],
/*TODO: authorizationCode*/ notification: notificationRequest,
})
if (errorOrSession instanceof Error) {
EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, {
eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR,
id: uuidv4(),
data: notificationRequest,
initiator: jwtResult.jwt,
initiatorType: InitiatorType.EXTERNAL,
system: System.OID4VCI,
subsystem: SubSystem.API,
})
return sendErrorResponse(response, 400, errorOrSession.message)
} else {
EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, {
eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED,
id: uuidv4(),
data: notificationRequest,
initiator: jwtResult.jwt,
initiatorType: InitiatorType.EXTERNAL,
system: System.OID4VCI,
subsystem: SubSystem.API,
})
}
} catch (e) {
LOG.warning(e)
return sendErrorResponse(response, 400, {
error: 'invalid_token',
})
}

// TODO Send event
return response.status(204).send()
} catch (e) {
return sendErrorResponse(
Expand Down Expand Up @@ -569,3 +601,31 @@ export function getBasePath(url?: URL | string) {
}
return `/${trimBoth(basePath, '/')}`
}

export async function oidcAccessTokenVerifyCallback(opts: {
credentialIssuer: string
authorizationServer: string
clientMetadata?: ClientMetadata
}): Promise<JWTVerifyCallback> {
const callback = async (args: { jwt: string; kid?: string }): Promise<JwtVerifyResult> => {
const introspection = await oidcClient.introspect(args.jwt)
if (!introspection.active) {
return Promise.reject(Error('Access token is not active or invalid'))
}
const jwt = { header: decodeProtectedHeader(args.jwt) as JWTHeader, payload: decodeJwt(args.jwt) }

return {
jwt,
alg: jwt.header.alg,
...(jwt.header.jwk && { jwk: jwt.header.jwk }),
...(jwt.header.x5c && { x5c: jwt.header.x5c }),
...(jwt.header.kid && { kid: jwt.header.kid }),
// We could resolve the did document here if the kid is a VM
}
}

const clientMetadata = opts.clientMetadata ?? { client_id: opts.credentialIssuer }
const oidcIssuer = await oidcDiscoverIssuer({ issuerUrl: opts.authorizationServer })
const oidcClient = await oidcGetClient(oidcIssuer.issuer, clientMetadata)
return callback
}
Loading

0 comments on commit f3b89fc

Please sign in to comment.