Skip to content

Commit bfecc7a

Browse files
committed
feat: oidc implementation
Signed-off-by: Tipu_Singh <[email protected]>
1 parent 6f4be27 commit bfecc7a

File tree

28 files changed

+3370
-6
lines changed

28 files changed

+3370
-6
lines changed

apps/agent-service/src/agent-service.controller.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,4 +323,38 @@ export class AgentServiceController {
323323
async agentdetailsByOrgId(payload: { orgId: string }): Promise<IStoreAgent> {
324324
return this.agentServiceService.getAgentDetails(payload.orgId);
325325
}
326+
327+
@MessagePattern({ cmd: 'agent-create-oidc-issuer' })
328+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
329+
async oidcIssuerCreate(payload: { issuerCreation; url: string; orgId: string }): Promise<any> {
330+
return this.agentServiceService.oidcIssuerCreate(payload.issuerCreation, payload.url, payload.orgId);
331+
}
332+
@MessagePattern({ cmd: 'delete-oidc-issuer' })
333+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
334+
async oidcDeleteIssuer(payload: { url: string; orgId: string }): Promise<any> {
335+
return this.agentServiceService.deleteOidcIssuer(payload.url, payload.orgId);
336+
}
337+
@MessagePattern({ cmd: 'agent-create-oidc-template' })
338+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
339+
async oidcIssuerTemplate(payload: { templatePayload; url: string; orgId: string }): Promise<any> {
340+
return this.agentServiceService.oidcIssuerTemplate(payload.templatePayload, payload.url, payload.orgId);
341+
}
342+
//TODO: change message for oidc
343+
@MessagePattern({ cmd: 'oidc-get-issuer-by-id' })
344+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
345+
async oidcGetIssuerById(payload: { url: string; orgId: string }): Promise<any> {
346+
return this.agentServiceService.oidcGetIssuerById(payload.url, payload.orgId);
347+
}
348+
349+
@MessagePattern({ cmd: 'oidc-get-issuers' })
350+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
351+
async oidcGetIssuers(payload: { url: string; orgId: string }): Promise<any> {
352+
return this.agentServiceService.oidcGetIssuers(payload.url, payload.orgId);
353+
}
354+
355+
@MessagePattern({ cmd: 'agent-service-oidc-create-credential-offer' })
356+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
357+
async oidcCreateCredentialOffer(payload: { credentialPayload; url: string; orgId: string }): Promise<any> {
358+
return this.agentServiceService.oidcCreateCredentialOffer(payload.credentialPayload, payload.url, payload.orgId);
359+
}
326360
}

apps/agent-service/src/agent-service.service.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,84 @@ export class AgentServiceService {
14101410
}
14111411
}
14121412

1413+
async oidcIssuerCreate(issueData, url: string, orgId: string): Promise<object> {
1414+
try {
1415+
const getApiKey = await this.getOrgAgentApiKey(orgId);
1416+
const data = await this.commonService
1417+
.httpPost(url, issueData, { headers: { authorization: getApiKey } })
1418+
.then(async (response) => response);
1419+
return data;
1420+
} catch (error) {
1421+
this.logger.error(`Error in oidcIssuerCreate in agent service : ${JSON.stringify(error)}`);
1422+
throw error;
1423+
}
1424+
}
1425+
1426+
async deleteOidcIssuer(url: string, orgId: string): Promise<object> {
1427+
try {
1428+
const getApiKey = await this.getOrgAgentApiKey(orgId);
1429+
const data = await this.commonService
1430+
.httpDelete(url, { headers: { authorization: getApiKey } })
1431+
.then(async (response) => response);
1432+
return data;
1433+
} catch (error) {
1434+
this.logger.error(`Error in deleteOidcIssuer in agent service : ${JSON.stringify(error)}`);
1435+
throw error;
1436+
}
1437+
}
1438+
1439+
async oidcGetIssuerById(url: string, orgId: string): Promise<object> {
1440+
try {
1441+
const getApiKey = await this.getOrgAgentApiKey(orgId);
1442+
const data = await this.commonService
1443+
.httpGet(url, { headers: { authorization: getApiKey } })
1444+
.then(async (response) => response);
1445+
return data;
1446+
} catch (error) {
1447+
this.logger.error(`Error in oidcGetIssuerById in agent service : ${JSON.stringify(error)}`);
1448+
throw error;
1449+
}
1450+
}
1451+
1452+
async oidcGetIssuers(url: string, orgId: string): Promise<object> {
1453+
try {
1454+
const getApiKey = await this.getOrgAgentApiKey(orgId);
1455+
const data = await this.commonService
1456+
.httpGet(url, { headers: { authorization: getApiKey } })
1457+
.then(async (response) => response);
1458+
return data;
1459+
} catch (error) {
1460+
this.logger.error(`Error in oidcGetIssuers in agent service : ${JSON.stringify(error)}`);
1461+
throw error;
1462+
}
1463+
}
1464+
1465+
async oidcCreateCredentialOffer(credentialPayload, url: string, orgId: string): Promise<object> {
1466+
try {
1467+
const getApiKey = await this.getOrgAgentApiKey(orgId);
1468+
const data = await this.commonService
1469+
.httpPost(url, credentialPayload, { headers: { authorization: getApiKey } })
1470+
.then(async (response) => response);
1471+
return data;
1472+
} catch (error) {
1473+
this.logger.error(`Error in oidcCreateCredentialOffer in agent service : ${JSON.stringify(error)}`);
1474+
throw error;
1475+
}
1476+
}
1477+
1478+
async oidcIssuerTemplate(templatePayload, url: string, orgId: string): Promise<object> {
1479+
try {
1480+
const getApiKey = await this.getOrgAgentApiKey(orgId);
1481+
const data = await this.commonService
1482+
.httpPut(url, templatePayload, { headers: { authorization: getApiKey } })
1483+
.then(async (response) => response);
1484+
return data;
1485+
} catch (error) {
1486+
this.logger.error(`Error in oidcIssuerTemplate in agent service : ${JSON.stringify(error)}`);
1487+
throw error;
1488+
}
1489+
}
1490+
14131491
async getIssueCredentials(url: string, apiKey: string): Promise<object> {
14141492
try {
14151493
const data = await this.commonService
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
2+
/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */
3+
4+
import {
5+
IsArray,
6+
IsEnum,
7+
IsNotEmpty,
8+
IsObject,
9+
IsOptional,
10+
IsString,
11+
Matches,
12+
ValidateNested,
13+
registerDecorator,
14+
ValidationOptions,
15+
IsInt,
16+
Min,
17+
IsIn,
18+
ArrayMinSize,
19+
IsUrl,
20+
ValidatorConstraint,
21+
ValidatorConstraintInterface,
22+
ValidationArguments,
23+
Validate
24+
} from 'class-validator';
25+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
26+
import { Type } from 'class-transformer';
27+
28+
export enum CredentialFormat {
29+
SdJwtVc = 'vc+sd-jwt',
30+
Mdoc = 'mdoc'
31+
}
32+
33+
export enum SignerMethodOption {
34+
DID = 'did',
35+
X5C = 'x5c'
36+
}
37+
38+
/** ---------- custom validator: disclosureFrame ---------- */
39+
function isDisclosureFrameValue(v: unknown): boolean {
40+
if ('boolean' === typeof v) {
41+
return true;
42+
}
43+
if (v && 'object' === typeof v && !Array.isArray(v)) {
44+
return Object.values(v as Record<string, unknown>).every((x) => 'boolean' === typeof x);
45+
}
46+
return false;
47+
}
48+
49+
export function IsDisclosureFrame(options?: ValidationOptions) {
50+
return function (object: unknown, propertyName: string) {
51+
registerDecorator({
52+
name: 'IsDisclosureFrame',
53+
target: (object as object).constructor,
54+
propertyName,
55+
options,
56+
validator: {
57+
validate(value: unknown) {
58+
if (value === undefined) {
59+
return true;
60+
}
61+
if (!value || 'object' !== typeof value || Array.isArray(value)) {
62+
return false;
63+
}
64+
return Object.values(value as Record<string, unknown>).every(isDisclosureFrameValue);
65+
},
66+
defaultMessage() {
67+
return 'disclosureFrame must be a map of booleans or nested maps of booleans';
68+
}
69+
}
70+
});
71+
};
72+
}
73+
74+
/** ---------- payload DTOs ---------- */
75+
export class CredentialPayloadDto {
76+
@ApiPropertyOptional()
77+
@IsOptional()
78+
@IsString()
79+
vct?: string;
80+
81+
@ApiPropertyOptional({ example: 'Garry' })
82+
@IsOptional()
83+
@IsString()
84+
full_name?: string;
85+
86+
@ApiPropertyOptional({ example: '2000-01-01', description: 'YYYY-MM-DD' })
87+
@IsOptional()
88+
@Matches(/^\d{4}-\d{2}-\d{2}$/, { message: 'birth_date must be YYYY-MM-DD' })
89+
birth_date?: string;
90+
91+
@ApiPropertyOptional({ example: 'Africa' })
92+
@IsOptional()
93+
@IsString()
94+
birth_place?: string;
95+
96+
@ApiPropertyOptional({ example: 'James Bear' })
97+
@IsOptional()
98+
@IsString()
99+
parent_names?: string;
100+
101+
[key: string]: unknown;
102+
}
103+
104+
export class CredentialRequestDto {
105+
@ApiProperty({ example: '1b2d3c4e-...' })
106+
@IsString()
107+
@IsNotEmpty()
108+
templateId!: string;
109+
110+
@ApiProperty({ enum: CredentialFormat, example: CredentialFormat.SdJwtVc })
111+
@IsEnum(CredentialFormat)
112+
format!: CredentialFormat;
113+
114+
@ApiProperty({
115+
type: CredentialPayloadDto,
116+
description: 'Credential payload (structure depends on the format)'
117+
})
118+
@ValidateNested()
119+
@Type(() => CredentialPayloadDto)
120+
payload!: CredentialPayloadDto;
121+
122+
@ApiPropertyOptional({
123+
description: 'Selective disclosure frame (claim -> boolean or nested map).',
124+
example: { full_name: true, birth_date: true, birth_place: false, parent_names: false },
125+
required: false
126+
})
127+
@IsOptional()
128+
@IsObject()
129+
@IsDisclosureFrame()
130+
disclosureFrame?: Record<string, boolean | Record<string, boolean>>;
131+
}
132+
133+
/** ---------- auth-config DTOs ---------- */
134+
export class TxCodeDto {
135+
@ApiPropertyOptional({ example: 'test abc' })
136+
@IsOptional()
137+
@IsString()
138+
description?: string;
139+
140+
@ApiProperty({ example: 4 })
141+
@IsInt()
142+
@Min(1)
143+
length!: number;
144+
145+
@ApiProperty({ example: 'numeric', enum: ['numeric'] })
146+
@IsString()
147+
@IsIn(['numeric'])
148+
input_mode!: 'numeric';
149+
}
150+
151+
export class PreAuthorizedCodeFlowConfigDto {
152+
@ApiProperty({ type: TxCodeDto })
153+
@ValidateNested()
154+
@Type(() => TxCodeDto)
155+
txCode!: TxCodeDto;
156+
157+
@ApiProperty({
158+
example: 'http://localhost:4001/oid4vci/abc-gov',
159+
description: 'AS (Authorization Server) base URL'
160+
})
161+
@IsUrl({ require_tld: false })
162+
authorizationServerUrl!: string;
163+
}
164+
165+
export class AuthorizationCodeFlowConfigDto {
166+
@ApiProperty({
167+
example: 'https://id.credebl.ae:8443/realms/credebl',
168+
description: 'AS (Authorization Server) base URL'
169+
})
170+
@IsUrl({ require_tld: false })
171+
authorizationServerUrl!: string;
172+
}
173+
174+
/** ---------- class-level constraint: EXACTLY ONE of the two configs ---------- */
175+
@ValidatorConstraint({ name: 'ExactlyOneOf', async: false })
176+
class ExactlyOneOfConstraint implements ValidatorConstraintInterface {
177+
validate(_: unknown, args: ValidationArguments) {
178+
const obj = args.object as Record<string, unknown>;
179+
const keys = (args.constraints ?? []) as string[];
180+
const present = keys.filter((k) => obj[k] !== undefined && null !== obj[k]);
181+
return 1 === present.length;
182+
}
183+
defaultMessage(args: ValidationArguments) {
184+
const keys = (args.constraints ?? []) as string[];
185+
return `Exactly one of [${keys.join(', ')}] must be provided (not both, not neither).`;
186+
}
187+
}
188+
function ExactlyOneOf(keys: string[], options?: ValidationOptions) {
189+
return Validate(ExactlyOneOfConstraint, keys, options);
190+
}
191+
192+
/** ---------- root DTO (no authenticationType) ---------- */
193+
export class CreateOidcCredentialOfferDto {
194+
@ApiProperty({
195+
type: [CredentialRequestDto],
196+
description: 'At least one credential to be issued.'
197+
})
198+
@IsArray()
199+
@ArrayMinSize(1)
200+
@ValidateNested({ each: true })
201+
@Type(() => CredentialRequestDto)
202+
credentials!: CredentialRequestDto[];
203+
204+
// Each is optional individually; XOR rule below enforces exactly one present.
205+
@ApiPropertyOptional({ type: PreAuthorizedCodeFlowConfigDto })
206+
@IsOptional()
207+
@ValidateNested()
208+
@Type(() => PreAuthorizedCodeFlowConfigDto)
209+
preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto;
210+
211+
@ApiPropertyOptional({ type: AuthorizationCodeFlowConfigDto })
212+
@IsOptional()
213+
@ValidateNested()
214+
@Type(() => AuthorizationCodeFlowConfigDto)
215+
authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto;
216+
217+
issuerId?: string;
218+
219+
// Host the class-level XOR validator on a dummy property
220+
@ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], {
221+
message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'
222+
})
223+
private readonly _exactlyOne?: unknown;
224+
}

0 commit comments

Comments
 (0)