src/verifier/oid4vp/oid4vp.service.ts
Methods |
|
constructor(cryptoService: CryptoService, encryptionService: EncryptionService, configService: ConfigService, registrarService: RegistrarService, presentationsService: PresentationsService, sessionService: SessionService, sessionLogger: SessionLoggerService, webhookService: WebhookService, cryptoImplementationService: CryptoImplementationService)
|
||||||||||||||||||||||||||||||
Defined in src/verifier/oid4vp/oid4vp.service.ts:21
|
||||||||||||||||||||||||||||||
Parameters :
|
Async createAuthorizationRequest |
createAuthorizationRequest(session: Session, origin: string)
|
Defined in src/verifier/oid4vp/oid4vp.service.ts:42
|
Creates an authorization request for the OID4VP flow. This method generates a JWT that includes the necessary parameters for the authorization request. It initializes the session logging context and logs the start of the flow.
Returns :
Promise<string>
|
Async createRequest | ||||||||||||||||||
createRequest(requestId: string, values: PresentationRequestOptions, tenantId: string, useDcApi: boolean, origin: string)
|
||||||||||||||||||
Defined in src/verifier/oid4vp/oid4vp.service.ts:217
|
||||||||||||||||||
Creates a request for the OID4VP flow.
Parameters :
Returns :
Promise<OfferResponse>
|
Async getResponse | |||||||||
getResponse(body: AuthorizationResponse, session: Session)
|
|||||||||
Defined in src/verifier/oid4vp/oid4vp.service.ts:297
|
|||||||||
Processes the response from the wallet.
Parameters :
Returns :
unknown
|
import { randomUUID } from "node:crypto";
import { BadRequestException, Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { v4 } from "uuid";
import { CryptoService } from "../../crypto/crypto.service";
import { EncryptionService } from "../../crypto/encryption/encryption.service";
import { CryptoImplementationService } from "../../crypto/key/crypto-implementation/crypto-implementation.service";
import { OfferResponse } from "../../issuer/oid4vci/dto/offer-request.dto";
import { RegistrarService } from "../../registrar/registrar.service";
import { Session, SessionStatus } from "../../session/entities/session.entity";
import { SessionService } from "../../session/session.service";
import { SessionLoggerService } from "../../utils/logger/session-logger.service";
import { SessionLogContext } from "../../utils/logger/session-logger-context";
import { WebhookService } from "../../utils/webhook/webhook.service";
import { AuthResponse } from "../presentations/dto/auth-response.dto";
import { PresentationsService } from "../presentations/presentations.service";
import { AuthorizationResponse } from "./dto/authorization-response.dto";
import { PresentationRequestOptions } from "./dto/presentation-request-options.dto";
@Injectable()
export class Oid4vpService {
constructor(
private cryptoService: CryptoService,
private encryptionService: EncryptionService,
private configService: ConfigService,
private registrarService: RegistrarService,
private presentationsService: PresentationsService,
private sessionService: SessionService,
private sessionLogger: SessionLoggerService,
private webhookService: WebhookService,
private cryptoImplementationService: CryptoImplementationService,
) {}
/**
* Creates an authorization request for the OID4VP flow.
* This method generates a JWT that includes the necessary parameters for the authorization request.
* It initializes the session logging context and logs the start of the flow.
* @param session
* @param origin
* @returns
*/
async createAuthorizationRequest(
session: Session,
origin: string,
): Promise<string> {
// Create session logging context
const logContext: SessionLogContext = {
sessionId: session.id,
tenantId: session.tenantId,
flowType: "OID4VP",
stage: "authorization_request",
};
this.sessionLogger.logFlowStart(logContext, {
requestId: session.requestId,
action: "create_authorization_request",
});
try {
const host = this.configService.getOrThrow<string>("PUBLIC_URL");
const tenantHost = `${host}/${session.tenantId}`;
const presentationConfig =
await this.presentationsService.getPresentationConfig(
session.requestId!,
session.tenantId,
);
let regCert: string | undefined = undefined;
const dcql_query = JSON.parse(
JSON.stringify(presentationConfig.dcql_query).replace(
/<PUBLIC_URL>/g,
tenantHost,
),
);
if (this.registrarService.isEnabled()) {
const registrationCert = JSON.parse(
JSON.stringify(presentationConfig.registrationCert).replace(
/<PUBLIC_URL>/g,
tenantHost,
),
);
regCert =
await this.registrarService.addRegistrationCertificate(
registrationCert,
dcql_query,
session.requestId!,
session.tenantId,
);
}
const nonce = randomUUID();
await this.sessionService.add(session.id, {
vp_nonce: nonce,
});
this.sessionLogger.logAuthorizationRequest(logContext, {
requestId: session.requestId,
nonce,
regCert,
dcqlQueryCount: Array.isArray(dcql_query)
? dcql_query.length
: 1,
});
const hostname = new URL(
this.configService.getOrThrow<string>("PUBLIC_URL"),
).hostname;
const lifeTime = 60 * 60;
const request = {
payload: {
response_type: "vp_token",
client_id: "x509_san_dns:" + hostname,
response_uri: !session.useDcApi
? `${host}/${session.id}/oid4vp`
: undefined,
response_mode: !session.useDcApi
? "direct_post.jwt"
: "dc_api.jwt",
nonce,
expected_origins: session.useDcApi ? [origin] : undefined,
dcql_query,
client_metadata: {
jwks: {
keys: [
await this.encryptionService.getEncryptionPublicKey(
session.tenantId,
),
],
},
vp_formats: {
//MDOC not supported yet
/* mso_mdoc: {
alg: ["ES256", "Ed25519"],
}, */
"dc+sd-jwt": {
"kb-jwt_alg_values":
this.cryptoImplementationService.getSupportedAlgorithms(),
"sd-jwt_alg_values":
this.cryptoImplementationService.getSupportedAlgorithms(),
},
},
authorization_encrypted_response_alg: "ECDH-ES",
authorization_encrypted_response_enc: "A128GCM",
client_name: session.tenant.name,
response_types_supported: ["vp_token"],
},
state: !session.useDcApi ? session.id : undefined,
//TODO: check if this value is correct accroding to https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-aud-of-a-request-object
aud: "https://self-issued.me/v2",
exp: Math.floor(Date.now() / 1000) + lifeTime,
iat: Math.floor(new Date().getTime() / 1000),
verifier_attestations: regCert
? [
{
format: "jwt",
data: regCert,
},
]
: undefined,
},
header: {
typ: "oauth-authz-req+jwt",
},
};
const accessCert = await this.cryptoService.getCertChain(
"access",
session.tenantId,
);
const header = {
...request.header,
alg: "ES256",
x5c: accessCert,
};
const keyId = await this.cryptoService.keyService.getKid(
session.tenantId,
"access",
);
const signedJwt = await this.cryptoService.signJwt(
header,
request.payload,
session.tenantId,
keyId,
);
this.sessionLogger.logSession(
logContext,
"Authorization request created successfully",
{
signedJwtLength: signedJwt.length,
certificateChainLength: accessCert?.length || 0,
},
);
return signedJwt;
} catch (error) {
this.sessionLogger.logFlowError(logContext, error as Error, {
requestId: session.requestId,
action: "create_authorization_request",
});
throw error;
}
}
/**
* Creates a request for the OID4VP flow.
* @param requestId
* @param values
* @param tenantId
* @returns
*/
async createRequest(
requestId: string,
values: PresentationRequestOptions,
tenantId: string,
useDcApi: boolean,
origin: string,
): Promise<OfferResponse> {
const presentationConfig =
await this.presentationsService.getPresentationConfig(
requestId,
tenantId,
);
const fresh = values.session === undefined;
values.session = values.session || v4();
const hostname = new URL(
this.configService.getOrThrow<string>("PUBLIC_URL"),
).hostname;
const request_uri_method: "get" | "post" = "get";
const params = {
client_id: `x509_san_dns:${hostname}`,
request_uri: `${this.configService.getOrThrow<string>("PUBLIC_URL")}/${values.session}/oid4vp/request`,
request_uri_method,
};
const queryString = Object.entries(params)
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
)
.join("&");
const expiresAt = new Date(
Date.now() + (presentationConfig.lifeTime ?? 300) * 1000,
);
if (fresh) {
const session = await this.sessionService.create({
id: values.session,
claimsWebhook: values.webhook ?? presentationConfig.webhook,
tenantId,
requestId,
requestUrl: `openid4vp://?${queryString}`,
expiresAt,
useDcApi,
});
if (request_uri_method === "get") {
// load the session to get nested object like tenant
const loadedSession = await this.sessionService.get(session.id);
const signedJwt = await this.createAuthorizationRequest(
loadedSession,
origin,
);
this.sessionService.add(values.session, {
requestObject: signedJwt,
});
}
} else {
await this.sessionService.add(values.session, {
claimsWebhook: values.webhook ?? presentationConfig.webhook,
requestUrl: `openid4vp://?${queryString}`,
expiresAt,
useDcApi,
});
}
return {
uri: queryString,
session: values.session,
};
}
/**
* Processes the response from the wallet.
* @param body
* @param tenantId
*/
async getResponse(body: AuthorizationResponse, session: Session) {
const res = await this.encryptionService.decryptJwe<AuthResponse>(
body.response,
session.tenantId,
);
//for dc api the state is no longer included in the res, see: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-request
// Create session logging context
const logContext: SessionLogContext = {
sessionId: session.id,
tenantId: session.tenantId,
flowType: "OID4VP",
stage: "response_processing",
};
this.sessionLogger.logFlowStart(logContext, {
action: "process_presentation_response",
hasWebhook: !!session.claimsWebhook,
});
try {
//TODO: load required fields from the config
const credentials = await this.presentationsService.parseResponse(
res,
[],
session.vp_nonce as string,
);
this.sessionLogger.logCredentialVerification(
logContext,
!!credentials && credentials.length > 0,
{
credentialCount: credentials?.length || 0,
nonce: session.vp_nonce,
},
);
//tell the auth server the result of the session.
await this.sessionService.add(res.state, {
//TODO: not clear why it has to be any
credentials: credentials as any,
status: SessionStatus.Completed,
});
// if there a a webook URL, send the response there
//TODO: move to dedicated service to reuse it also in the oid4vci flow.
if (session.claimsWebhook) {
await this.webhookService.sendWebhook(
session,
logContext,
credentials,
//when issuance id is defined, we expect a claim response that needs to be saved
!!session.issuanceId,
);
}
this.sessionLogger.logFlowComplete(logContext, {
credentialCount: credentials?.length || 0,
webhookSent: !!session.claimsWebhook,
});
if (body.sendResponse) {
return credentials;
}
} catch (error) {
this.sessionLogger.logFlowError(logContext, error as Error, {
action: "process_presentation_response",
});
throw new BadRequestException(error.message);
}
}
}