Skip to content

feat: [SIW-2209] Update credentials data model and issuance flow to 1.0.0 #219

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

Draft
wants to merge 18 commits into
base: SIW-2206-pid-1.0.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f656df8
chore: update credential metadata to 0.9.1
RiccardoMolinari95 Mar 21, 2025
677ed7a
chore: update to 0.9.1 credential issuing flow completeUserAuthorizat…
RiccardoMolinari95 Mar 31, 2025
bc6dcd6
fix: prettier
RiccardoMolinari95 Mar 31, 2025
ebf01fb
chore: update readme, beautify code
RiccardoMolinari95 Mar 31, 2025
f247aed
chore: readme
RiccardoMolinari95 Mar 31, 2025
07ae5ee
refactor: use union instead enum
RiccardoMolinari95 Mar 31, 2025
752de9e
chore: update Sdjwt type
RiccardoMolinari95 Apr 11, 2025
2bec15d
chore: add trust_chain to SdJwt4VC
RiccardoMolinari95 Apr 11, 2025
bd269e7
Merge remote-tracking branch 'origin/SIW-2206-pid-1.0.0' into SIW-220…
ChrisMattew Apr 16, 2025
4f0aa33
Merge remote-tracking branch 'origin/SIW-2206-pid-1.0.0' into SIW-220…
ChrisMattew Apr 16, 2025
e892c56
chore(CredentialResponse): make notification_id attribute optional
ChrisMattew Apr 16, 2025
5bb8bbb
feat(user-authorization): update createAuthzResponsePayload to return…
ChrisMattew Apr 16, 2025
0dc9221
chore(TokenResponse): add refresh_token optional attribute
ChrisMattew Apr 18, 2025
fcc1923
Merge branch 'SIW-2206-pid-1.0.0' into SIW-2209-credentials-1.0.0
ChrisMattew Apr 23, 2025
79e2f28
Merge branch 'SIW-2206-pid-1.0.0' into SIW-2209-credentials-1.0.0
gispada May 8, 2025
ab88533
Merge branch 'SIW-2206-pid-1.0.0' into SIW-2209-credentials-1.0.0
gispada May 30, 2025
5169693
Merge branch 'SIW-2206-pid-1.0.0' into SIW-2209-credentials-1.0.0
gispada Jun 5, 2025
7b911be
Merge branch 'SIW-2206-pid-1.0.0' into SIW-2209-credentials-1.0.0
gispada Jun 6, 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
5 changes: 2 additions & 3 deletions example/src/thunks/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*/
type GetCredentialStatusAttestationThunkInput = {
credentialType: SupportedCredentialsWithoutPid;
credential: Out<Credential.Issuance.ObtainCredential>["credential"];

Check failure on line 37 in example/src/thunks/credential.ts

View workflow job for this annotation

GitHub Actions / code_review

Property 'credential' does not exist on type '{ credentials: { credential: string; }[]; notification_id?: string | undefined; }'.
keyTag: string;
};

Expand Down Expand Up @@ -81,15 +81,14 @@
if (!pid) {
throw new Error("PID not found");
}
const pidCryptoContext = createCryptoContextFor(pid.keyTag);
return await getCredential({
credentialIssuerUrl: WALLET_EAA_PROVIDER_BASE_URL,
redirectUri: REDIRECT_URI,
credentialType,
pid: pid,
// TODO handle like PID
walletInstanceAttestation,
wiaCryptoContext,
pid: pid.credential,
pidCryptoContext,
});
});

Expand Down
39 changes: 31 additions & 8 deletions example/src/utils/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import { v4 as uuidv4 } from "uuid";
import { generate } from "@pagopa/io-react-native-crypto";
import appFetch from "../utils/fetch";
import { DPOP_KEYTAG, regenerateCryptoKey } from "../utils/crypto";
import { DPOP_KEYTAG, regenerateCryptoKey, WIA_KEYTAG } from "../utils/crypto";
import type { CryptoContext } from "@pagopa/io-react-native-jwt";
import type {
CredentialResult,
PidResult,
SupportedCredentialsWithoutPid,
} from "../store/types";
import { openUrlAndListenForAuthRedirect } from "./openUrlAndListenForRedirect";
import { DcqlQuery } from "dcql";

/**
* Implements a flow to obtain a PID credential.
Expand Down Expand Up @@ -114,11 +115,11 @@
);

// Obtain che eID credential
const { credential, format } = await Credential.Issuance.obtainCredential(

Check failure on line 118 in example/src/utils/credential.ts

View workflow job for this annotation

GitHub Actions / code_review

Property 'credential' does not exist on type '{ credentials: { credential: string; }[]; notification_id?: string | undefined; }'.

Check failure on line 118 in example/src/utils/credential.ts

View workflow job for this annotation

GitHub Actions / code_review

Property 'format' does not exist on type '{ credentials: { credential: string; }[]; notification_id?: string | undefined; }'.
issuerConf,
accessToken,
clientId,
credentialDefinition,

Check failure on line 122 in example/src/utils/credential.ts

View workflow job for this annotation

GitHub Actions / code_review

Argument of type '{ type: "openid_credential"; credential_configuration_id: string; }' is not assignable to parameter of type '{ type: "openid_credential"; credential_configuration_id: string; } & { credential_identifier: string; }'.
{
credentialCryptoContext,
dPopCryptoContext,
Expand Down Expand Up @@ -148,28 +149,25 @@
* @param credentialIssuerUrl - The credential issuer URL
* @param redirectUri - The redirect URI for the authorization flow
* @param credentialType - The type of the credential to obtain, which must be `PersonIdentificationData`
* @param pid - The PID credential
* @param walletInstanceAttestation - The Wallet Instance Attestation
* @param wiaCryptoContext - The Wallet Instance Attestation crypto context
* @param pid - The PID credential
* @param pidCryptoContext - The PID credential crypto context
* @returns The obtained credential result
*/
export const getCredential = async ({
credentialIssuerUrl,
redirectUri,
credentialType,
pid,
walletInstanceAttestation,
wiaCryptoContext,
pid,
pidCryptoContext,
}: {
credentialIssuerUrl: string;
redirectUri: string;
credentialType: SupportedCredentialsWithoutPid;
pid: PidResult;
walletInstanceAttestation: string;
wiaCryptoContext: CryptoContext;
pid: string;
pidCryptoContext: CryptoContext;
}): Promise<CredentialResult> => {
// Create credential crypto context
const credentialKeyTag = uuidv4().toString();
Expand Down Expand Up @@ -209,13 +207,38 @@
appFetch
);

// The credentials to be presented will always include the PID and WIA
// in a credential issuance flow
const credentialsSdJwt = [
[pid.keyTag, pid.credential],
[WIA_KEYTAG, walletInstanceAttestation],
] as [string, string][];

if (!requestObject.dcql_query) {
throw new Error("Invalid request object");
}

// Assuming that WIA is a SD-JWT
const dcqlQueryResult = Credential.Presentation.evaluateDcqlQuery(
credentialsSdJwt,
requestObject.dcql_query as DcqlQuery
);

const credentialsToPresent = dcqlQueryResult.map(
({ requiredDisclosures, ...rest }) => ({
...rest,
requestedClaims: requiredDisclosures.map(([, claimName]) => claimName),
})
);

// The app here should ask the user to confirm the required data contained in the requestObject

// Complete the user authorization via form_post.jwt mode
const { code } =
await Credential.Issuance.completeUserAuthorizationWithFormPostJwtMode(
requestObject,
{ wiaCryptoContext, pidCryptoContext, pid, walletInstanceAttestation }
credentialsToPresent,
{ wiaCryptoContext }
);

// Generate the DPoP context which will be used for the whole issuance flow
Expand All @@ -237,7 +260,7 @@
);

// Obtain the credential
const { credential, format } = await Credential.Issuance.obtainCredential(

Check failure on line 263 in example/src/utils/credential.ts

View workflow job for this annotation

GitHub Actions / code_review

Property 'credential' does not exist on type '{ credentials: { credential: string; }[]; notification_id?: string | undefined; }'.

Check failure on line 263 in example/src/utils/credential.ts

View workflow job for this annotation

GitHub Actions / code_review

Property 'format' does not exist on type '{ credentials: { credential: string; }[]; notification_id?: string | undefined; }'.
issuerConf,
accessToken,
clientId,
Expand Down
161 changes: 71 additions & 90 deletions src/credential/issuance/04-complete-user-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ import { IssuerResponseError, ValidationFailed } from "../../utils/errors";
import type { EvaluateIssuerTrust } from "./02-evaluate-issuer-trust";
import {
decode,
encodeBase64,
SignJWT,
type CryptoContext,
} from "@pagopa/io-react-native-jwt";
import { RequestObject } from "../presentation/types";
import { v4 as uuidv4 } from "uuid";
import { type RemotePresentation, RequestObject } from "../presentation/types";
import { ResponseUriResultShape } from "./types";
import { getJwtFromFormPost } from "../../utils/decoder";
import { AuthorizationError, AuthorizationIdpError } from "./errors";
import { LogLevel, Logger } from "../../utils/logging";
import { Presentation } from "..";

/**
* The interface of the phase to complete User authorization via strong identification when the response mode is "query" and the request credential is a PersonIdentificationData.
Expand All @@ -30,11 +29,14 @@ export type CompleteUserAuthorizationWithQueryMode = (

export type CompleteUserAuthorizationWithFormPostJwtMode = (
requestObject: Out<GetRequestedCredentialToBePresented>,
credentials: {
id: string;
credential: string;
keyTag: string;
requestedClaims: string[];
}[],
context: {
wiaCryptoContext: CryptoContext;
pidCryptoContext: CryptoContext;
pid: string;
walletInstanceAttestation: string;
appFetch?: GlobalFetch["fetch"];
}
) => Promise<AuthorizationResult>;
Expand Down Expand Up @@ -160,101 +162,36 @@ export const getRequestedCredentialToBePresented: GetRequestedCredentialToBePres
/**
* WARNING: This function must be called after {@link startUserAuthorization}. The next function to be called is {@link completeUserAuthorizationWithFormPostJwtMode}.
* The interface of the phase to complete User authorization via presentation of existing credentials when the response mode is "form_post.jwt".
* It is used as a first step to complete the user authorization by obtaining the requested credential to be presented from the authorization server.
* The information is obtained by performing a GET request to the authorization endpoint with request_uri and client_id parameters.
* @param issuerRequestUri the URI of the issuer where the request is sent
* @param clientId Identifies the current client across all the requests of the issuing flow returned by {@link startUserAuthorization}
* @param issuerConf The issuer configuration returned by {@link evaluateIssuerTrust}
* @param context.walletInstanceAccestation the Wallet Instance's attestation to be presented
* @param context.pid the PID to be presented
* @param context.wiaCryptoContext The Wallet Instance's crypto context associated with the walletInstanceAttestation parameter
* @param context.pidCryptoContext The PID crypto context associated with the pid parameter
* @param context.appFetch (optional) fetch api implementation. Default: built-in fetch
* The information is obtained by performing a POST request to the endpoint received in the response_uri field of the requestObject, where the Authorization Response payload is posted.
* Following this,the redirect_uri from the response is used to obtain the final authorization response.
* @param requestObject - The request object containing the necessary parameters for authorization.
* @param credentialsToPresent the credentials to be presented, which will always include the PID and WIA in a credential issuance flow
* @param appFetch (optional) fetch api implementation. Default: built-in fetch
* @throws {ValidationFailed} if an error while validating the response
* @returns the authorization response which contains code, state and iss
*/
export const completeUserAuthorizationWithFormPostJwtMode: CompleteUserAuthorizationWithFormPostJwtMode =
async (requestObject, ctx) => {
async (
requestObject,
credentialsToPresent,
{ wiaCryptoContext, appFetch = fetch }
) => {
Logger.log(
LogLevel.DEBUG,
`The requeste credential is not a PersonIdentificationData, completing the user authorization with form_post.jwt mode`
);

const {
wiaCryptoContext,
pidCryptoContext,
pid,
walletInstanceAttestation,
appFetch = fetch,
} = ctx;

const wiaWpToken = await new SignJWT(wiaCryptoContext)
.setProtectedHeader({
alg: "ES256",
typ: "JWT",
})
.setPayload({
vp: walletInstanceAttestation,
jti: uuidv4().toString(),
nonce: requestObject.nonce,
})
.setIssuedAt()
.setExpirationTime("5m")
.setAudience(requestObject.response_uri)
.sign();

const pidWpToken = await new SignJWT(pidCryptoContext)
.setProtectedHeader({
alg: "ES256",
typ: "JWT",
})
.setPayload({
vp: pid,
jti: uuidv4().toString(),
nonce: requestObject.nonce,
})
.setIssuedAt()
.setExpirationTime("5m")
.setAudience(requestObject.response_uri)
.sign();

Logger.log(
LogLevel.DEBUG,
`Wallet instance attestation JWT token: ${wiaWpToken}`
const remotePresentations = await Presentation.prepareRemotePresentations(
credentialsToPresent,
requestObject.nonce,
requestObject.client_id
);

/* The path parameter refers to the vp_token variable of the authzResponsePayload and must point to the plain credential which
* is cointaned in the `vp` property of the signed jwt token payload
*/
const presentationSubmission = {
definition_id: `${uuidv4()}`,
id: `${uuidv4()}`,
descriptor_map: [
{
id: "PersonIdentificationData",
path: "$.vp_token[0].vp",
format: "vc+sd-jwt",
},
{
id: "WalletAttestation",
path: "$.vp_token[1].vp",
format: "jwt",
},
],
};

Logger.log(
LogLevel.DEBUG,
`Presentation submission: ${JSON.stringify(presentationSubmission)}`
);

const authzResponsePayload = encodeBase64(
JSON.stringify({
state: requestObject.state,
presentation_submission: presentationSubmission,
vp_token: [pidWpToken, wiaWpToken],
})
);
const authzResponsePayload = await createAuthzResponsePayload({
state: requestObject.state,
remotePresentations,
wiaCryptoContext,
});

Logger.log(
LogLevel.DEBUG,
Expand Down Expand Up @@ -334,3 +271,47 @@ export const parseAuthorizationResponse = (
}
return authResParsed.data;
};

/**
* Creates the authorization response payload to be sent.
* This payload includes the state and the VP tokens for the presented credentials.
* The payload is encoded in Base64.
* @param state - The state parameter from the request object (optional).
* @param remotePresentations - An array of remote presentations containing credential IDs and their corresponding VP tokens.
* @returns The Base64 encoded authorization response payload.
*/
const createAuthzResponsePayload = async ({
state,
remotePresentations,
wiaCryptoContext,
}: {
state?: string;
remotePresentations: RemotePresentation[];
wiaCryptoContext: CryptoContext;
}): Promise<string> => {
const { kid } = await wiaCryptoContext.getPublicKey();

return new SignJWT(wiaCryptoContext)
.setProtectedHeader({
typ: "jwt",
kid,
})
.setPayload({
/**
* TODO [SIW-2264]: `state` coming from `requestObject` is marked as `optional`
* At the moment, it is not entirely clear whether this value can indeed be omitted
* and, if so, what the consequences of its absence might be.
*/
...(state ? { state } : {}),
vp_token: remotePresentations.reduce(
(vp_token, { credentialId, vpToken }) => ({
...vp_token,
[credentialId]: vpToken,
}),
{}
),
})
.setIssuedAt()
.setExpirationTime("1h")
.sign();
};
26 changes: 25 additions & 1 deletion src/credential/issuance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,37 @@ const requestObject =
appFetch
);

// The credentials to be presented will always include the PID and WIA
// in a credential issuance flow
const credentialsSdJwt = [
[pid.keyTag, pid.credential],
[WIA_KEYTAG, walletInstanceAttestation],
] as [string, string][];

if (!requestObject.dcql_query) {
throw new Error("Invalid request object");
}

// Assuming that WIA is a SD-JWT
const dcqlQueryResult = Credential.Presentation.evaluateDcqlQuery(
credentialsSdJwt,
requestObject.dcql_query as DcqlQuery
);

const credentialsToPresent = dcqlQueryResult.map(
({ requiredDisclosures, ...rest }) => ({
...rest,
requestedClaims: requiredDisclosures.map(([, claimName]) => claimName),
})
);

// The app here should ask the user to confirm the required data contained in the requestObject

// Complete the user authorization via form_post.jwt mode
const { code } =
await Credential.Issuance.completeUserAuthorizationWithFormPostJwtMode(
requestObject,
{ wiaCryptoContext, pidCryptoContext, pid, walletInstanceAttestation }
credentialsToPresent
);

// Generate the DPoP context which will be used for the whole issuance flow
Expand Down
3 changes: 2 additions & 1 deletion src/credential/issuance/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type TokenResponse = z.infer<typeof TokenResponse>;

export const TokenResponse = z.object({
access_token: z.string(),
refresh_token: z.string().optional(),
authorization_details: z.array(AuthorizationDetail),
expires_in: z.number(),
token_type: z.string(),
Expand All @@ -24,7 +25,7 @@ export const CredentialResponse = z.object({
credential: z.string(),
})
),
notification_id: z.string(),
notification_id: z.string().optional(),
});

/**
Expand Down
2 changes: 1 addition & 1 deletion src/credential/presentation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const RequestObject = z.object({
state: z.string().optional(),
nonce: z.string(),
response_uri: z.string(),
response_uri_method: z.string().optional(),
request_uri_method: z.string().optional(),
response_type: z.literal("vp_token"),
response_mode: z.literal("direct_post.jwt"),
client_id: z.string(),
Expand Down
Loading
Loading