Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feature/MWALL-715
Browse files Browse the repository at this point in the history
# Conflicts:
#	packages/client/lib/OpenID4VCIClientV1_0_13.ts
#	packages/issuer-rest/lib/OID4VCIServer.ts
#	packages/issuer-rest/lib/oid4vci-api-functions.ts
#	packages/issuer-rest/package.json
#	packages/issuer/package.json
#	packages/siop-oid4vp/lib/__tests__/SdJwt.spec.ts
#	packages/siop-oid4vp/lib/authorization-request/AuthorizationRequest.ts
#	packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts
#	packages/siop-oid4vp/lib/authorization-response/Dcql.ts
#	packages/siop-oid4vp/lib/rp/RP.ts
#	packages/siop-oid4vp/lib/rp/RPBuilder.ts
#	pnpm-lock.yaml
  • Loading branch information
nklomp committed Jan 27, 2025
2 parents c9b4d6f + 5194986 commit 9866b0a
Show file tree
Hide file tree
Showing 54 changed files with 1,111 additions and 251 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-test-on-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
node-version: '20.x'
- uses: pnpm/action-setup@v4
with:
version: 9
version: 9.15.3 # TODO remove later, we are temporary dealing with a broken pnpm version in the CI container
- run: pnpm install
- run: pnpm build
- name: run CI tests
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-test-publish-on-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
node-version: '20.x'
- uses: pnpm/action-setup@v4
with:
version: 9
version: 9.15.3 # TODO remove later, we are temporary dealing with a broken pnpm version in the CI container
- run: pnpm install
- run: pnpm build
- name: run integration tests
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"node": ">=18"
},
"resolutions": {
"@sphereon/ssi-types": "0.32.0",
"dcql": "0.2.11",
"@sphereon/ssi-types": "0.32.1-next.113",
"dcql": "0.2.19",
"node-fetch": "2.6.12"
},
"prettier": {
Expand Down
2 changes: 1 addition & 1 deletion packages/callback-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@sphereon/oid4vci-client": "workspace:*",
"@sphereon/oid4vci-common": "workspace:*",
"@sphereon/oid4vci-issuer": "workspace:*",
"@sphereon/ssi-types": "0.32.0",
"@sphereon/ssi-types": "0.32.1-next.113",
"jose": "^4.10.0"
},
"devDependencies": {
Expand Down
91 changes: 89 additions & 2 deletions packages/client/lib/AuthorizationCodeClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {
AuthorizationChallengeCodeResponse,
AuthorizationChallengeRequestOpts,
AuthorizationDetails,
AuthorizationRequestOpts,
CodeChallengeMethod,
CommonAuthorizationChallengeRequest,
convertJsonToURI,
CreateRequestObjectMode,
CredentialConfigurationSupportedV1_0_13,
Expand All @@ -10,20 +13,24 @@ import {
CredentialOfferPayloadV1_0_13,
CredentialOfferRequestWithBaseUrl,
determineSpecVersionFromOffer,
EndpointMetadata,
EndpointMetadataResultV1_0_13,
formPost,
IssuerOpts,
isW3cCredentialSupported,
JsonURIMode,
Jwt,
OpenId4VCIVersion,
OpenIDResponse,
PARMode,
PKCEOpts,
PushedAuthorizationResponse,
RequestObjectOpts,
ResponseType,
} from '@sphereon/oid4vci-common';
ResponseType
} from '@sphereon/oid4vci-common'
import Debug from 'debug';

import { MetadataClient } from './MetadataClient'
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';

const debug = Debug('sphereon:oid4vci');
Expand Down Expand Up @@ -272,3 +279,83 @@ const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_13, author
}
return authorizationDetails;
};

export const acquireAuthorizationChallengeAuthCode = async (opts: AuthorizationChallengeRequestOpts): Promise<OpenIDResponse<AuthorizationChallengeCodeResponse>> => {
const { metadata } = opts

const issuer = opts.credentialIssuer ?? opts?.metadata?.issuer as string
if (!issuer) {
throw Error('Issuer required at this point');
}

const issuerOpts = {
issuer,
}

return await acquireAuthorizationChallengeAuthCodeUsingRequest({
authorizationChallengeRequest: await createAuthorizationChallengeRequest(opts),
metadata,
issuerOpts
});
}

export const acquireAuthorizationChallengeAuthCodeUsingRequest = async (
opts: {
authorizationChallengeRequest: CommonAuthorizationChallengeRequest,
metadata?: EndpointMetadata,
issuerOpts?: IssuerOpts
}
): Promise<OpenIDResponse<AuthorizationChallengeCodeResponse>> => {
const { authorizationChallengeRequest, issuerOpts } = opts
const metadata = opts?.metadata
? opts?.metadata
: issuerOpts?.fetchMetadata
? await MetadataClient.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false })
: undefined
const authorizationChallengeCodeUrl = metadata?.authorization_challenge_endpoint

if (!authorizationChallengeCodeUrl) {
return Promise.reject(Error('Cannot determine authorization challenge endpoint URL'))
}

const response = await sendAuthorizationChallengeRequest(
authorizationChallengeCodeUrl,
authorizationChallengeRequest
);

return response
}

export const createAuthorizationChallengeRequest = async (opts: AuthorizationChallengeRequestOpts): Promise<CommonAuthorizationChallengeRequest> => {
const {
clientId,
issuerState,
authSession,
scope,
codeChallenge,
codeChallengeMethod,
presentationDuringIssuanceSession
} = opts;

const request: CommonAuthorizationChallengeRequest = {
client_id: clientId,
issuer_state: issuerState,
auth_session: authSession,
scope,
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
presentation_during_issuance_session: presentationDuringIssuanceSession
}

return request
}

export const sendAuthorizationChallengeRequest = async (
authorizationChallengeCodeUrl: string,
authorizationChallengeRequest: CommonAuthorizationChallengeRequest,
opts?: { headers?: Record<string, string> }
): Promise<OpenIDResponse<AuthorizationChallengeCodeResponse>> => {
return await formPost(authorizationChallengeCodeUrl, convertJsonToURI(authorizationChallengeRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), {
customHeaders: opts?.headers ? opts.headers : undefined,
});
}
11 changes: 10 additions & 1 deletion packages/client/lib/MetadataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class MetadataClient {
let credential_endpoint: string | undefined;
let deferred_credential_endpoint: string | undefined;
let authorization_endpoint: string | undefined;
let authorization_challenge_endpoint: string | undefined;
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
let authorization_servers: string[] | undefined = [issuer];
let authorization_server: string | undefined = undefined;
Expand All @@ -84,6 +85,7 @@ export class MetadataClient {
if (credentialIssuerMetadata.token_endpoint) {
token_endpoint = credentialIssuerMetadata.token_endpoint;
}
authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint
if (credentialIssuerMetadata.authorization_servers) {
authorization_servers = credentialIssuerMetadata.authorization_servers as string[];
} else if (credentialIssuerMetadata.authorization_server) {
Expand Down Expand Up @@ -130,8 +132,14 @@ export class MetadataClient {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) {
throw Error(
`Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`,
);
}
authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint;
if (!authMetadata.token_endpoint) {
throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`);
throw Error(`Authorization Server ${authorization_servers} did not provide a token_endpoint`);
} else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
throw Error(
`Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`,
Expand Down Expand Up @@ -193,6 +201,7 @@ export class MetadataClient {
deferred_credential_endpoint,
...(authorization_server ? { authorization_server } : { authorization_servers: authorization_servers }),
authorization_endpoint,
authorization_challenge_endpoint,
authorizationServerType,
credentialIssuerMetadata: authorization_server
? (credentialIssuerMetadata as IssuerMetadataV1_0_08 & Partial<AuthorizationServerMetadata>)
Expand Down
11 changes: 10 additions & 1 deletion packages/client/lib/MetadataClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class MetadataClientV1_0_11 {
let credential_endpoint: string | undefined;
let deferred_credential_endpoint: string | undefined;
let authorization_endpoint: string | undefined;
let authorization_challenge_endpoint: string | undefined;
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
let authorization_server: string = issuer;
const oid4vciResponse = await MetadataClientV1_0_11.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations
Expand All @@ -61,6 +62,7 @@ export class MetadataClientV1_0_11 {
if (credentialIssuerMetadata.token_endpoint) {
token_endpoint = credentialIssuerMetadata.token_endpoint;
}
authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint
if (credentialIssuerMetadata.authorization_server) {
authorization_server = credentialIssuerMetadata.authorization_server;
}
Expand Down Expand Up @@ -105,8 +107,14 @@ export class MetadataClientV1_0_11 {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) {
throw Error(
`Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`,
);
}
authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint;
if (!authMetadata.token_endpoint) {
throw Error(`Authorization Sever ${authorization_server} did not provide a token_endpoint`);
throw Error(`Authorization Server ${authorization_server} did not provide a token_endpoint`);
} else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
throw Error(
`Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`,
Expand Down Expand Up @@ -165,6 +173,7 @@ export class MetadataClientV1_0_11 {
deferred_credential_endpoint,
authorization_server,
authorization_endpoint,
authorization_challenge_endpoint,
authorizationServerType,
credentialIssuerMetadata: credentialIssuerMetadata as unknown as Partial<AuthorizationServerMetadata> & IssuerMetadataV1_0_08,
authorizationServerMetadata: authMetadata,
Expand Down
11 changes: 10 additions & 1 deletion packages/client/lib/MetadataClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class MetadataClientV1_0_13 {
let credential_endpoint: string | undefined;
let deferred_credential_endpoint: string | undefined;
let authorization_endpoint: string | undefined;
let authorization_challenge_endpoint: string | undefined;
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
let authorization_servers: string[] = [issuer];
const oid4vciResponse = await MetadataClientV1_0_13.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations
Expand All @@ -61,6 +62,7 @@ export class MetadataClientV1_0_13 {
if (credentialIssuerMetadata.token_endpoint) {
token_endpoint = credentialIssuerMetadata.token_endpoint;
}
authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint
if (credentialIssuerMetadata.authorization_servers) {
authorization_servers = credentialIssuerMetadata.authorization_servers;
}
Expand Down Expand Up @@ -104,8 +106,14 @@ export class MetadataClientV1_0_13 {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) {
throw Error(
`Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`,
);
}
authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint;
if (!authMetadata.token_endpoint) {
throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`);
throw Error(`Authorization Server ${authorization_servers} did not provide a token_endpoint`);
} else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
throw Error(
`Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`,
Expand Down Expand Up @@ -164,6 +172,7 @@ export class MetadataClientV1_0_13 {
deferred_credential_endpoint,
authorization_server: authorization_servers[0],
authorization_endpoint,
authorization_challenge_endpoint,
authorizationServerType,
credentialIssuerMetadata: credentialIssuerMetadata,
authorizationServerMetadata: authMetadata,
Expand Down
63 changes: 52 additions & 11 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {
AccessTokenRequestOpts,
AccessTokenResponse,
Alg,
AuthorizationChallengeCodeResponse,
AuthorizationChallengeErrorResponse,
AuthorizationChallengeRequestOpts,
AuthorizationRequestOpts,
AuthorizationResponse,
AuthorizationServerOpts,
Expand Down Expand Up @@ -33,14 +36,17 @@ import {
OpenId4VCIVersion,
PKCEOpts,
ProofOfPossessionCallbacks,
toAuthorizationResponsePayload,
} from '@sphereon/oid4vci-common';
toAuthorizationResponsePayload
} from '@sphereon/oid4vci-common'
import { CredentialFormat } from '@sphereon/ssi-types';
import Debug from 'debug';

import { AccessTokenClient } from './AccessTokenClient';
import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11';
import { createAuthorizationRequestUrl } from './AuthorizationCodeClient';
import {
acquireAuthorizationChallengeAuthCode,
createAuthorizationRequestUrl
} from './AuthorizationCodeClient'
import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11';
import { CredentialOfferClient } from './CredentialOfferClient';
import { CredentialRequestOpts } from './CredentialRequestClient';
Expand Down Expand Up @@ -89,7 +95,7 @@ export class OpenID4VCIClient {
endpointMetadata?: EndpointMetadataResult;
accessTokenResponse?: AccessTokenResponse;
authorizationRequestOpts?: AuthorizationRequestOpts;
authorizationCodeResponse?: AuthorizationResponse;
authorizationCodeResponse?: AuthorizationResponse | AuthorizationChallengeCodeResponse;
authorizationURL?: string;
}) {
const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined);
Expand Down Expand Up @@ -276,21 +282,37 @@ export class OpenID4VCIClient {
this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce });
}

public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise<AuthorizationChallengeCodeResponse> {
const response = await acquireAuthorizationChallengeAuthCode({
metadata: this.endpointMetadata,
credentialIssuer: this.getIssuer(),
clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId,
...opts
})

if (response.errorBody) {
debug(`Authorization code error:\r\n${JSON.stringify(response.errorBody)}`);
const error = response.errorBody as AuthorizationChallengeErrorResponse
return Promise.reject(error)
} else if (!response.successBody) {
debug(`Authorization code error. No success body`);
return Promise.reject(Error(`Retrieving an authorization code token from ${this._state.endpointMetadata?.authorization_challenge_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`))
}

return { ...response.successBody }
}

public async acquireAccessToken(
opts?: Omit<AccessTokenRequestOpts, 'credentialOffer' | 'credentialIssuer' | 'metadata' | 'additionalParams'> & {
clientId?: string;
authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse; // Pass in an auth response, either as URI/redirect, or object
additionalRequestParams?: Record<string, any>;
},
): Promise<AccessTokenResponse & { params?: DPoPResponseParams }> {
const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {};
let { redirectUri } = opts ?? {};
if (opts?.authorizationResponse) {
this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(opts.authorizationResponse) };
} else if (opts?.code) {
this._state.authorizationCodeResponse = { code: opts.code };
}
const code = this._state.authorizationCodeResponse?.code;

const code = this.getAuthorizationCode(opts?.authorizationResponse, opts?.code)

if (opts?.codeVerifier) {
this._state.pkce.codeVerifier = opts.codeVerifier;
Expand Down Expand Up @@ -660,6 +682,15 @@ export class OpenID4VCIClient {
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
}

public getAuthorizationChallengeEndpoint(): string | undefined {
this.assertIssuerData();
return this.endpointMetadata?.authorization_challenge_endpoint;
}

public hasAuthorizationChallengeEndpoint(): boolean {
return !!this.getAuthorizationChallengeEndpoint();
}

public hasDeferredCredentialEndpoint(): boolean {
return !!this.getAccessTokenEndpoint();
}
Expand Down Expand Up @@ -733,4 +764,14 @@ export class OpenID4VCIClient {
authorizationRequestOpts.clientId = clientId;
return authorizationRequestOpts;
}

private getAuthorizationCode = (authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse, code?: string): string | undefined => {
if (authorizationResponse) {
this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(authorizationResponse) };
} else if (code) {
this._state.authorizationCodeResponse = { code };
}

return (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code;
}
}
Loading

0 comments on commit 9866b0a

Please sign in to comment.