@kaapi/oauth2-auth-design 0.0.35 → 0.0.36
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/lib/cli.d.ts +2 -13
- package/lib/cli.js +2 -177
- package/lib/cli.js.map +1 -1
- package/lib/flows/auth-code/authorization-route.js +4 -2
- package/lib/flows/auth-code/authorization-route.js.map +1 -1
- package/lib/flows/authorization-code.d.ts +5 -1
- package/lib/flows/authorization-code.js +16 -9
- package/lib/flows/authorization-code.js.map +1 -1
- package/lib/flows/client-credentials.d.ts +5 -1
- package/lib/flows/client-credentials.js +13 -6
- package/lib/flows/client-credentials.js.map +1 -1
- package/lib/flows/common.d.ts +9 -1
- package/lib/flows/common.js +15 -2
- package/lib/flows/common.js.map +1 -1
- package/lib/flows/device-authorization.d.ts +5 -1
- package/lib/flows/device-authorization.js +16 -9
- package/lib/flows/device-authorization.js.map +1 -1
- package/lib/flows/oidc-multiple-flows.d.ts +6 -1
- package/lib/flows/oidc-multiple-flows.js +10 -1
- package/lib/flows/oidc-multiple-flows.js.map +1 -1
- package/lib/generators/oauth2-design-generator.d.ts +14 -0
- package/lib/generators/oauth2-design-generator.js +297 -0
- package/lib/generators/oauth2-design-generator.js.map +1 -0
- package/lib/generators/oauth2-flow-generator.d.ts +14 -0
- package/lib/generators/oauth2-flow-generator.js +606 -0
- package/lib/generators/oauth2-flow-generator.js.map +1 -0
- package/lib/generators/oauth2-util-generator.d.ts +14 -0
- package/lib/generators/oauth2-util-generator.js +240 -0
- package/lib/generators/oauth2-util-generator.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var _OAuth2FlowGenerator_instances, _OAuth2FlowGenerator_values, _OAuth2FlowGenerator_getOidcAuthCodeContent, _OAuth2FlowGenerator_getOidcClientCredentialsContent, _OAuth2FlowGenerator_getOidcDeviceAuthContent;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.OAuth2FlowGenerator = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const definitions_1 = require("@kaapi/cli/definitions");
|
|
7
|
+
const utils_1 = require("@kaapi/cli/utils");
|
|
8
|
+
var FLOW_ENUM;
|
|
9
|
+
(function (FLOW_ENUM) {
|
|
10
|
+
FLOW_ENUM["oidcAuthCode"] = "oidc-auth-code";
|
|
11
|
+
FLOW_ENUM["oidcClientCredentials"] = "oidc-client-credentials";
|
|
12
|
+
FLOW_ENUM["oidcDeviceAuth"] = "oidc-device-auth";
|
|
13
|
+
})(FLOW_ENUM || (FLOW_ENUM = {}));
|
|
14
|
+
const FLOW_OPTIONS = [
|
|
15
|
+
{
|
|
16
|
+
value: FLOW_ENUM.oidcAuthCode,
|
|
17
|
+
label: 'OIDC Authorization Code',
|
|
18
|
+
hint: ''
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: FLOW_ENUM.oidcClientCredentials,
|
|
22
|
+
label: 'OIDC Client Credentials',
|
|
23
|
+
hint: ''
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
value: FLOW_ENUM.oidcDeviceAuth,
|
|
27
|
+
label: 'OIDC Device Authorization',
|
|
28
|
+
hint: ''
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
class OAuth2FlowGenerator {
|
|
32
|
+
constructor() {
|
|
33
|
+
_OAuth2FlowGenerator_instances.add(this);
|
|
34
|
+
_OAuth2FlowGenerator_values.set(this, {
|
|
35
|
+
name: '',
|
|
36
|
+
flow: ''
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
get type() {
|
|
40
|
+
return 'auth-design';
|
|
41
|
+
}
|
|
42
|
+
get name() {
|
|
43
|
+
return 'oauth2-flow';
|
|
44
|
+
}
|
|
45
|
+
get description() {
|
|
46
|
+
return 'Creates an auth design based on OAuth2 specifications.';
|
|
47
|
+
}
|
|
48
|
+
get notes() {
|
|
49
|
+
return [
|
|
50
|
+
'Allowed values for --flow:',
|
|
51
|
+
...FLOW_OPTIONS.map(o => ` - ${o.value}`)
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
get options() {
|
|
55
|
+
return {
|
|
56
|
+
name: 'The name of the strategy',
|
|
57
|
+
flow: 'The grant type flow'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
init(options) {
|
|
61
|
+
if (typeof options['name'] == 'string') {
|
|
62
|
+
tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").name = (0, utils_1.camelCase)(options['name']);
|
|
63
|
+
}
|
|
64
|
+
if (typeof options['flow'] == 'string') {
|
|
65
|
+
if (!FLOW_OPTIONS.map(v => v.value).includes(options['flow'])) {
|
|
66
|
+
throw new Error(`Invalid value for '--flow'. Allowed values are: ${FLOW_OPTIONS.map(v => v.value).join(', ')}.`);
|
|
67
|
+
}
|
|
68
|
+
tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").flow = options['flow'];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
isValid() {
|
|
72
|
+
return !!tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").name;
|
|
73
|
+
}
|
|
74
|
+
getFileContent() {
|
|
75
|
+
if (tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").flow === FLOW_ENUM.oidcAuthCode)
|
|
76
|
+
return tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_instances, "m", _OAuth2FlowGenerator_getOidcAuthCodeContent).call(this);
|
|
77
|
+
else if (tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").flow === FLOW_ENUM.oidcClientCredentials)
|
|
78
|
+
return tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_instances, "m", _OAuth2FlowGenerator_getOidcClientCredentialsContent).call(this);
|
|
79
|
+
else if (tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").flow === FLOW_ENUM.oidcDeviceAuth)
|
|
80
|
+
return tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_instances, "m", _OAuth2FlowGenerator_getOidcDeviceAuthContent).call(this);
|
|
81
|
+
else
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
getQuestions() {
|
|
85
|
+
const r = [];
|
|
86
|
+
if (!tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").name) {
|
|
87
|
+
r.push({
|
|
88
|
+
type: definitions_1.QuestionType.text,
|
|
89
|
+
options: {
|
|
90
|
+
message: 'The name of the strategy?',
|
|
91
|
+
defaultValue: 'oauth2-strategy',
|
|
92
|
+
placeholder: 'oauth2-strategy'
|
|
93
|
+
},
|
|
94
|
+
setValue: (value) => {
|
|
95
|
+
tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").name = (0, utils_1.camelCase)(value);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!(tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").flow && FLOW_OPTIONS.map(v => v.value).includes(tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").flow))) {
|
|
100
|
+
r.push({
|
|
101
|
+
type: definitions_1.QuestionType.select,
|
|
102
|
+
options: {
|
|
103
|
+
message: 'The authorization flow?',
|
|
104
|
+
options: FLOW_OPTIONS,
|
|
105
|
+
initialValue: 'oidc-auth-code'
|
|
106
|
+
},
|
|
107
|
+
setValue: (value) => {
|
|
108
|
+
tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").flow = `${value}`;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return r;
|
|
113
|
+
}
|
|
114
|
+
getFilename() {
|
|
115
|
+
return (0, utils_1.kebabCase)(`${tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").name}`) + '.ts';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.OAuth2FlowGenerator = OAuth2FlowGenerator;
|
|
119
|
+
_OAuth2FlowGenerator_values = new WeakMap(), _OAuth2FlowGenerator_instances = new WeakSet(), _OAuth2FlowGenerator_getOidcAuthCodeContent = function _OAuth2FlowGenerator_getOidcAuthCodeContent() {
|
|
120
|
+
return `// generated by @kaapi/oauth2-auth-design
|
|
121
|
+
|
|
122
|
+
import {
|
|
123
|
+
BearerToken,
|
|
124
|
+
OIDCAuthorizationCodeBuilder,
|
|
125
|
+
createInMemoryKeyStore,
|
|
126
|
+
createMatchAuthCodeResult,
|
|
127
|
+
OAuth2TokenResponse,
|
|
128
|
+
ClientSecretBasic,
|
|
129
|
+
ClientSecretPost,
|
|
130
|
+
NoneAuthMethod,
|
|
131
|
+
OAuth2ErrorCode,
|
|
132
|
+
} from '@kaapi/oauth2-auth-design';
|
|
133
|
+
|
|
134
|
+
const VALID_CLIENTS = [
|
|
135
|
+
{
|
|
136
|
+
client_id: 'service-api-client',
|
|
137
|
+
client_secret: 's3cr3tK3y123!',
|
|
138
|
+
allowed_scopes: ['openid', 'profile', 'email', 'read', 'write'],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const REGISTERED_USERS = [{ id: 'user-1234', username: 'user', password: 'password', email: 'user@email.com' }];
|
|
143
|
+
|
|
144
|
+
const authCodesStore: Map<
|
|
145
|
+
string,
|
|
146
|
+
{ clientId: string; scopes: string[]; userId: string; codeChallenge?: string | undefined }
|
|
147
|
+
> = new Map();
|
|
148
|
+
|
|
149
|
+
export const oidcAuthCodeBuilder = OIDCAuthorizationCodeBuilder.create()
|
|
150
|
+
// the name of the strategy
|
|
151
|
+
.strategyName('${(0, utils_1.kebabCase)(tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").name)}')
|
|
152
|
+
// access token TTL (used in generateToken controller)
|
|
153
|
+
.setTokenTtl(3600)
|
|
154
|
+
// activate auto parsing of access token (jwtAccessTokenPayload + createJwtAccessToken)
|
|
155
|
+
.useAccessTokenJwks(true)
|
|
156
|
+
// Client authentication methods
|
|
157
|
+
.addClientAuthenticationMethod(new ClientSecretBasic())
|
|
158
|
+
.addClientAuthenticationMethod(new ClientSecretPost())
|
|
159
|
+
.addClientAuthenticationMethod(new NoneAuthMethod())
|
|
160
|
+
.setTokenType(new BearerToken())
|
|
161
|
+
// Define scopes
|
|
162
|
+
.setScopes({
|
|
163
|
+
profile: 'Access to basic profile information such as name and picture.',
|
|
164
|
+
email: "Access to the user's email address and its verification status.",
|
|
165
|
+
read: 'Grants read-only access to protected resources',
|
|
166
|
+
write: 'Grants write access to protected resources',
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Authorization
|
|
170
|
+
.authorizationRoute<object, { Payload: { username?: string; password?: string } }>((route) =>
|
|
171
|
+
route
|
|
172
|
+
.setPath('/oauth2/v1.0/authorize')
|
|
173
|
+
.setUsernameField('username')
|
|
174
|
+
.setPasswordField('password')
|
|
175
|
+
.generateCode(async ({ clientId, codeChallenge, scope }, req, _h) => {
|
|
176
|
+
// client exists?
|
|
177
|
+
const client = VALID_CLIENTS.find((c) => c.client_id === clientId);
|
|
178
|
+
if (!client) return null;
|
|
179
|
+
|
|
180
|
+
// filter client's allowed scoped
|
|
181
|
+
const requestedScopes = (scope ?? '').split(/\\s+/).filter(Boolean);
|
|
182
|
+
const grantedScopes = requestedScopes.length
|
|
183
|
+
? requestedScopes.filter((s) => client.allowed_scopes.includes(s))
|
|
184
|
+
: client.allowed_scopes;
|
|
185
|
+
if (grantedScopes.length === 0) return null;
|
|
186
|
+
|
|
187
|
+
// user exists?
|
|
188
|
+
const user = REGISTERED_USERS.find(
|
|
189
|
+
(u) => u.username === req.payload.username && u.password === req.payload.password
|
|
190
|
+
);
|
|
191
|
+
if (!user) return null;
|
|
192
|
+
|
|
193
|
+
// generate code
|
|
194
|
+
const code = \`auth-${Date.now()}\`;
|
|
195
|
+
authCodesStore.set(code, { clientId, scopes: grantedScopes, userId: user.id, codeChallenge });
|
|
196
|
+
return { type: 'code', value: code };
|
|
197
|
+
})
|
|
198
|
+
.finalizeAuthorization(async (ctx, _params, _req, h) => {
|
|
199
|
+
// redirect to callback url (could do something else depending on the code result)
|
|
200
|
+
const matcher = createMatchAuthCodeResult({
|
|
201
|
+
code: async () => h.redirect(ctx.fullRedirectUri),
|
|
202
|
+
continue: async () => h.redirect(ctx.fullRedirectUri),
|
|
203
|
+
deny: async () => h.redirect(ctx.fullRedirectUri),
|
|
204
|
+
});
|
|
205
|
+
return matcher(ctx.authorizationResult);
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
// Token exchange
|
|
210
|
+
.tokenRoute((route) =>
|
|
211
|
+
route.generateToken(
|
|
212
|
+
async ({
|
|
213
|
+
clientId,
|
|
214
|
+
ttl,
|
|
215
|
+
tokenType,
|
|
216
|
+
code,
|
|
217
|
+
clientSecret,
|
|
218
|
+
codeVerifier,
|
|
219
|
+
createJwtAccessToken,
|
|
220
|
+
createIdToken,
|
|
221
|
+
verifyCodeVerifier,
|
|
222
|
+
}) => {
|
|
223
|
+
const entry = authCodesStore.get(code);
|
|
224
|
+
if (!entry || entry.clientId !== clientId) return null;
|
|
225
|
+
|
|
226
|
+
const client = VALID_CLIENTS.find((c) => c.client_id === clientId);
|
|
227
|
+
if (!client) {
|
|
228
|
+
return {
|
|
229
|
+
error: OAuth2ErrorCode.INVALID_CLIENT,
|
|
230
|
+
error_description: 'Client authentication failed.',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (entry.codeChallenge && codeVerifier) {
|
|
235
|
+
if (!verifyCodeVerifier(codeVerifier, entry.codeChallenge)) {
|
|
236
|
+
return {
|
|
237
|
+
error: OAuth2ErrorCode.INVALID_GRANT,
|
|
238
|
+
error_description: 'Invalid authorization grant.',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
} else if (clientSecret) {
|
|
242
|
+
if (client.client_secret !== clientSecret) {
|
|
243
|
+
return {
|
|
244
|
+
error: OAuth2ErrorCode.INVALID_CLIENT,
|
|
245
|
+
error_description: 'Client authentication failed.',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
return {
|
|
250
|
+
error: OAuth2ErrorCode.INVALID_REQUEST,
|
|
251
|
+
error_description: 'Missing or invalid request parameter.',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const user = REGISTERED_USERS.find((u) => u.id === entry.userId);
|
|
256
|
+
if (!user) {
|
|
257
|
+
return {
|
|
258
|
+
error: OAuth2ErrorCode.INVALID_GRANT,
|
|
259
|
+
error_description: 'Invalid authorization grant.',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Generate a signed JWT access token
|
|
264
|
+
const { token: accessToken } = await createJwtAccessToken!({
|
|
265
|
+
sub: entry.userId,
|
|
266
|
+
client_id: clientId,
|
|
267
|
+
scope: entry.scopes,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Generate a signed JWT id token
|
|
271
|
+
const idToken = entry.scopes.includes('openid')
|
|
272
|
+
? (await createIdToken!({ sub: entry.userId, name: user.username, aud: clientId })).token
|
|
273
|
+
: undefined;
|
|
274
|
+
|
|
275
|
+
// Return token response
|
|
276
|
+
return new OAuth2TokenResponse({ access_token: accessToken })
|
|
277
|
+
.setExpiresIn(ttl)
|
|
278
|
+
.setTokenType(tokenType)
|
|
279
|
+
.setScope(entry.scopes)
|
|
280
|
+
.setIdToken(idToken);
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
// Access Token Validation
|
|
286
|
+
.validate(async (_req, { jwtAccessTokenPayload }) => {
|
|
287
|
+
if (!jwtAccessTokenPayload?.sub) return { isValid: false };
|
|
288
|
+
return {
|
|
289
|
+
isValid: true,
|
|
290
|
+
credentials: {
|
|
291
|
+
user: {
|
|
292
|
+
id: jwtAccessTokenPayload.sub,
|
|
293
|
+
clientId: jwtAccessTokenPayload.client_id,
|
|
294
|
+
},
|
|
295
|
+
scope: Array.isArray(jwtAccessTokenPayload.scope) ? jwtAccessTokenPayload.scope : [],
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// JWKS
|
|
301
|
+
.jwksRoute((route) => route.setPath('/discovery/v1.0/keys')) // activates jwks uri
|
|
302
|
+
.setPublicKeyExpiry(86400) // 24h
|
|
303
|
+
.setJwksKeyStore(createInMemoryKeyStore()) // store for JWKS
|
|
304
|
+
.setJwksRotatorOptions({
|
|
305
|
+
intervalMs: 7.884e9, // 91 days
|
|
306
|
+
timestampStore: createInMemoryKeyStore(),
|
|
307
|
+
});
|
|
308
|
+
`;
|
|
309
|
+
}, _OAuth2FlowGenerator_getOidcClientCredentialsContent = function _OAuth2FlowGenerator_getOidcClientCredentialsContent() {
|
|
310
|
+
return `// generated by @kaapi/oauth2-auth-design
|
|
311
|
+
|
|
312
|
+
import {
|
|
313
|
+
OIDCClientCredentialsBuilder,
|
|
314
|
+
OAuth2TokenResponse,
|
|
315
|
+
OAuth2ErrorCode,
|
|
316
|
+
ClientSecretBasic,
|
|
317
|
+
ClientSecretPost,
|
|
318
|
+
BearerToken,
|
|
319
|
+
createInMemoryKeyStore,
|
|
320
|
+
} from '@kaapi/oauth2-auth-design';
|
|
321
|
+
|
|
322
|
+
const VALID_CLIENTS = [
|
|
323
|
+
{
|
|
324
|
+
client_id: 'internal-service',
|
|
325
|
+
client_secret: 'Int3rnalK3y!',
|
|
326
|
+
allowed_scopes: ['read', 'write', 'admin'],
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
export const oidcClientCredentialsBuilder = OIDCClientCredentialsBuilder.create()
|
|
331
|
+
// the name of the strategy
|
|
332
|
+
.strategyName('${(0, utils_1.kebabCase)(tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").name)}')
|
|
333
|
+
// access token ttl (used in generateToken controller)
|
|
334
|
+
.setTokenTtl(600)
|
|
335
|
+
// activate auto parsing of access token (jwtAccessTokenPayload + createJwtAccessToken)
|
|
336
|
+
.useAccessTokenJwks(true)
|
|
337
|
+
// Client authentication methods
|
|
338
|
+
.addClientAuthenticationMethod(new ClientSecretBasic())
|
|
339
|
+
.addClientAuthenticationMethod(new ClientSecretPost())
|
|
340
|
+
.setTokenType(new BearerToken())
|
|
341
|
+
// Define available scopes
|
|
342
|
+
.setScopes({
|
|
343
|
+
read: 'Grants read-only access to protected resources',
|
|
344
|
+
write: 'Grants write access to protected resources',
|
|
345
|
+
admin: 'Administrative access',
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// Token exchange
|
|
349
|
+
.tokenRoute((route) =>
|
|
350
|
+
route.generateToken(async ({ clientId, clientSecret, ttl, tokenType, scope, createJwtAccessToken }) => {
|
|
351
|
+
// Validate client credentials
|
|
352
|
+
const client = VALID_CLIENTS.find((c) => c.client_id === clientId && c.client_secret === clientSecret);
|
|
353
|
+
|
|
354
|
+
if (!client) {
|
|
355
|
+
return {
|
|
356
|
+
error: OAuth2ErrorCode.INVALID_CLIENT,
|
|
357
|
+
error_description: 'Invalid client_id or client_secret',
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Determine requested scopes
|
|
362
|
+
const requestedScopes = (scope ?? '').split(/\\s+/).filter(Boolean);
|
|
363
|
+
|
|
364
|
+
// Compute granted scopes
|
|
365
|
+
let grantedScopes = client.allowed_scopes;
|
|
366
|
+
if (requestedScopes.length > 0) {
|
|
367
|
+
grantedScopes = requestedScopes.filter((s) => client.allowed_scopes.includes(s));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (grantedScopes.length === 0) {
|
|
371
|
+
return {
|
|
372
|
+
error: OAuth2ErrorCode.INVALID_SCOPE,
|
|
373
|
+
error_description: 'No valid scopes granted for this client',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Generate a signed JWT access token
|
|
378
|
+
const { token: accessToken } = await createJwtAccessToken!({
|
|
379
|
+
sub: clientId,
|
|
380
|
+
app: true,
|
|
381
|
+
scope: grantedScopes,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Return token response
|
|
385
|
+
return new OAuth2TokenResponse({ access_token: accessToken })
|
|
386
|
+
.setExpiresIn(ttl)
|
|
387
|
+
.setTokenType(tokenType)
|
|
388
|
+
.setScope(grantedScopes.join(' '));
|
|
389
|
+
})
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
// Access Token Validation
|
|
393
|
+
.validate(async (_req, { jwtAccessTokenPayload }) => {
|
|
394
|
+
if (!jwtAccessTokenPayload?.sub || !jwtAccessTokenPayload.app) return { isValid: false };
|
|
395
|
+
return {
|
|
396
|
+
isValid: true,
|
|
397
|
+
credentials: {
|
|
398
|
+
app: {
|
|
399
|
+
id: jwtAccessTokenPayload.sub,
|
|
400
|
+
},
|
|
401
|
+
scope: Array.isArray(jwtAccessTokenPayload.scope) ? jwtAccessTokenPayload.scope : [],
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
// JWKS
|
|
407
|
+
.jwksRoute((route) => route.setPath('/discovery/v1.0/keys')) // activates jwks uri
|
|
408
|
+
.setPublicKeyExpiry(86400) // 24h
|
|
409
|
+
.setJwksKeyStore(createInMemoryKeyStore()) // store for JWKS
|
|
410
|
+
.setJwksRotatorOptions({
|
|
411
|
+
intervalMs: 7.884e9, // 91 days
|
|
412
|
+
timestampStore: createInMemoryKeyStore(),
|
|
413
|
+
});
|
|
414
|
+
`;
|
|
415
|
+
}, _OAuth2FlowGenerator_getOidcDeviceAuthContent = function _OAuth2FlowGenerator_getOidcDeviceAuthContent() {
|
|
416
|
+
return `// generated by @kaapi/oauth2-auth-design
|
|
417
|
+
|
|
418
|
+
import {
|
|
419
|
+
BearerToken,
|
|
420
|
+
OIDCAuthorizationCodeBuilder,
|
|
421
|
+
createInMemoryKeyStore,
|
|
422
|
+
createMatchAuthCodeResult,
|
|
423
|
+
OAuth2TokenResponse,
|
|
424
|
+
ClientSecretBasic,
|
|
425
|
+
ClientSecretPost,
|
|
426
|
+
NoneAuthMethod,
|
|
427
|
+
OAuth2ErrorCode,
|
|
428
|
+
} from '@kaapi/oauth2-auth-design';
|
|
429
|
+
|
|
430
|
+
const VALID_CLIENTS = [
|
|
431
|
+
{
|
|
432
|
+
client_id: 'service-api-client',
|
|
433
|
+
client_secret: 's3cr3tK3y123!',
|
|
434
|
+
allowed_scopes: ['openid', 'profile', 'email', 'read', 'write'],
|
|
435
|
+
},
|
|
436
|
+
];
|
|
437
|
+
|
|
438
|
+
const REGISTERED_USERS = [{ id: 'user-1234', username: 'user', password: 'password', email: 'user@email.com' }];
|
|
439
|
+
|
|
440
|
+
const authCodesStore: Map<
|
|
441
|
+
string,
|
|
442
|
+
{ clientId: string; scopes: string[]; userId: string; codeChallenge?: string | undefined }
|
|
443
|
+
> = new Map();
|
|
444
|
+
|
|
445
|
+
export const oidcAuthCodeBuilder = OIDCAuthorizationCodeBuilder.create()
|
|
446
|
+
// the name of the strategy
|
|
447
|
+
.strategyName('${(0, utils_1.kebabCase)(tslib_1.__classPrivateFieldGet(this, _OAuth2FlowGenerator_values, "f").name)}')
|
|
448
|
+
// access token TTL (used in generateToken controller)
|
|
449
|
+
.setTokenTtl(3600)
|
|
450
|
+
// activate auto parsing of access token (jwtAccessTokenPayload + createJwtAccessToken)
|
|
451
|
+
.useAccessTokenJwks(true)
|
|
452
|
+
// Client authentication methods
|
|
453
|
+
.addClientAuthenticationMethod(new ClientSecretBasic())
|
|
454
|
+
.addClientAuthenticationMethod(new ClientSecretPost())
|
|
455
|
+
.addClientAuthenticationMethod(new NoneAuthMethod())
|
|
456
|
+
.setTokenType(new BearerToken())
|
|
457
|
+
// Define scopes
|
|
458
|
+
.setScopes({
|
|
459
|
+
profile: 'Access to basic profile information such as name and picture.',
|
|
460
|
+
email: "Access to the user's email address and its verification status.",
|
|
461
|
+
read: 'Grants read-only access to protected resources',
|
|
462
|
+
write: 'Grants write access to protected resources',
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
// Authorization
|
|
466
|
+
.authorizationRoute<object, { Payload: { username?: string; password?: string } }>((route) =>
|
|
467
|
+
route
|
|
468
|
+
.setPath('/oauth2/v1.0/authorize')
|
|
469
|
+
.setUsernameField('username')
|
|
470
|
+
.setPasswordField('password')
|
|
471
|
+
.generateCode(async ({ clientId, codeChallenge, scope }, req, _h) => {
|
|
472
|
+
// client exists?
|
|
473
|
+
const client = VALID_CLIENTS.find((c) => c.client_id === clientId);
|
|
474
|
+
if (!client) return null;
|
|
475
|
+
|
|
476
|
+
// filter client's allowed scoped
|
|
477
|
+
const requestedScopes = (scope ?? '').split(/\\s+/).filter(Boolean);
|
|
478
|
+
const grantedScopes = requestedScopes.length
|
|
479
|
+
? requestedScopes.filter((s) => client.allowed_scopes.includes(s))
|
|
480
|
+
: client.allowed_scopes;
|
|
481
|
+
if (grantedScopes.length === 0) return null;
|
|
482
|
+
|
|
483
|
+
// user exists?
|
|
484
|
+
const user = REGISTERED_USERS.find(
|
|
485
|
+
(u) => u.username === req.payload.username && u.password === req.payload.password
|
|
486
|
+
);
|
|
487
|
+
if (!user) return null;
|
|
488
|
+
|
|
489
|
+
// generate code
|
|
490
|
+
const code = \`auth-${Date.now()}\`;
|
|
491
|
+
authCodesStore.set(code, { clientId, scopes: grantedScopes, userId: user.id, codeChallenge });
|
|
492
|
+
return { type: 'code', value: code };
|
|
493
|
+
})
|
|
494
|
+
.finalizeAuthorization(async (ctx, _params, _req, h) => {
|
|
495
|
+
// redirect to callback url (could do something else depending on the code result)
|
|
496
|
+
const matcher = createMatchAuthCodeResult({
|
|
497
|
+
code: async () => h.redirect(ctx.fullRedirectUri),
|
|
498
|
+
continue: async () => h.redirect(ctx.fullRedirectUri),
|
|
499
|
+
deny: async () => h.redirect(ctx.fullRedirectUri),
|
|
500
|
+
});
|
|
501
|
+
return matcher(ctx.authorizationResult);
|
|
502
|
+
})
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
// Token exchange
|
|
506
|
+
.tokenRoute((route) =>
|
|
507
|
+
route.generateToken(
|
|
508
|
+
async ({
|
|
509
|
+
clientId,
|
|
510
|
+
ttl,
|
|
511
|
+
tokenType,
|
|
512
|
+
code,
|
|
513
|
+
clientSecret,
|
|
514
|
+
codeVerifier,
|
|
515
|
+
createJwtAccessToken,
|
|
516
|
+
createIdToken,
|
|
517
|
+
verifyCodeVerifier,
|
|
518
|
+
}) => {
|
|
519
|
+
const entry = authCodesStore.get(code);
|
|
520
|
+
if (!entry || entry.clientId !== clientId) return null;
|
|
521
|
+
|
|
522
|
+
const client = VALID_CLIENTS.find((c) => c.client_id === clientId);
|
|
523
|
+
if (!client) {
|
|
524
|
+
return {
|
|
525
|
+
error: OAuth2ErrorCode.INVALID_CLIENT,
|
|
526
|
+
error_description: 'Client authentication failed.',
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (entry.codeChallenge && codeVerifier) {
|
|
531
|
+
if (!verifyCodeVerifier(codeVerifier, entry.codeChallenge)) {
|
|
532
|
+
return {
|
|
533
|
+
error: OAuth2ErrorCode.INVALID_GRANT,
|
|
534
|
+
error_description: 'Invalid authorization grant.',
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
} else if (clientSecret) {
|
|
538
|
+
if (client.client_secret !== clientSecret) {
|
|
539
|
+
return {
|
|
540
|
+
error: OAuth2ErrorCode.INVALID_CLIENT,
|
|
541
|
+
error_description: 'Client authentication failed.',
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
return {
|
|
546
|
+
error: OAuth2ErrorCode.INVALID_REQUEST,
|
|
547
|
+
error_description: 'Missing or invalid request parameter.',
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const user = REGISTERED_USERS.find((u) => u.id === entry.userId);
|
|
552
|
+
if (!user) {
|
|
553
|
+
return {
|
|
554
|
+
error: OAuth2ErrorCode.INVALID_GRANT,
|
|
555
|
+
error_description: 'Invalid authorization grant.',
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Generate a signed JWT access token
|
|
560
|
+
const { token: accessToken } = await createJwtAccessToken!({
|
|
561
|
+
sub: entry.userId,
|
|
562
|
+
client_id: clientId,
|
|
563
|
+
scope: entry.scopes,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Generate a signed JWT id token
|
|
567
|
+
const idToken = entry.scopes.includes('openid')
|
|
568
|
+
? (await createIdToken!({ sub: entry.userId, name: user.username, aud: clientId })).token
|
|
569
|
+
: undefined;
|
|
570
|
+
|
|
571
|
+
// Return token response
|
|
572
|
+
return new OAuth2TokenResponse({ access_token: accessToken })
|
|
573
|
+
.setExpiresIn(ttl)
|
|
574
|
+
.setTokenType(tokenType)
|
|
575
|
+
.setScope(entry.scopes)
|
|
576
|
+
.setIdToken(idToken);
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
// Access Token Validation
|
|
582
|
+
.validate(async (_req, { jwtAccessTokenPayload }) => {
|
|
583
|
+
if (!jwtAccessTokenPayload?.sub) return { isValid: false };
|
|
584
|
+
return {
|
|
585
|
+
isValid: true,
|
|
586
|
+
credentials: {
|
|
587
|
+
user: {
|
|
588
|
+
id: jwtAccessTokenPayload.sub,
|
|
589
|
+
clientId: jwtAccessTokenPayload.client_id,
|
|
590
|
+
},
|
|
591
|
+
scope: Array.isArray(jwtAccessTokenPayload.scope) ? jwtAccessTokenPayload.scope : [],
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
// JWKS
|
|
597
|
+
.jwksRoute((route) => route.setPath('/discovery/v1.0/keys')) // activates jwks uri
|
|
598
|
+
.setPublicKeyExpiry(86400) // 24h
|
|
599
|
+
.setJwksKeyStore(createInMemoryKeyStore()) // store for JWKS
|
|
600
|
+
.setJwksRotatorOptions({
|
|
601
|
+
intervalMs: 7.884e9, // 91 days
|
|
602
|
+
timestampStore: createInMemoryKeyStore(),
|
|
603
|
+
});
|
|
604
|
+
`;
|
|
605
|
+
};
|
|
606
|
+
//# sourceMappingURL=oauth2-flow-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth2-flow-generator.js","sourceRoot":"","sources":["../../src/generators/oauth2-flow-generator.ts"],"names":[],"mappings":";;;;;AAAA,wDAAiG;AACjG,4CAAuD;AAEvD,IAAK,SAIJ;AAJD,WAAK,SAAS;IACV,4CAA+B,CAAA;IAC/B,8DAAiD,CAAA;IACjD,gDAAmC,CAAA;AACvC,CAAC,EAJI,SAAS,KAAT,SAAS,QAIb;AAED,MAAM,YAAY,GAIZ;IACE;QACI,KAAK,EAAE,SAAS,CAAC,YAAY;QAC7B,KAAK,EAAE,yBAAyB;QAChC,IAAI,EAAE,EAAE;KACX;IACD;QACI,KAAK,EAAE,SAAS,CAAC,qBAAqB;QACtC,KAAK,EAAE,yBAAyB;QAChC,IAAI,EAAE,EAAE;KACX;IACD;QACI,KAAK,EAAE,SAAS,CAAC,cAAc;QAC/B,KAAK,EAAE,2BAA2B;QAClC,IAAI,EAAE,EAAE;KACX;CACJ,CAAC;AAEN,MAAa,mBAAmB;IAAhC;;QA4BI,sCAAU;YACN,IAAI,EAAE,EAAE;YACR,IAAI,EAAE,EAAE;SACX,EAAA;IA8iBL,CAAC;IA3kBG,IAAI,IAAI;QACJ,OAAO,aAAa,CAAA;IACxB,CAAC;IAED,IAAI,IAAI;QACJ,OAAO,aAAa,CAAA;IACxB,CAAC;IAED,IAAI,WAAW;QACX,OAAO,wDAAwD,CAAA;IACnE,CAAC;IAED,IAAI,KAAK;QACL,OAAO;YACH,4BAA4B;YAC5B,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC;SAC7C,CAAA;IACL,CAAC;IAED,IAAI,OAAO;QACP,OAAO;YACH,IAAI,EAAE,0BAA0B;YAChC,IAAI,EAAE,qBAAqB;SAC9B,CAAA;IACL,CAAC;IAOD,IAAI,CAAC,OAAgC;QACjC,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACrC,+BAAA,IAAI,mCAAQ,CAAC,IAAI,GAAG,IAAA,iBAAS,EAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;QAClD,CAAC;QACD,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACrC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;gBAC5D,MAAM,IAAI,KAAK,CAAC,mDAAmD,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACpH,CAAC;YACD,+BAAA,IAAI,mCAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QACvC,CAAC;IACL,CAAC;IAED,OAAO;QACH,OAAO,CAAC,CAAC,+BAAA,IAAI,mCAAQ,CAAC,IAAI,CAAA;IAC9B,CAAC;IAED,cAAc;QACV,IAAI,+BAAA,IAAI,mCAAQ,CAAC,IAAI,KAAK,SAAS,CAAC,YAAY;YAC5C,OAAO,+BAAA,IAAI,mFAAwB,MAA5B,IAAI,CAA0B,CAAA;aACpC,IAAI,+BAAA,IAAI,mCAAQ,CAAC,IAAI,KAAK,SAAS,CAAC,qBAAqB;YAC1D,OAAO,+BAAA,IAAI,4FAAiC,MAArC,IAAI,CAAmC,CAAA;aAC7C,IAAI,+BAAA,IAAI,mCAAQ,CAAC,IAAI,KAAK,SAAS,CAAC,cAAc;YACnD,OAAO,+BAAA,IAAI,qFAA0B,MAA9B,IAAI,CAA4B,CAAA;;YAEvC,OAAO,EAAE,CAAA;IACjB,CAAC;IAED,YAAY;QACR,MAAM,CAAC,GAAe,EAAE,CAAA;QAExB,IAAI,CAAC,+BAAA,IAAI,mCAAQ,CAAC,IAAI,EAAE,CAAC;YACrB,CAAC,CAAC,IAAI,CAAC;gBACH,IAAI,EAAE,0BAAY,CAAC,IAAI;gBACvB,OAAO,EAAE;oBACL,OAAO,EAAE,2BAA2B;oBACpC,YAAY,EAAE,iBAAiB;oBAC/B,WAAW,EAAE,iBAAiB;iBACjC;gBACD,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;oBAChB,+BAAA,IAAI,mCAAQ,CAAC,IAAI,GAAG,IAAA,iBAAS,EAAC,KAAK,CAAC,CAAA;gBACxC,CAAC;aACJ,CAAC,CAAA;QACN,CAAC;QAED,IAAI,CAAC,CAAC,+BAAA,IAAI,mCAAQ,CAAC,IAAI,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,+BAAA,IAAI,mCAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACrF,CAAC,CAAC,IAAI,CAAC;gBACH,IAAI,EAAE,0BAAY,CAAC,MAAM;gBACzB,OAAO,EAAE;oBACL,OAAO,EAAE,yBAAyB;oBAClC,OAAO,EAAE,YAAY;oBACrB,YAAY,EAAE,gBAAgB;iBACjC;gBACD,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;oBAChB,+BAAA,IAAI,mCAAQ,CAAC,IAAI,GAAG,GAAG,KAAK,EAAE,CAAA;gBAClC,CAAC;aACJ,CAAC,CAAA;QACN,CAAC;QAED,OAAO,CAAC,CAAA;IACZ,CAAC;IAED,WAAW;QACP,OAAO,IAAA,iBAAS,EAAC,GAAG,+BAAA,IAAI,mCAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAA;IACpD,CAAC;CA6eJ;AA7kBD,kDA6kBC;;IA1eO,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA+BM,IAAA,iBAAS,EAAC,+BAAA,IAAI,mCAAQ,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCA2CX,IAAI,CAAC,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkH/C,CAAA;AACG,CAAC;IAGG,OAAO;;;;;;;;;;;;;;;;;;;;;;qBAsBM,IAAA,iBAAS,EAAC,+BAAA,IAAI,mCAAQ,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkFhD,CAAA;AACG,CAAC;IAGG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA+BM,IAAA,iBAAS,EAAC,+BAAA,IAAI,mCAAQ,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCA2CX,IAAI,CAAC,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkH/C,CAAA;AACG,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FileGenerator, FileGeneratorType, Question } from '@kaapi/cli/definitions';
|
|
2
|
+
export declare class OAuth2UtilGenerator implements FileGenerator {
|
|
3
|
+
#private;
|
|
4
|
+
get type(): FileGeneratorType;
|
|
5
|
+
get name(): 'oauth2-util';
|
|
6
|
+
get description(): string;
|
|
7
|
+
get notes(): string[];
|
|
8
|
+
get options(): Record<string, string>;
|
|
9
|
+
init(options: Record<string, unknown>): void;
|
|
10
|
+
isValid(): boolean;
|
|
11
|
+
getFileContent(): string;
|
|
12
|
+
getQuestions(): Question[];
|
|
13
|
+
getFilename(): string;
|
|
14
|
+
}
|