Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/SPRIND-89 #177

Merged
merged 29 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9c273b9
feat: added support for first party applications
Brummos Jan 9, 2025
f3b89fc
Merge branch 'develop' into feature/SPRIND-89
Brummos Jan 9, 2025
5a0dba4
chore: cleanup
Brummos Jan 9, 2025
bf1217f
chore: cleanup
Brummos Jan 9, 2025
ce3ef0b
chore: typo fix
Brummos Jan 9, 2025
2b4c069
chore: specifically check for true in isAuthorizationChallengeEndpoin…
Brummos Jan 9, 2025
acf2cdc
chore: added acquiring authorization challenge authorization code to …
Brummos Jan 13, 2025
58af8fe
chore: cleanup
Brummos Jan 13, 2025
b97ff27
chore: make opts optional as client id is default from the client
Brummos Jan 13, 2025
3a426b5
chore: use authorization_code for acquiring the access token
Brummos Jan 13, 2025
1825b96
chore: cleanup
Brummos Jan 13, 2025
e8dca63
chore: refactor authorization challenge code error handling
Brummos Jan 13, 2025
3e2c8d7
chore: small fixes
Brummos Jan 15, 2025
084d916
chore: fixes
Brummos Jan 15, 2025
ba107cc
chore: added tests
Brummos Jan 15, 2025
1eef34b
chore: added tests
Brummos Jan 15, 2025
c6c5df2
chore: added tests
Brummos Jan 15, 2025
a8f712d
chore: fixes
Brummos Jan 15, 2025
4a74802
chore: fix test
Brummos Jan 15, 2025
92430bc
chore: cleanup
Brummos Jan 15, 2025
9b61ea6
chore: for first party flow use presentation id from issuer options
Brummos Jan 17, 2025
1989813
chore: extract IEndpointOpts for agent project
sanderPostma Jan 17, 2025
908b2e7
chore: use auth server endpoint when available
Brummos Jan 21, 2025
bfb52ee
Merge branch 'feature/SPRIND-89' of https://github.com/Sphereon-Opens…
Brummos Jan 21, 2025
5c4b66e
chore: addressing PR comments
Brummos Jan 22, 2025
d416e00
chore: fix test url
Brummos Jan 22, 2025
8eca880
chore: removed first party flag
Brummos Jan 22, 2025
fa16634
chore: revert isFirstParty flag
Brummos Jan 22, 2025
147da96
Merge branch 'develop' into feature/SPRIND-89
Brummos Jan 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
});
}
12 changes: 11 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 Down Expand Up @@ -130,8 +131,16 @@ export class MetadataClient {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (!authMetadata.authorization_challenge_endpoint) {
debug(`Authorization Server ${authorization_servers} did not provide a authorization_challenge_endpoint`);
nklomp marked this conversation as resolved.
Show resolved Hide resolved
} else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) {
nklomp marked this conversation as resolved.
Show resolved Hide resolved
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 +202,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
12 changes: 11 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 Down Expand Up @@ -105,8 +106,16 @@ export class MetadataClientV1_0_11 {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (!authMetadata.authorization_challenge_endpoint) {
BtencateSphereon marked this conversation as resolved.
Show resolved Hide resolved
debug(`Authorization Server ${authorization_server} did not provide a authorization_challenge_endpoint`);
} else 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 +174,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
12 changes: 11 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 Down Expand Up @@ -104,8 +105,16 @@ export class MetadataClientV1_0_13 {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (!authMetadata.authorization_challenge_endpoint) {
nklomp marked this conversation as resolved.
Show resolved Hide resolved
debug(`Authorization Server ${authorization_servers} did not provide a authorization_challenge_endpoint`);
} else 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 +173,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 @@ -270,21 +276,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 @@ -654,6 +676,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 @@ -727,4 +758,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
Loading