@sphereon/oid4vci-client
Version:
OpenID for Verifiable Credential Issuance (OpenID4VCI) client
171 lines (158 loc) • 7.17 kB
text/typescript
import {
AuthorizationDetails,
AuthorizationRequestOpts,
CodeChallengeMethod,
convertJsonToURI,
CreateRequestObjectMode,
CredentialOfferFormatV1_0_11,
CredentialOfferPayloadV1_0_11,
CredentialOfferRequestWithBaseUrl,
CredentialsSupportedLegacy,
EndpointMetadataResultV1_0_11,
formPost,
JsonURIMode,
PARMode,
PKCEOpts,
PushedAuthorizationResponse,
ResponseType,
} from '@sphereon/oid4vci-common';
import Debug from 'debug';
import { createSignedAuthRequestWhenNeeded } from './AuthorizationCodeClient';
const debug = Debug('sphereon:oid4vci');
export const createAuthorizationRequestUrlV1_0_11 = async ({
pkce,
endpointMetadata,
authorizationRequest,
credentialOffer,
credentialsSupported,
}: {
pkce: PKCEOpts;
endpointMetadata: EndpointMetadataResultV1_0_11;
authorizationRequest: AuthorizationRequestOpts;
credentialOffer?: CredentialOfferRequestWithBaseUrl;
credentialsSupported?: CredentialsSupportedLegacy[];
}): Promise<string> => {
const { redirectUri, clientId, requestObjectOpts = { requestObjectMode: CreateRequestObjectMode.NONE } } = authorizationRequest;
let { scope, authorizationDetails } = authorizationRequest;
const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
? PARMode.REQUIRE
: (authorizationRequest.parMode ?? PARMode.AUTO);
// Scope and authorization_details can be used in the same authorization request
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
if (!scope && !authorizationDetails) {
if (!credentialOffer) {
throw Error('Please provide a scope or authorization_details if no credential offer is present');
}
const creds: (CredentialOfferFormatV1_0_11 | string)[] = (credentialOffer.credential_offer as CredentialOfferPayloadV1_0_11).credentials;
// FIXME: complains about VCT for sd-jwt
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
authorizationDetails = creds
.flatMap((cred) => (typeof cred === 'string' ? credentialsSupported : (cred as CredentialsSupportedLegacy)))
.filter((cred) => !!cred)
.map((cred) => {
return {
...cred,
type: 'openid_credential',
locations: [endpointMetadata.issuer],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
format: cred!.format,
} satisfies AuthorizationDetails;
});
if (!authorizationDetails || (Array.isArray(authorizationDetails) && authorizationDetails.length === 0)) {
throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`);
}
}
if (!endpointMetadata?.authorization_endpoint) {
throw Error('Server metadata does not contain authorization endpoint');
}
const parEndpoint = endpointMetadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint;
// add 'openid' scope if not present
if (!scope?.includes('openid')) {
scope = ['openid', scope].filter((s) => !!s).join(' ');
}
let queryObj: { [key: string]: string } | PushedAuthorizationResponse = {
response_type: ResponseType.AUTH_CODE,
...(!pkce.disabled && {
code_challenge_method: pkce.codeChallengeMethod ?? CodeChallengeMethod.S256,
code_challenge: pkce.codeChallenge,
}),
authorization_details: JSON.stringify(handleAuthorizationDetailsV1_0_11(endpointMetadata, authorizationDetails)),
...(redirectUri && { redirect_uri: redirectUri }),
...(clientId && { client_id: clientId }),
...(credentialOffer?.issuerState && { issuer_state: credentialOffer.issuerState }),
scope,
};
if (!parEndpoint && parMode === PARMode.REQUIRE) {
throw Error(`PAR mode is set to required by Authorization Server does not support PAR!`);
} else if (parEndpoint && parMode !== PARMode.NEVER) {
debug(`USING PAR with endpoint ${parEndpoint}`);
const parResponse = await formPost<PushedAuthorizationResponse>(
parEndpoint,
convertJsonToURI(queryObj, {
mode: JsonURIMode.X_FORM_WWW_URLENCODED,
uriTypeProperties: ['client_id', 'request_uri', 'redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
}),
{ contentType: 'application/x-www-form-urlencoded', accept: 'application/json' },
);
if (parResponse.errorBody || !parResponse.successBody) {
console.log(JSON.stringify(parResponse.errorBody));
console.log('Falling back to regular request URI, since PAR failed');
if (parMode === PARMode.REQUIRE) {
throw Error(`PAR error: ${parResponse.origResponse.statusText}`);
}
} else {
debug(`PAR response: ${JSON.stringify(parResponse.successBody, null, 2)}`);
queryObj = { request_uri: parResponse.successBody.request_uri };
}
}
await createSignedAuthRequestWhenNeeded(queryObj, { ...requestObjectOpts, aud: endpointMetadata.authorization_server });
debug(`Object that will become query params: ` + JSON.stringify(queryObj, null, 2));
const url = convertJsonToURI(queryObj, {
baseUrl: endpointMetadata.authorization_endpoint,
uriTypeProperties: ['client_id', 'request_uri', 'redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
// arrayTypeProperties: ['authorization_details'],
mode: JsonURIMode.X_FORM_WWW_URLENCODED,
// We do not add the version here, as this always needs to be form encoded
});
debug(`Authorization Request URL: ${url}`);
return url;
};
const handleAuthorizationDetailsV1_0_11 = (
endpointMetadata: EndpointMetadataResultV1_0_11,
authorizationDetails?: AuthorizationDetails | AuthorizationDetails[],
): AuthorizationDetails | AuthorizationDetails[] | undefined => {
if (authorizationDetails) {
if (typeof authorizationDetails === 'string') {
// backwards compat for older versions of the lib
return authorizationDetails;
}
if (Array.isArray(authorizationDetails)) {
return authorizationDetails
.filter((value) => typeof value !== 'string')
.map((value) => handleLocations(endpointMetadata, typeof value === 'string' ? value : { ...value }));
} else {
return handleLocations(endpointMetadata, { ...authorizationDetails });
}
}
return authorizationDetails;
};
const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_11, authorizationDetails: AuthorizationDetails) => {
if (typeof authorizationDetails === 'string') {
// backwards compat for older versions of the lib
return authorizationDetails;
}
if (authorizationDetails && (endpointMetadata.credentialIssuerMetadata?.authorization_server || endpointMetadata.authorization_endpoint)) {
if (authorizationDetails.locations) {
if (Array.isArray(authorizationDetails.locations)) {
authorizationDetails.locations.push(endpointMetadata.issuer);
} else {
authorizationDetails.locations = [authorizationDetails.locations as string, endpointMetadata.issuer];
}
} else {
authorizationDetails.locations = [endpointMetadata.issuer];
}
}
return authorizationDetails;
};