Skip to content

Commit 74b8814

Browse files
authored
Merge pull request #446 from openwallet-foundation-labs/feat/iae
Feat/iae
2 parents 2041151 + f640af9 commit 74b8814

File tree

45 files changed

+10166
-52
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+10166
-52
lines changed

apps/backend/src/crypto/crypto.service.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ export class CryptoService {
7070
clientAuthentication: clientAuthenticationNone({
7171
clientId: "some-random",
7272
}),
73-
//clientId: 'some-random-client-id', // TODO: Replace with your real clientId if necessary
7473
signJwt: this.getSignJwtCallback(tenantId),
7574
verifyJwt: async (signer, { compact, payload }) => {
7675
if (signer.method === "jwk") {

apps/backend/src/issuer/configuration/configuration.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { HttpModule } from "@nestjs/axios";
22
import { Module } from "@nestjs/common";
33
import { TypeOrmModule } from "@nestjs/typeorm";
4-
import { ClientModule } from "../../auth/client/client.module";
54
import { CryptoModule } from "../../crypto/crypto.module";
65
import { SessionModule } from "../../session/session.module";
76
import { WebhookService } from "../../shared/utils/webhook/webhook.service";
7+
import { PresentationsModule } from "../../verifier/presentations/presentations.module";
88
import { StatusListModule } from "../lifecycle/status/status-list.module";
99
import { CredentialConfigService } from "./credentials/credential-config/credential-config.service";
1010
import { CredentialConfigController } from "./credentials/credential-config.controller";
@@ -30,6 +30,7 @@ import { IssuanceConfigController } from "./issuance/issuance-config.controller"
3030
StatusListModule,
3131
HttpModule,
3232
SessionModule,
33+
PresentationsModule,
3334
TypeOrmModule.forFeature([IssuanceConfig, CredentialConfig]),
3435
],
3536
controllers: [IssuanceConfigController, CredentialConfigController],

apps/backend/src/issuer/configuration/credentials/credential-config/credential-config.service.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readFileSync } from "node:fs";
2-
import { Injectable, Logger } from "@nestjs/common";
2+
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
33
import { InjectRepository } from "@nestjs/typeorm";
44
import { plainToClass } from "class-transformer";
55
import { Repository } from "typeorm";
@@ -11,9 +11,11 @@ import {
1111
ImportPhase,
1212
} from "../../../../shared/utils/config-import/config-import-orchestrator.service";
1313
import { FilesService } from "../../../../storage/files.service";
14+
import { PresentationsService } from "../../../../verifier/presentations/presentations.service";
1415
import { CredentialConfigCreate } from "../dto/credential-config-create.dto";
1516
import { CredentialConfigUpdate } from "../dto/credential-config-update.dto";
1617
import { CredentialConfig } from "../entities/credential.entity";
18+
import { IaeActionType } from "../entities/iae-action.dto";
1719

1820
/**
1921
* Service for managing credential configurations.
@@ -33,6 +35,7 @@ export class CredentialConfigService {
3335
private readonly filesService: FilesService,
3436
private readonly configImportService: ConfigImportService,
3537
private readonly configImportOrchestrator: ConfigImportOrchestratorService,
38+
private readonly presentationsService: PresentationsService,
3639
) {
3740
this.configImportOrchestrator.register(
3841
"credentials",
@@ -76,6 +79,8 @@ export class CredentialConfigService {
7679

7780
/**
7881
* Process a credential config for import.
82+
* Note: IAE action validation is skipped during import because
83+
* presentation configs are imported in a later phase (REFERENCES).
7984
*/
8085
private async processCredentialConfig(
8186
tenantId: string,
@@ -99,7 +104,8 @@ export class CredentialConfigService {
99104
(config as CredentialConfig).cert = cert;
100105
}
101106

102-
await this.store(tenantId, config);
107+
// Skip IAE validation during import - presentation configs are imported later
108+
await this.store(tenantId, config, true);
103109
}
104110

105111
/**
@@ -175,16 +181,60 @@ export class CredentialConfigService {
175181
});
176182
}
177183

184+
/**
185+
* Validates IAE actions in a credential configuration.
186+
* Checks that all referenced presentation configs exist.
187+
* @param tenantId - The ID of the tenant.
188+
* @param config - The credential config to validate.
189+
* @throws BadRequestException if a referenced presentation config doesn't exist.
190+
*/
191+
private async validateIaeActions(
192+
tenantId: string,
193+
config: CredentialConfigCreate | CredentialConfigUpdate,
194+
): Promise<void> {
195+
if (!config.iaeActions?.length) {
196+
return;
197+
}
198+
199+
for (const action of config.iaeActions) {
200+
if (action.type === IaeActionType.OPENID4VP_PRESENTATION) {
201+
const presentationConfigId = (
202+
action as { presentationConfigId: string }
203+
).presentationConfigId;
204+
205+
try {
206+
await this.presentationsService.getPresentationConfig(
207+
presentationConfigId,
208+
tenantId,
209+
);
210+
} catch {
211+
throw new BadRequestException(
212+
`IAE action references presentation config '${presentationConfigId}' which does not exist`,
213+
);
214+
}
215+
}
216+
}
217+
}
218+
178219
/**
179220
* Stores a credential configuration for a given tenant.
180221
* If the configuration already exists, it will be overwritten.
181222
* Automatically replaces image references with public URLs.
223+
* Validates IAE action references.
182224
* @param tenantId - The ID of the tenant.
183225
* @param config - The CredentialConfig entity to store.
226+
* @param skipValidation - Skip IAE action validation (used during file imports).
184227
* @returns A promise that resolves to the stored CredentialConfig entity.
185228
*/
186-
async store(tenantId: string, config: CredentialConfigCreate) {
229+
async store(
230+
tenantId: string,
231+
config: CredentialConfigCreate,
232+
skipValidation = false,
233+
) {
187234
await this.replaceImageReferences(tenantId, config);
235+
if (!skipValidation) {
236+
await this.validateIaeActions(tenantId, config);
237+
}
188238
return this.credentialConfigRepository.save({
189239
...config,
190240
tenantId,
@@ -196,13 +246,15 @@ export class CredentialConfigService {
196246
* Only updates fields that are provided in the config.
197247
* Set fields to null to clear them.
198248
* Automatically replaces image references with public URLs.
249+
* Validates IAE action references.
199250
* @param tenantId - The ID of the tenant.
200251
* @param id - The ID of the CredentialConfig entity to update.
201252
* @param config - The partial CredentialConfig to update.
202253
* @returns A promise that resolves to the updated CredentialConfig entity.
203254
*/
204255
async update(tenantId: string, id: string, config: CredentialConfigUpdate) {
205256
await this.replaceImageReferences(tenantId, config);
257+
await this.validateIaeActions(tenantId, config);
206258
const existing = await this.getById(tenantId, id);
207259
return this.credentialConfigRepository.save({
208260
...existing,

apps/backend/src/issuer/configuration/credentials/credentials.service.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ export class CredentialsService {
7171
private readonly cryptoImplementationService: CryptoImplementationService,
7272
) {}
7373

74+
/**
75+
* Returns a single credential configuration by ID.
76+
* @param id The credential configuration ID
77+
* @param tenantId The tenant ID
78+
* @returns The credential configuration or null if not found
79+
*/
80+
async getCredentialConfig(
81+
id: string,
82+
tenantId: string,
83+
): Promise<CredentialConfig | null> {
84+
return this.credentialConfigRepo.findOneBy({ id, tenantId });
85+
}
86+
7487
/**
7588
* Returns the credential configuration that is required for oid4vci
7689
* @param tenantId

apps/backend/src/issuer/configuration/credentials/entities/credential.entity.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "@nestjs/swagger";
77
import { Type } from "class-transformer";
88
import {
9+
IsArray,
910
IsBoolean,
1011
IsEnum,
1112
IsNumber,
@@ -20,6 +21,13 @@ import { CertEntity } from "../../../../crypto/key/entities/cert.entity";
2021
import { WebhookConfig } from "../../../../shared/utils/webhook/webhook.dto";
2122
import { SchemaResponse } from "../../../issuance/oid4vci/metadata/dto/schema-response.dto";
2223
import { VCT } from "../../../issuance/oid4vci/metadata/dto/vct.dto";
24+
import {
25+
IaeAction,
26+
IaeActionBase,
27+
IaeActionOpenid4vpPresentation,
28+
IaeActionRedirectToWeb,
29+
IaeActionType,
30+
} from "./iae-action.dto";
2331
import {
2432
AllowListPolicy,
2533
AttestationBasedPolicy,
@@ -108,6 +116,8 @@ export class IssuerMetadataCredentialConfig {
108116
AllowListPolicy,
109117
RootOfTrustPolicy,
110118
VCT,
119+
IaeActionOpenid4vpPresentation,
120+
IaeActionRedirectToWeb,
111121
)
112122
@Entity()
113123
export class CredentialConfig {
@@ -204,6 +214,57 @@ export class CredentialConfig {
204214
@IsBoolean()
205215
statusManagement?: boolean;
206216

217+
/**
218+
* List of Interactive Authorization Endpoint (IAE) actions to execute
219+
* before credential issuance. Actions are executed in order.
220+
*
221+
* Each action can be:
222+
* - `openid4vp_presentation`: Request a verifiable presentation from the wallet
223+
* - `redirect_to_web`: Redirect user to a web page for additional interaction
224+
*
225+
* If empty or not set, no interactive authorization is required.
226+
*
227+
* @example
228+
* [
229+
* { "type": "openid4vp_presentation", "presentationConfigId": "pid-config" },
230+
* { "type": "redirect_to_web", "url": "https://example.com/verify", "label": "Additional Verification" }
231+
* ]
232+
*/
233+
@IsOptional()
234+
@IsArray()
235+
@ValidateNested({ each: true })
236+
@Type(() => IaeActionBase, {
237+
discriminator: {
238+
property: "type",
239+
subTypes: [
240+
{
241+
name: IaeActionType.OPENID4VP_PRESENTATION,
242+
value: IaeActionOpenid4vpPresentation,
243+
},
244+
{
245+
name: IaeActionType.REDIRECT_TO_WEB,
246+
value: IaeActionRedirectToWeb,
247+
},
248+
],
249+
},
250+
keepDiscriminatorProperty: true,
251+
})
252+
@ApiProperty({
253+
description:
254+
"List of IAE actions to execute before credential issuance",
255+
type: "array",
256+
items: {
257+
oneOf: [
258+
{ $ref: getSchemaPath(IaeActionOpenid4vpPresentation) },
259+
{ $ref: getSchemaPath(IaeActionRedirectToWeb) },
260+
],
261+
},
262+
nullable: true,
263+
required: false,
264+
})
265+
@Column("json", { nullable: true })
266+
iaeActions?: IaeAction[] | null;
267+
207268
@IsOptional()
208269
@Column("int", { nullable: true })
209270
@IsNumber()

0 commit comments

Comments
 (0)