nestjs-ddd-cli 3.2.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -9
- package/ddd.schema.json +11 -1
- package/dist/commands/doctor-enhanced.d.ts +1 -0
- package/dist/commands/doctor-enhanced.js +94 -79
- package/dist/commands/doctor-enhanced.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +68 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-all.js +54 -28
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-controller.d.ts +1 -0
- package/dist/commands/generate-controller.js +71 -0
- package/dist/commands/generate-controller.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +1 -1
- package/dist/commands/generate-domain-service.js +6 -4
- package/dist/commands/generate-domain-service.js.map +1 -1
- package/dist/commands/generate-dto.d.ts +4 -0
- package/dist/commands/generate-dto.js +162 -0
- package/dist/commands/generate-dto.js.map +1 -0
- package/dist/commands/generate-entity.js +7 -2
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-event.js +10 -4
- package/dist/commands/generate-event.js.map +1 -1
- package/dist/commands/generate-migration-deployment.d.ts +15 -0
- package/dist/commands/generate-migration-deployment.js +326 -0
- package/dist/commands/generate-migration-deployment.js.map +1 -0
- package/dist/commands/generate-migration-deployment.spec.d.ts +1 -0
- package/dist/commands/generate-migration-deployment.spec.js +337 -0
- package/dist/commands/generate-migration-deployment.spec.js.map +1 -0
- package/dist/commands/generate-module.js +4 -2
- package/dist/commands/generate-module.js.map +1 -1
- package/dist/commands/generate-query.js +23 -4
- package/dist/commands/generate-query.js.map +1 -1
- package/dist/commands/generate-repository.d.ts +1 -1
- package/dist/commands/generate-repository.js +17 -34
- package/dist/commands/generate-repository.js.map +1 -1
- package/dist/commands/generate-service.js +19 -4
- package/dist/commands/generate-service.js.map +1 -1
- package/dist/commands/generate-usecase.js +12 -31
- package/dist/commands/generate-usecase.js.map +1 -1
- package/dist/commands/generate-value-object.js +3 -14
- package/dist/commands/generate-value-object.js.map +1 -1
- package/dist/commands/init-project.d.ts +3 -0
- package/dist/commands/init-project.js +19 -9
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.js +10 -0
- package/dist/commands/interactive-scaffold.js.map +1 -1
- package/dist/commands/migration.d.ts +3 -0
- package/dist/commands/migration.js +83 -48
- package/dist/commands/migration.js.map +1 -1
- package/dist/commands/recipe.d.ts +2 -0
- package/dist/commands/recipe.js +229 -85
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipes/banklink-connector-contract.recipe.d.ts +1 -0
- package/dist/commands/recipes/banklink-connector-contract.recipe.js +161 -0
- package/dist/commands/recipes/banklink-connector-contract.recipe.js.map +1 -0
- package/dist/commands/recipes/bounded-json-http.recipe.d.ts +2 -0
- package/dist/commands/recipes/bounded-json-http.recipe.js +53 -0
- package/dist/commands/recipes/bounded-json-http.recipe.js.map +1 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.d.ts +1 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.js +223 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.js.map +1 -0
- package/dist/commands/recipes/event-backbone.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-backbone.recipe.js +1118 -0
- package/dist/commands/recipes/event-backbone.recipe.js.map +1 -0
- package/dist/commands/recipes/external-projection-worker.recipe.d.ts +6 -0
- package/dist/commands/recipes/external-projection-worker.recipe.js +211 -0
- package/dist/commands/recipes/external-projection-worker.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +11 -0
- package/dist/commands/recipes/index.js +11 -0
- package/dist/commands/recipes/index.js.map +1 -1
- package/dist/commands/recipes/jest-cjs-compat.recipe.d.ts +3 -0
- package/dist/commands/recipes/jest-cjs-compat.recipe.js +130 -0
- package/dist/commands/recipes/jest-cjs-compat.recipe.js.map +1 -0
- package/dist/commands/recipes/joi-env.recipe.js +1 -1
- package/dist/commands/recipes/kafka-consumer.recipe.d.ts +1 -0
- package/dist/commands/recipes/kafka-consumer.recipe.js +65 -0
- package/dist/commands/recipes/kafka-consumer.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.js +60 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.js +67 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.js +57 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.js +463 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.js +85 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-context.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-context.recipe.js +229 -0
- package/dist/commands/recipes/platform-context.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.js +339 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js +56 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.js +103 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.js +415 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.js.map +1 -0
- package/dist/index.js +77 -9
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +39 -3
- package/dist/templates/ai-context/conventions.md.hbs +13 -8
- package/dist/templates/command/create-command.hbs +8 -3
- package/dist/templates/command/delete-command.hbs +1 -1
- package/dist/templates/command/update-command.hbs +1 -1
- package/dist/templates/controller/application-controller.hbs +7 -0
- package/dist/templates/controller/controller.hbs +6 -0
- package/dist/templates/dto/create-dto.hbs +17 -5
- package/dist/templates/dto/filter-dto.hbs +1 -1
- package/dist/templates/dto/response-dto.hbs +1 -1
- package/dist/templates/entity/entity.hbs +4 -0
- package/dist/templates/mapper/mapper.hbs +5 -1
- package/dist/templates/orm-entity/orm-entity.hbs +9 -1
- package/dist/templates/prisma/prisma-mapper.hbs +6 -0
- package/dist/templates/prisma/prisma-repository.hbs +57 -5
- package/dist/templates/prisma/prisma-schema.hbs +2 -0
- package/dist/templates/query/get-all.query.hbs +10 -2
- package/dist/templates/recipes/bounded-json-http/README.md.hbs +36 -0
- package/dist/templates/recipes/external-projection-worker/README.md.hbs +157 -0
- package/dist/templates/recipes/external-projection-worker/canonical-json.ts.hbs +123 -0
- package/dist/templates/recipes/external-projection-worker/enqueuer.ts.hbs +167 -0
- package/dist/templates/recipes/external-projection-worker/index.ts.hbs +6 -0
- package/dist/templates/recipes/external-projection-worker/migration.ts.hbs +88 -0
- package/dist/templates/recipes/external-projection-worker/module.ts.hbs +36 -0
- package/dist/templates/recipes/external-projection-worker/store.ts.hbs +354 -0
- package/dist/templates/recipes/external-projection-worker/types.ts.hbs +134 -0
- package/dist/templates/recipes/external-projection-worker/worker.ts.hbs +296 -0
- package/dist/templates/recipes/kafka-consumer/README.md.hbs +50 -0
- package/dist/templates/recipes/kafka-consumer/kafka-consumer.ts.hbs +135 -0
- package/dist/templates/recipes/kafka-consumer/migration.ts.hbs +17 -0
- package/dist/templates/recipes/oauth2-client-credentials/README.md.hbs +40 -0
- package/dist/templates/recipes/oauth2-client-credentials/client.spec.ts.hbs +234 -0
- package/dist/templates/recipes/oauth2-client-credentials/client.ts.hbs +210 -0
- package/dist/templates/recipes/oauth2-client-credentials/index.ts.hbs +2 -0
- package/dist/templates/recipes/oauth2-client-credentials/module.ts.hbs +58 -0
- package/dist/templates/recipes/oauth2-token-introspection/README.md.hbs +158 -0
- package/dist/templates/recipes/oauth2-token-introspection/client.spec.ts.hbs +350 -0
- package/dist/templates/recipes/oauth2-token-introspection/client.ts.hbs +241 -0
- package/dist/templates/recipes/oauth2-token-introspection/http-client.spec.ts.hbs +411 -0
- package/dist/templates/recipes/oauth2-token-introspection/http-client.ts.hbs +343 -0
- package/dist/templates/recipes/oauth2-token-introspection/index.ts.hbs +3 -0
- package/dist/templates/recipes/oauth2-token-introspection/module.ts.hbs +58 -0
- package/dist/templates/recipes/oidc-bff-session/README.md.hbs +96 -0
- package/dist/templates/recipes/oidc-bff-session/client.test.ts.hbs +663 -0
- package/dist/templates/recipes/oidc-bff-session/client.ts.hbs +635 -0
- package/dist/templates/recipes/oidc-bff-session/index.ts.hbs +3 -0
- package/dist/templates/recipes/oidc-bff-session/store.integration.test.ts.hbs +480 -0
- package/dist/templates/recipes/oidc-bff-session/store.test.ts.hbs +87 -0
- package/dist/templates/recipes/oidc-bff-session/store.ts.hbs +549 -0
- package/dist/templates/recipes/oidc-bff-session/valkey.test.ts.hbs +158 -0
- package/dist/templates/recipes/oidc-bff-session/valkey.ts.hbs +148 -0
- package/dist/templates/recipes/oidc-resource-server/README.md.hbs +74 -0
- package/dist/templates/recipes/oidc-resource-server/index.ts.hbs +2 -0
- package/dist/templates/recipes/oidc-resource-server/module.ts.hbs +51 -0
- package/dist/templates/recipes/oidc-resource-server/verifier.spec.ts.hbs +186 -0
- package/dist/templates/recipes/oidc-resource-server/verifier.ts.hbs +440 -0
- package/dist/templates/recipes/platform-parc-bound-authorization/README.md.hbs +92 -0
- package/dist/templates/recipes/platform-parc-bound-authorization/client.ts.hbs +142 -0
- package/dist/templates/recipes/platform-service-access-request-context/docs.md.hbs +61 -0
- package/dist/templates/recipes/platform-service-access-request-context/env.example.hbs +7 -0
- package/dist/templates/recipes/platform-service-access-request-context/index.ts.hbs +7 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-headers.ts.hbs +61 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.parser.ts.hbs +132 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.types.ts.hbs +40 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.client.ts.hbs +45 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.config.ts.hbs +142 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.guard.ts.hbs +77 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.module.ts.hbs +17 -0
- package/dist/templates/repository/repository.hbs +42 -4
- package/dist/templates/resolver/graphql-input.hbs +23 -6
- package/dist/templates/resolver/resolver.hbs +4 -0
- package/dist/templates/service/domain-service.hbs +2 -2
- package/dist/templates/test/controller.spec.hbs +5 -1
- package/dist/templates/test/prisma-repository.spec.hbs +192 -0
- package/dist/templates/test/repository.spec.hbs +40 -1
- package/dist/templates/test/usecase.spec.hbs +23 -4
- package/dist/templates/usecase/application-usecase.hbs +17 -0
- package/dist/templates/usecase/create-usecase.hbs +34 -3
- package/dist/templates/usecase/update-usecase.hbs +15 -1
- package/dist/utils/config.utils.d.ts +2 -0
- package/dist/utils/config.utils.js +14 -2
- package/dist/utils/config.utils.js.map +1 -1
- package/dist/utils/dependency.utils.d.ts +16 -2
- package/dist/utils/dependency.utils.js +100 -12
- package/dist/utils/dependency.utils.js.map +1 -1
- package/dist/utils/field.utils.d.ts +6 -0
- package/dist/utils/field.utils.js +50 -8
- package/dist/utils/field.utils.js.map +1 -1
- package/dist/utils/field.utils.spec.d.ts +1 -0
- package/dist/utils/field.utils.spec.js +38 -0
- package/dist/utils/field.utils.spec.js.map +1 -0
- package/dist/utils/file.utils.d.ts +23 -1
- package/dist/utils/file.utils.js +37 -1
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/naming.utils.js +5 -4
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/static-recipe.utils.d.ts +4 -0
- package/dist/utils/static-recipe.utils.js +148 -0
- package/dist/utils/static-recipe.utils.js.map +1 -0
- package/package.json +12 -5
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createLocalJWKSet, jwtVerify, type JWK, type JWTPayload } from 'jose';
|
|
3
|
+
|
|
4
|
+
const RESPONSE_BYTES = 65_536;
|
|
5
|
+
const TOKEN_BYTES = 16_384;
|
|
6
|
+
const LOGIN_SECONDS = 600;
|
|
7
|
+
export type OidcEndpoints = Readonly<{
|
|
8
|
+
discovery: string;
|
|
9
|
+
authorization: string;
|
|
10
|
+
token: string;
|
|
11
|
+
revocation: string;
|
|
12
|
+
jwks: string;
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
export type OidcBffConfig = Readonly<{
|
|
16
|
+
issuer: string;
|
|
17
|
+
clientId: string;
|
|
18
|
+
clientSecret: string;
|
|
19
|
+
redirectUri: string;
|
|
20
|
+
scopes: readonly string[];
|
|
21
|
+
endpoints: OidcEndpoints;
|
|
22
|
+
authorizationParameters?: Readonly<Record<string, string>>;
|
|
23
|
+
maxAccessTokenSeconds?: number;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
clockToleranceSeconds?: number;
|
|
26
|
+
}>;
|
|
27
|
+
export type PendingAuthorization = Readonly<{
|
|
28
|
+
state: string;
|
|
29
|
+
nonce: string;
|
|
30
|
+
codeVerifier: string;
|
|
31
|
+
startedAtMs: number;
|
|
32
|
+
requireFreshAuth: true;
|
|
33
|
+
}>;
|
|
34
|
+
export type VerifiedTokenSet = Readonly<{
|
|
35
|
+
accessToken: string;
|
|
36
|
+
refreshToken: string;
|
|
37
|
+
idToken: string;
|
|
38
|
+
expiresAtMs: number;
|
|
39
|
+
scopes: readonly string[];
|
|
40
|
+
subject: string;
|
|
41
|
+
authenticatedAtSeconds: number;
|
|
42
|
+
issuer: string;
|
|
43
|
+
clientId: string;
|
|
44
|
+
}>;
|
|
45
|
+
export type OidcBffDependencies = Readonly<{
|
|
46
|
+
fetch?: (url: string, init: RequestInit) => Promise<Response>;
|
|
47
|
+
now?: () => number;
|
|
48
|
+
onTokenPairIssued?: (
|
|
49
|
+
pair: Readonly<{ accessToken?: string; refreshToken?: string; expiresAtMs: number }>,
|
|
50
|
+
) => Promise<void>;
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
export class OidcInvalidGrantError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
readonly reason: 'pending' | 'state' | 'code' | 'invalid_grant' | 'token_response' | 'id_token',
|
|
56
|
+
) {
|
|
57
|
+
super('Sign-in could not be verified');
|
|
58
|
+
this.name = 'OidcInvalidGrantError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class OidcUnavailableError extends Error {
|
|
62
|
+
constructor(
|
|
63
|
+
readonly reason:
|
|
64
|
+
| 'timeout'
|
|
65
|
+
| 'network'
|
|
66
|
+
| 'http'
|
|
67
|
+
| 'invalid_response'
|
|
68
|
+
| 'metadata'
|
|
69
|
+
| 'jwks'
|
|
70
|
+
| 'persistence',
|
|
71
|
+
) {
|
|
72
|
+
super('Sign-in provider is unavailable');
|
|
73
|
+
this.name = 'OidcUnavailableError';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
78
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
function visible(value: unknown, maximum: number): value is string {
|
|
81
|
+
return (
|
|
82
|
+
typeof value === 'string' &&
|
|
83
|
+
value.length > 0 &&
|
|
84
|
+
value.length <= maximum &&
|
|
85
|
+
Array.from(value).every(
|
|
86
|
+
(character) => character.charCodeAt(0) >= 33 && character.charCodeAt(0) <= 126,
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
function strings(value: unknown): value is string[] {
|
|
91
|
+
return Array.isArray(value) && value.every((item: unknown) => typeof item === 'string');
|
|
92
|
+
}
|
|
93
|
+
function integer(value: unknown): value is number {
|
|
94
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
95
|
+
}
|
|
96
|
+
function equal(left: string, right: string): boolean {
|
|
97
|
+
const a = Buffer.from(left),
|
|
98
|
+
b = Buffer.from(right);
|
|
99
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
100
|
+
}
|
|
101
|
+
function parseJson(text: string): unknown {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(text) as unknown;
|
|
104
|
+
} catch {
|
|
105
|
+
throw new OidcUnavailableError('invalid_response');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function scopeName(value: unknown): value is string {
|
|
109
|
+
return visible(value, 256) && !value.includes('"') && !value.includes('\\');
|
|
110
|
+
}
|
|
111
|
+
function boundedSetting(value: number, minimum: number, maximum: number): number {
|
|
112
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
113
|
+
throw new TypeError('Invalid OIDC sign-in configuration');
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function snapshotConfig(input: OidcBffConfig) {
|
|
118
|
+
const config = {
|
|
119
|
+
...input,
|
|
120
|
+
scopes: Object.freeze([...input.scopes]),
|
|
121
|
+
endpoints: Object.freeze({ ...input.endpoints }),
|
|
122
|
+
authorizationParameters: Object.freeze({ ...input.authorizationParameters }),
|
|
123
|
+
};
|
|
124
|
+
let callback: URL, issuer: URL;
|
|
125
|
+
try {
|
|
126
|
+
callback = new URL(config.redirectUri);
|
|
127
|
+
issuer = new URL(config.issuer);
|
|
128
|
+
} catch {
|
|
129
|
+
throw new TypeError('Invalid OIDC sign-in configuration');
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
issuer.protocol !== 'https:' ||
|
|
133
|
+
issuer.username ||
|
|
134
|
+
issuer.password ||
|
|
135
|
+
issuer.search ||
|
|
136
|
+
issuer.hash ||
|
|
137
|
+
!visible(config.clientId, 256) ||
|
|
138
|
+
!visible(config.clientSecret, 4096) ||
|
|
139
|
+
config.scopes.length > 32 ||
|
|
140
|
+
!config.scopes.includes('openid') ||
|
|
141
|
+
!config.scopes.includes('offline_access') ||
|
|
142
|
+
new Set(config.scopes).size !== config.scopes.length ||
|
|
143
|
+
!config.scopes.every(scopeName) ||
|
|
144
|
+
callback.protocol !== 'https:' ||
|
|
145
|
+
callback.username ||
|
|
146
|
+
callback.password ||
|
|
147
|
+
callback.search ||
|
|
148
|
+
callback.hash ||
|
|
149
|
+
callback.href !== config.redirectUri
|
|
150
|
+
) {
|
|
151
|
+
throw new TypeError('Invalid OIDC sign-in configuration');
|
|
152
|
+
}
|
|
153
|
+
const endpointNames = ['discovery', 'authorization', 'token', 'revocation', 'jwks'] as const;
|
|
154
|
+
if (
|
|
155
|
+
endpointNames.some((name) => {
|
|
156
|
+
try {
|
|
157
|
+
const endpoint = new URL(config.endpoints[name]);
|
|
158
|
+
return (
|
|
159
|
+
endpoint.protocol !== 'https:' ||
|
|
160
|
+
endpoint.origin !== issuer.origin ||
|
|
161
|
+
endpoint.username ||
|
|
162
|
+
endpoint.password ||
|
|
163
|
+
endpoint.hash ||
|
|
164
|
+
endpoint.search ||
|
|
165
|
+
endpoint.href !== config.endpoints[name] ||
|
|
166
|
+
endpoint.href.length > 2048
|
|
167
|
+
);
|
|
168
|
+
} catch {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
throw new TypeError('Invalid OIDC sign-in configuration');
|
|
174
|
+
const reserved = new Set([
|
|
175
|
+
'client_id',
|
|
176
|
+
'redirect_uri',
|
|
177
|
+
'response_type',
|
|
178
|
+
'scope',
|
|
179
|
+
'state',
|
|
180
|
+
'nonce',
|
|
181
|
+
'code_challenge',
|
|
182
|
+
'code_challenge_method',
|
|
183
|
+
'prompt',
|
|
184
|
+
'max_age',
|
|
185
|
+
]);
|
|
186
|
+
if (
|
|
187
|
+
Object.keys(config.authorizationParameters).length > 16 ||
|
|
188
|
+
Object.entries(config.authorizationParameters).some(
|
|
189
|
+
([name, value]) =>
|
|
190
|
+
!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name) ||
|
|
191
|
+
reserved.has(name) ||
|
|
192
|
+
typeof value !== 'string' ||
|
|
193
|
+
value.length > 2048 ||
|
|
194
|
+
Array.from(value).some(
|
|
195
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
) {
|
|
199
|
+
throw new TypeError('Invalid OIDC sign-in configuration');
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
...config,
|
|
203
|
+
maxAccessTokenSeconds: boundedSetting(config.maxAccessTokenSeconds ?? 3600, 1, 86400),
|
|
204
|
+
timeoutMs: boundedSetting(config.timeoutMs ?? 5000, 10, 30_000),
|
|
205
|
+
clockToleranceSeconds: boundedSetting(config.clockToleranceSeconds ?? 5, 0, 10),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// One deadline covers discovery, token exchange, JWKS download and signature verification.
|
|
210
|
+
async function operation<T>(
|
|
211
|
+
timeoutMs: number,
|
|
212
|
+
work: (signal: AbortSignal) => Promise<T>,
|
|
213
|
+
): Promise<T> {
|
|
214
|
+
const controller = new AbortController();
|
|
215
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
216
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
217
|
+
timer = setTimeout(() => {
|
|
218
|
+
controller.abort();
|
|
219
|
+
reject(new OidcUnavailableError('timeout'));
|
|
220
|
+
}, timeoutMs);
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
return await Promise.race([work(controller.signal), deadline]);
|
|
224
|
+
} finally {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
controller.abort();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function responseText(response: Response, signal: AbortSignal): Promise<string> {
|
|
231
|
+
const length = response.headers.get('content-length');
|
|
232
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > RESPONSE_BYTES)) {
|
|
233
|
+
void response.body?.cancel().catch(() => {});
|
|
234
|
+
throw new OidcUnavailableError('invalid_response');
|
|
235
|
+
}
|
|
236
|
+
if (!response.body) return '';
|
|
237
|
+
const reader = response.body.getReader();
|
|
238
|
+
const abort = () => {
|
|
239
|
+
void reader.cancel().catch(() => {});
|
|
240
|
+
};
|
|
241
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
242
|
+
const chunks: Uint8Array[] = [];
|
|
243
|
+
let size = 0;
|
|
244
|
+
try {
|
|
245
|
+
for (;;) {
|
|
246
|
+
if (signal.aborted) throw new OidcUnavailableError('timeout');
|
|
247
|
+
const chunk = await reader.read();
|
|
248
|
+
if (chunk.done) break;
|
|
249
|
+
size += chunk.value.byteLength;
|
|
250
|
+
if (size > RESPONSE_BYTES) throw new OidcUnavailableError('invalid_response');
|
|
251
|
+
chunks.push(chunk.value);
|
|
252
|
+
}
|
|
253
|
+
const bytes = new Uint8Array(size);
|
|
254
|
+
let position = 0;
|
|
255
|
+
for (const chunk of chunks) {
|
|
256
|
+
bytes.set(chunk, position);
|
|
257
|
+
position += chunk.byteLength;
|
|
258
|
+
}
|
|
259
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error instanceof OidcUnavailableError) throw error;
|
|
262
|
+
throw new OidcUnavailableError(signal.aborted ? 'timeout' : 'invalid_response');
|
|
263
|
+
} finally {
|
|
264
|
+
signal.removeEventListener('abort', abort);
|
|
265
|
+
void reader.cancel().catch(() => {});
|
|
266
|
+
reader.releaseLock();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function jwks(value: unknown): { keys: JWK[] } {
|
|
271
|
+
if (
|
|
272
|
+
!record(value) ||
|
|
273
|
+
!Array.isArray(value['keys']) ||
|
|
274
|
+
value['keys'].length === 0 ||
|
|
275
|
+
value['keys'].length > 16
|
|
276
|
+
) {
|
|
277
|
+
throw new OidcUnavailableError('jwks');
|
|
278
|
+
}
|
|
279
|
+
const keys: JWK[] = [];
|
|
280
|
+
for (const key of value['keys'] as unknown[]) {
|
|
281
|
+
if (!record(key)) throw new OidcUnavailableError('jwks');
|
|
282
|
+
if (
|
|
283
|
+
key['kty'] !== 'RSA' ||
|
|
284
|
+
(key['use'] !== undefined && key['use'] !== 'sig') ||
|
|
285
|
+
(key['alg'] !== undefined && key['alg'] !== 'RS256')
|
|
286
|
+
)
|
|
287
|
+
continue;
|
|
288
|
+
if (
|
|
289
|
+
!visible(key['kid'], 128) ||
|
|
290
|
+
typeof key['n'] !== 'string' ||
|
|
291
|
+
!/^[A-Za-z0-9_-]+$/.test(key['n']) ||
|
|
292
|
+
typeof key['e'] !== 'string' ||
|
|
293
|
+
!/^[A-Za-z0-9_-]{1,8}$/.test(key['e']) ||
|
|
294
|
+
['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth'].some((field) => key[field] !== undefined) ||
|
|
295
|
+
(key['key_ops'] !== undefined &&
|
|
296
|
+
(!strings(key['key_ops']) || !key['key_ops'].includes('verify')))
|
|
297
|
+
) {
|
|
298
|
+
throw new OidcUnavailableError('jwks');
|
|
299
|
+
}
|
|
300
|
+
const modulus = Buffer.from(key['n'], 'base64url'),
|
|
301
|
+
exponent = Buffer.from(key['e'], 'base64url');
|
|
302
|
+
const firstModulusByte = modulus[0],
|
|
303
|
+
firstExponentByte = exponent[0],
|
|
304
|
+
lastExponentByte = exponent[exponent.length - 1];
|
|
305
|
+
if (
|
|
306
|
+
firstModulusByte === undefined ||
|
|
307
|
+
firstExponentByte === undefined ||
|
|
308
|
+
lastExponentByte === undefined ||
|
|
309
|
+
modulus.length < 256 ||
|
|
310
|
+
modulus.length > 1024 ||
|
|
311
|
+
firstModulusByte < 128 ||
|
|
312
|
+
exponent.length === 0 ||
|
|
313
|
+
exponent.length > 4 ||
|
|
314
|
+
lastExponentByte % 2 !== 1 ||
|
|
315
|
+
(exponent.length === 1 && firstExponentByte < 3)
|
|
316
|
+
) {
|
|
317
|
+
throw new OidcUnavailableError('jwks');
|
|
318
|
+
}
|
|
319
|
+
if (keys.some((existing) => existing.kid === key['kid']))
|
|
320
|
+
throw new OidcUnavailableError('jwks');
|
|
321
|
+
keys.push({
|
|
322
|
+
kty: 'RSA',
|
|
323
|
+
kid: key['kid'],
|
|
324
|
+
n: key['n'],
|
|
325
|
+
e: key['e'],
|
|
326
|
+
alg: 'RS256',
|
|
327
|
+
use: 'sig',
|
|
328
|
+
key_ops: ['verify'],
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (!keys.length) throw new OidcUnavailableError('jwks');
|
|
332
|
+
return { keys };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function createOidcBffClient(input: OidcBffConfig, dependencies: OidcBffDependencies = {}) {
|
|
336
|
+
const config = snapshotConfig(input);
|
|
337
|
+
const endpoints = config.endpoints;
|
|
338
|
+
const transport = dependencies.fetch ?? fetch;
|
|
339
|
+
const now = dependencies.now ?? Date.now;
|
|
340
|
+
const onTokenPairIssued = dependencies.onTokenPairIssued;
|
|
341
|
+
const credential = (value: string) => new URLSearchParams({ v: value }).toString().slice(2);
|
|
342
|
+
const authorization = `Basic ${Buffer.from(`${credential(config.clientId)}:${credential(config.clientSecret)}`).toString('base64')}`;
|
|
343
|
+
|
|
344
|
+
function pendingSnapshot(input: PendingAuthorization): PendingAuthorization {
|
|
345
|
+
const pending = Object.freeze({ ...input });
|
|
346
|
+
if (
|
|
347
|
+
!/^[A-Za-z0-9_-]{16,128}$/.test(pending.state) ||
|
|
348
|
+
!/^[A-Za-z0-9_-]{16,128}$/.test(pending.nonce) ||
|
|
349
|
+
!/^[A-Za-z0-9._~-]{43,128}$/.test(pending.codeVerifier) ||
|
|
350
|
+
!integer(pending.startedAtMs) ||
|
|
351
|
+
pending.requireFreshAuth !== true ||
|
|
352
|
+
pending.startedAtMs > now() + config.clockToleranceSeconds * 1000 ||
|
|
353
|
+
now() - pending.startedAtMs > LOGIN_SECONDS * 1000
|
|
354
|
+
)
|
|
355
|
+
throw new OidcInvalidGrantError('pending');
|
|
356
|
+
return pending;
|
|
357
|
+
}
|
|
358
|
+
async function request(url: string, signal: AbortSignal, form?: URLSearchParams) {
|
|
359
|
+
if (signal.aborted) throw new OidcUnavailableError('timeout');
|
|
360
|
+
let response: Response;
|
|
361
|
+
try {
|
|
362
|
+
response = await transport(url, {
|
|
363
|
+
method: form ? 'POST' : 'GET',
|
|
364
|
+
redirect: 'error',
|
|
365
|
+
cache: 'no-store',
|
|
366
|
+
signal,
|
|
367
|
+
headers: form
|
|
368
|
+
? {
|
|
369
|
+
Accept: 'application/json',
|
|
370
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
371
|
+
Authorization: authorization,
|
|
372
|
+
}
|
|
373
|
+
: { Accept: 'application/json' },
|
|
374
|
+
...(form ? { body: form.toString() } : {}),
|
|
375
|
+
});
|
|
376
|
+
} catch {
|
|
377
|
+
throw new OidcUnavailableError(signal.aborted ? 'timeout' : 'network');
|
|
378
|
+
}
|
|
379
|
+
if (!(response instanceof Response)) throw new OidcUnavailableError('invalid_response');
|
|
380
|
+
if (
|
|
381
|
+
response.redirected ||
|
|
382
|
+
(response.url && response.url !== url) ||
|
|
383
|
+
(response.status >= 300 && response.status < 400)
|
|
384
|
+
) {
|
|
385
|
+
void response.body?.cancel().catch(() => {});
|
|
386
|
+
throw new OidcUnavailableError('http');
|
|
387
|
+
}
|
|
388
|
+
const text = await responseText(response, signal);
|
|
389
|
+
return { response, text };
|
|
390
|
+
}
|
|
391
|
+
async function discover(signal: AbortSignal) {
|
|
392
|
+
const { response, text } = await request(endpoints.discovery, signal);
|
|
393
|
+
if (response.status !== 200) throw new OidcUnavailableError('http');
|
|
394
|
+
const metadata = parseJson(text);
|
|
395
|
+
if (
|
|
396
|
+
!record(metadata) ||
|
|
397
|
+
metadata['issuer'] !== config.issuer ||
|
|
398
|
+
metadata['authorization_endpoint'] !== endpoints.authorization ||
|
|
399
|
+
metadata['token_endpoint'] !== endpoints.token ||
|
|
400
|
+
metadata['revocation_endpoint'] !== endpoints.revocation ||
|
|
401
|
+
metadata['jwks_uri'] !== endpoints.jwks ||
|
|
402
|
+
!strings(metadata['response_types_supported']) ||
|
|
403
|
+
!metadata['response_types_supported'].includes('code') ||
|
|
404
|
+
!strings(metadata['id_token_signing_alg_values_supported']) ||
|
|
405
|
+
!metadata['id_token_signing_alg_values_supported'].includes('RS256') ||
|
|
406
|
+
!strings(metadata['token_endpoint_auth_methods_supported']) ||
|
|
407
|
+
!metadata['token_endpoint_auth_methods_supported'].includes('client_secret_basic') ||
|
|
408
|
+
!strings(metadata['code_challenge_methods_supported']) ||
|
|
409
|
+
!metadata['code_challenge_methods_supported'].includes('S256')
|
|
410
|
+
) {
|
|
411
|
+
throw new OidcUnavailableError('metadata');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function revokeKnownTokens(
|
|
415
|
+
signal: AbortSignal,
|
|
416
|
+
tokens: readonly (readonly [string, string])[],
|
|
417
|
+
) {
|
|
418
|
+
await Promise.all(
|
|
419
|
+
tokens.map(async ([hint, token]) => {
|
|
420
|
+
const { response } = await request(
|
|
421
|
+
endpoints.revocation,
|
|
422
|
+
signal,
|
|
423
|
+
new URLSearchParams({ token, token_type_hint: hint }),
|
|
424
|
+
);
|
|
425
|
+
if (response.status !== 200) throw new OidcUnavailableError('http');
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
function validateClaims(
|
|
430
|
+
payload: JWTPayload,
|
|
431
|
+
pending: PendingAuthorization,
|
|
432
|
+
accessToken: string,
|
|
433
|
+
code: string,
|
|
434
|
+
) {
|
|
435
|
+
const at = Math.floor(now() / 1000),
|
|
436
|
+
start = Math.floor(pending.startedAtMs / 1000),
|
|
437
|
+
tolerance = config.clockToleranceSeconds;
|
|
438
|
+
const audiences = typeof payload.aud === 'string' ? [payload.aud] : payload.aud;
|
|
439
|
+
if (
|
|
440
|
+
!visible(payload.sub, 256) ||
|
|
441
|
+
typeof payload['nonce'] !== 'string' ||
|
|
442
|
+
!equal(payload['nonce'], pending.nonce) ||
|
|
443
|
+
!strings(audiences) ||
|
|
444
|
+
audiences.length === 0 ||
|
|
445
|
+
audiences.length > 16 ||
|
|
446
|
+
new Set(audiences).size !== audiences.length ||
|
|
447
|
+
!audiences.includes(config.clientId) ||
|
|
448
|
+
(payload['azp'] !== undefined && payload['azp'] !== config.clientId) ||
|
|
449
|
+
(audiences.length > 1 && payload['azp'] !== config.clientId) ||
|
|
450
|
+
!integer(payload.exp) ||
|
|
451
|
+
!integer(payload.iat) ||
|
|
452
|
+
!integer(payload['auth_time']) ||
|
|
453
|
+
payload.exp <= at ||
|
|
454
|
+
payload.exp <= payload.iat ||
|
|
455
|
+
payload.exp - payload.iat > 86_400 ||
|
|
456
|
+
payload.iat < start - tolerance ||
|
|
457
|
+
payload.iat > at + tolerance ||
|
|
458
|
+
payload['auth_time'] < start - tolerance ||
|
|
459
|
+
payload['auth_time'] > at + tolerance ||
|
|
460
|
+
payload['auth_time'] > payload.iat + tolerance
|
|
461
|
+
) {
|
|
462
|
+
throw new OidcInvalidGrantError('id_token');
|
|
463
|
+
}
|
|
464
|
+
for (const [claim, value] of [
|
|
465
|
+
['at_hash', accessToken],
|
|
466
|
+
['c_hash', code],
|
|
467
|
+
] as const) {
|
|
468
|
+
if (payload[claim] === undefined) continue;
|
|
469
|
+
const expected = createHash('sha256')
|
|
470
|
+
.update(value)
|
|
471
|
+
.digest()
|
|
472
|
+
.subarray(0, 16)
|
|
473
|
+
.toString('base64url');
|
|
474
|
+
if (typeof payload[claim] !== 'string' || !equal(payload[claim], expected))
|
|
475
|
+
throw new OidcInvalidGrantError('id_token');
|
|
476
|
+
}
|
|
477
|
+
return { subject: payload.sub, authenticatedAtSeconds: payload['auth_time'] };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return Object.freeze({
|
|
481
|
+
async beginAuthorization(input: PendingAuthorization): Promise<string> {
|
|
482
|
+
const pending = pendingSnapshot(input);
|
|
483
|
+
return operation(config.timeoutMs, async (signal) => {
|
|
484
|
+
await discover(signal);
|
|
485
|
+
const query = new URLSearchParams({
|
|
486
|
+
client_id: config.clientId,
|
|
487
|
+
redirect_uri: config.redirectUri,
|
|
488
|
+
response_type: 'code',
|
|
489
|
+
scope: config.scopes.join(' '),
|
|
490
|
+
state: pending.state,
|
|
491
|
+
nonce: pending.nonce,
|
|
492
|
+
code_challenge: createHash('sha256').update(pending.codeVerifier).digest('base64url'),
|
|
493
|
+
code_challenge_method: 'S256',
|
|
494
|
+
...config.authorizationParameters,
|
|
495
|
+
prompt: 'login',
|
|
496
|
+
max_age: '0',
|
|
497
|
+
});
|
|
498
|
+
return `${endpoints.authorization}?${query}`;
|
|
499
|
+
});
|
|
500
|
+
},
|
|
501
|
+
async exchangeAuthorizationCode(
|
|
502
|
+
input: Readonly<{ code: string; state: string; pending: PendingAuthorization }>,
|
|
503
|
+
): Promise<VerifiedTokenSet> {
|
|
504
|
+
const pending = pendingSnapshot(input.pending),
|
|
505
|
+
code = input.code,
|
|
506
|
+
state = input.state;
|
|
507
|
+
if (!visible(state, 128) || !equal(state, pending.state))
|
|
508
|
+
throw new OidcInvalidGrantError('state');
|
|
509
|
+
if (!visible(code, 4096)) throw new OidcInvalidGrantError('code');
|
|
510
|
+
return operation(config.timeoutMs, async (signal) => {
|
|
511
|
+
await discover(signal);
|
|
512
|
+
const tokenRequestedAtMs = now();
|
|
513
|
+
const { response, text } = await request(
|
|
514
|
+
endpoints.token,
|
|
515
|
+
signal,
|
|
516
|
+
new URLSearchParams({
|
|
517
|
+
grant_type: 'authorization_code',
|
|
518
|
+
code,
|
|
519
|
+
redirect_uri: config.redirectUri,
|
|
520
|
+
code_verifier: pending.codeVerifier,
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
const raw = parseJson(text);
|
|
524
|
+
if (response.status === 400 && record(raw) && raw['error'] === 'invalid_grant')
|
|
525
|
+
throw new OidcInvalidGrantError('invalid_grant');
|
|
526
|
+
if (response.status !== 200) throw new OidcUnavailableError('http');
|
|
527
|
+
const minted: [string, string][] = [];
|
|
528
|
+
if (record(raw)) {
|
|
529
|
+
if (visible(raw['access_token'], TOKEN_BYTES))
|
|
530
|
+
minted.push(['access_token', raw['access_token']]);
|
|
531
|
+
if (visible(raw['refresh_token'], TOKEN_BYTES))
|
|
532
|
+
minted.push(['refresh_token', raw['refresh_token']]);
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
if (onTokenPairIssued && minted.length) {
|
|
536
|
+
// Custody precedes admission. Fallback expiry is only a configured retirement hint.
|
|
537
|
+
const lifetime =
|
|
538
|
+
record(raw) &&
|
|
539
|
+
integer(raw['expires_in']) &&
|
|
540
|
+
raw['expires_in'] > 0 &&
|
|
541
|
+
raw['expires_in'] <= config.maxAccessTokenSeconds
|
|
542
|
+
? raw['expires_in']
|
|
543
|
+
: config.maxAccessTokenSeconds;
|
|
544
|
+
const retirement = {
|
|
545
|
+
...(record(raw) && visible(raw['access_token'], TOKEN_BYTES)
|
|
546
|
+
? { accessToken: raw['access_token'] }
|
|
547
|
+
: {}),
|
|
548
|
+
...(record(raw) && visible(raw['refresh_token'], TOKEN_BYTES)
|
|
549
|
+
? { refreshToken: raw['refresh_token'] }
|
|
550
|
+
: {}),
|
|
551
|
+
expiresAtMs: tokenRequestedAtMs + lifetime * 1000,
|
|
552
|
+
};
|
|
553
|
+
try {
|
|
554
|
+
await onTokenPairIssued(Object.freeze(retirement));
|
|
555
|
+
} catch {
|
|
556
|
+
throw new OidcUnavailableError('persistence');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (
|
|
560
|
+
!record(raw) ||
|
|
561
|
+
!visible(raw['access_token'], TOKEN_BYTES) ||
|
|
562
|
+
!visible(raw['refresh_token'], TOKEN_BYTES) ||
|
|
563
|
+
!visible(raw['id_token'], TOKEN_BYTES) ||
|
|
564
|
+
!integer(raw['expires_in']) ||
|
|
565
|
+
raw['expires_in'] < 1 ||
|
|
566
|
+
raw['expires_in'] > config.maxAccessTokenSeconds ||
|
|
567
|
+
typeof raw['scope'] !== 'string' ||
|
|
568
|
+
raw['scope'].length > 8192 ||
|
|
569
|
+
raw['token_type'] !== 'Bearer'
|
|
570
|
+
) {
|
|
571
|
+
throw new OidcInvalidGrantError('token_response');
|
|
572
|
+
}
|
|
573
|
+
const scopes = raw['scope'].split(' ');
|
|
574
|
+
if (
|
|
575
|
+
scopes.length !== config.scopes.length ||
|
|
576
|
+
new Set(scopes).size !== scopes.length ||
|
|
577
|
+
!scopes.every((scope) => config.scopes.includes(scope))
|
|
578
|
+
)
|
|
579
|
+
throw new OidcInvalidGrantError('token_response');
|
|
580
|
+
const expiresAtMs = tokenRequestedAtMs + raw['expires_in'] * 1000;
|
|
581
|
+
const downloaded = await request(endpoints.jwks, signal);
|
|
582
|
+
if (downloaded.response.status !== 200) throw new OidcUnavailableError('http');
|
|
583
|
+
const keys = jwks(parseJson(downloaded.text));
|
|
584
|
+
let payload: JWTPayload;
|
|
585
|
+
try {
|
|
586
|
+
const verified = await jwtVerify(raw['id_token'], createLocalJWKSet(keys), {
|
|
587
|
+
issuer: config.issuer,
|
|
588
|
+
audience: config.clientId,
|
|
589
|
+
algorithms: ['RS256'],
|
|
590
|
+
currentDate: new Date(now()),
|
|
591
|
+
clockTolerance: config.clockToleranceSeconds,
|
|
592
|
+
maxTokenAge: LOGIN_SECONDS,
|
|
593
|
+
requiredClaims: ['sub', 'exp', 'iat', 'nonce', 'auth_time'],
|
|
594
|
+
});
|
|
595
|
+
if (!visible(verified.protectedHeader.kid, 128))
|
|
596
|
+
throw new OidcInvalidGrantError('id_token');
|
|
597
|
+
payload = verified.payload;
|
|
598
|
+
} catch {
|
|
599
|
+
throw new OidcInvalidGrantError('id_token');
|
|
600
|
+
}
|
|
601
|
+
const claims = validateClaims(payload, pending, raw['access_token'], code);
|
|
602
|
+
if (signal.aborted || expiresAtMs <= now()) throw new OidcUnavailableError('timeout');
|
|
603
|
+
return Object.freeze({
|
|
604
|
+
accessToken: raw['access_token'],
|
|
605
|
+
refreshToken: raw['refresh_token'],
|
|
606
|
+
idToken: raw['id_token'],
|
|
607
|
+
expiresAtMs,
|
|
608
|
+
scopes: Object.freeze(scopes),
|
|
609
|
+
...claims,
|
|
610
|
+
issuer: config.issuer,
|
|
611
|
+
clientId: config.clientId,
|
|
612
|
+
});
|
|
613
|
+
} catch (error) {
|
|
614
|
+
// Rejected minted credentials never enter the app store. Cleanup has the same deadline.
|
|
615
|
+
if (!signal.aborted && minted.length)
|
|
616
|
+
await revokeKnownTokens(signal, minted).catch(() => {});
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
},
|
|
621
|
+
async revokeTokenPair(
|
|
622
|
+
pair: Readonly<{ accessToken?: string; refreshToken?: string }>,
|
|
623
|
+
): Promise<void> {
|
|
624
|
+
const tokens: [string, string][] = [];
|
|
625
|
+
if (visible(pair.accessToken, TOKEN_BYTES)) tokens.push(['access_token', pair.accessToken]);
|
|
626
|
+
if (visible(pair.refreshToken, TOKEN_BYTES))
|
|
627
|
+
tokens.push(['refresh_token', pair.refreshToken]);
|
|
628
|
+
if (!tokens.length) throw new OidcInvalidGrantError('token_response');
|
|
629
|
+
await operation(config.timeoutMs, async (signal) => {
|
|
630
|
+
await discover(signal);
|
|
631
|
+
await revokeKnownTokens(signal, tokens);
|
|
632
|
+
});
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|