@adonis-agora/authkit-client 0.18.8 → 0.19.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/build/index.d.ts +9 -3
- package/build/index.js +5 -2
- package/build/providers/authkit_client_provider.d.ts +63 -8
- package/build/providers/authkit_client_provider.js +209 -37
- package/build/src/authorization.d.ts +125 -0
- package/build/src/authorization.js +142 -0
- package/build/src/backchannel_logout.d.ts +1 -1
- package/build/src/backchannel_logout.js +3 -15
- package/build/src/define_config.d.ts +24 -0
- package/build/src/define_config.js +6 -0
- package/build/src/discovery.d.ts +52 -20
- package/build/src/discovery.js +104 -42
- package/build/src/id_token.d.ts +57 -0
- package/build/src/id_token.js +61 -0
- package/build/src/jwks.d.ts +15 -0
- package/build/src/jwks.js +36 -0
- package/build/src/oidc_login.d.ts +7 -1
- package/build/src/oidc_login.js +31 -2
- package/build/src/register_oidc_client.js +30 -31
- package/build/src/resolvers/factory.d.ts +9 -2
- package/build/src/resolvers/factory.js +37 -5
- package/build/src/resolvers/jwt_resolver.d.ts +7 -3
- package/build/src/resolvers/jwt_resolver.js +8 -17
- package/build/src/resolvers/opaque_resolver.d.ts +5 -2
- package/build/src/resolvers/opaque_resolver.js +4 -1
- package/build/src/verify_access_token.d.ts +0 -2
- package/build/src/verify_access_token.js +2 -27
- package/build/stubs/controllers/oidc_session_controller.stub +31 -26
- package/package.json +7 -6
- package/stubs/controllers/oidc_session_controller.stub +31 -26
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRemoteJWKSet } from 'jose';
|
|
2
|
+
/**
|
|
3
|
+
* Signature algorithms accepted by default on every JWT this package verifies
|
|
4
|
+
* (id_token, logout_token, access token). Asymmetric only — defence against
|
|
5
|
+
* alg confusion.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_ALGS: string[];
|
|
8
|
+
/**
|
|
9
|
+
* The remote key set at `uri`, one per URI for the whole process: keys are
|
|
10
|
+
* fetched once and reused (jose refetches on an unknown `kid`, so rotation
|
|
11
|
+
* works). A rotated `jwks_uri` simply gets its own entry.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getJwks(uri: string): ReturnType<typeof createRemoteJWKSet>;
|
|
14
|
+
/** Clears the remote key sets (tests). */
|
|
15
|
+
export declare function clearJwksCache(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createRemoteJWKSet } from 'jose';
|
|
2
|
+
/**
|
|
3
|
+
* Signature algorithms accepted by default on every JWT this package verifies
|
|
4
|
+
* (id_token, logout_token, access token). Asymmetric only — defence against
|
|
5
|
+
* alg confusion.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_ALGS = [
|
|
8
|
+
'RS256',
|
|
9
|
+
'RS384',
|
|
10
|
+
'RS512',
|
|
11
|
+
'PS256',
|
|
12
|
+
'PS384',
|
|
13
|
+
'PS512',
|
|
14
|
+
'ES256',
|
|
15
|
+
'ES384',
|
|
16
|
+
'ES512',
|
|
17
|
+
'EdDSA',
|
|
18
|
+
];
|
|
19
|
+
const cache = new Map();
|
|
20
|
+
/**
|
|
21
|
+
* The remote key set at `uri`, one per URI for the whole process: keys are
|
|
22
|
+
* fetched once and reused (jose refetches on an unknown `kid`, so rotation
|
|
23
|
+
* works). A rotated `jwks_uri` simply gets its own entry.
|
|
24
|
+
*/
|
|
25
|
+
export function getJwks(uri) {
|
|
26
|
+
let set = cache.get(uri);
|
|
27
|
+
if (!set) {
|
|
28
|
+
set = createRemoteJWKSet(new URL(uri));
|
|
29
|
+
cache.set(uri, set);
|
|
30
|
+
}
|
|
31
|
+
return set;
|
|
32
|
+
}
|
|
33
|
+
/** Clears the remote key sets (tests). */
|
|
34
|
+
export function clearJwksCache() {
|
|
35
|
+
cache.clear();
|
|
36
|
+
}
|
|
@@ -20,6 +20,11 @@ export interface AuthorizeParams {
|
|
|
20
20
|
scopes: string[];
|
|
21
21
|
state: string;
|
|
22
22
|
codeChallenge: string;
|
|
23
|
+
/**
|
|
24
|
+
* OIDC `nonce`, echoed back in the id_token and checked by `validateIdToken`.
|
|
25
|
+
* Binds the id_token to this authorization request (replay protection).
|
|
26
|
+
*/
|
|
27
|
+
nonce?: string;
|
|
23
28
|
/**
|
|
24
29
|
* Endpoint de autorização do IdP — obtenha via `discoverEndpoints(issuer)`
|
|
25
30
|
* para IdPs de terceiros. Default: `${issuer}/auth` (convenção oidc-provider).
|
|
@@ -28,7 +33,8 @@ export interface AuthorizeParams {
|
|
|
28
33
|
/**
|
|
29
34
|
* Parâmetros extras anexados à URL de autorização (ex.: `audience`, `prompt`,
|
|
30
35
|
* `login_hint`, `ui_locales`, `acr_values`). Evita manipulação manual de URL no
|
|
31
|
-
* controller. Valores `undefined`/`null` são ignorados
|
|
36
|
+
* controller. Valores `undefined`/`null` são ignorados, assim como as chaves que a
|
|
37
|
+
* própria função monta (`state`, `nonce`, `code_challenge`, `scope`, `client_id`…).
|
|
32
38
|
*/
|
|
33
39
|
extraParams?: Record<string, string | number | undefined | null>;
|
|
34
40
|
}
|
package/build/src/oidc_login.js
CHANGED
|
@@ -14,6 +14,17 @@ export async function generatePkce() {
|
|
|
14
14
|
const challenge = base64url.encode(createHash('sha256').update(verifier).digest());
|
|
15
15
|
return { verifier, challenge, method: 'S256' };
|
|
16
16
|
}
|
|
17
|
+
/** Parameters `buildAuthorizeUrl` owns; `extraParams` cannot override them. */
|
|
18
|
+
const RESERVED_AUTHORIZE_PARAMS = new Set([
|
|
19
|
+
'client_id',
|
|
20
|
+
'response_type',
|
|
21
|
+
'redirect_uri',
|
|
22
|
+
'scope',
|
|
23
|
+
'state',
|
|
24
|
+
'nonce',
|
|
25
|
+
'code_challenge',
|
|
26
|
+
'code_challenge_method',
|
|
27
|
+
]);
|
|
17
28
|
export function buildAuthorizeUrl(p) {
|
|
18
29
|
const url = new URL(p.authorizationEndpoint ?? `${p.issuer}/auth`);
|
|
19
30
|
url.searchParams.set('client_id', p.clientId);
|
|
@@ -23,8 +34,14 @@ export function buildAuthorizeUrl(p) {
|
|
|
23
34
|
url.searchParams.set('state', p.state);
|
|
24
35
|
url.searchParams.set('code_challenge', p.codeChallenge);
|
|
25
36
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
37
|
+
if (p.nonce)
|
|
38
|
+
url.searchParams.set('nonce', p.nonce);
|
|
26
39
|
if (p.extraParams) {
|
|
27
40
|
for (const [key, value] of Object.entries(p.extraParams)) {
|
|
41
|
+
// A reserved key would silently replace the session-bound value (state, nonce,
|
|
42
|
+
// PKCE…) and the callback could never validate.
|
|
43
|
+
if (RESERVED_AUTHORIZE_PARAMS.has(key))
|
|
44
|
+
continue;
|
|
28
45
|
if (value !== undefined && value !== null)
|
|
29
46
|
url.searchParams.set(key, String(value));
|
|
30
47
|
}
|
|
@@ -62,8 +79,20 @@ async function tokenEndpoint(issuer, body, fetchImpl = fetch, endpoint, resilien
|
|
|
62
79
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
63
80
|
body: body.toString(),
|
|
64
81
|
}, resilience, fetchImpl);
|
|
65
|
-
if (!res.ok)
|
|
66
|
-
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
// RFC 6749 §5.2: the body says why (`invalid_grant`, `not_allowed`…). Without it a
|
|
84
|
+
// misconfigured client is a bare "400" and the cause is only in the IdP's logs.
|
|
85
|
+
const detail = await res
|
|
86
|
+
.json()
|
|
87
|
+
.then((json) => {
|
|
88
|
+
const body = json;
|
|
89
|
+
return typeof body?.error === 'string'
|
|
90
|
+
? `: ${body.error}${typeof body.error_description === 'string' ? ` (${body.error_description})` : ''}`
|
|
91
|
+
: '';
|
|
92
|
+
})
|
|
93
|
+
.catch(() => '');
|
|
94
|
+
throw new Error(`Token endpoint returned ${res.status}${detail}`);
|
|
95
|
+
}
|
|
67
96
|
const json = (await res.json());
|
|
68
97
|
return {
|
|
69
98
|
idToken: json.id_token ?? '',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { OidcCallbackError } from './authorization.js';
|
|
2
|
+
import { buildEndSessionUrl } from './oidc_login.js';
|
|
3
3
|
/**
|
|
4
4
|
* Registra as rotas OIDC de CLIENT (login/callback/logout [+ back-channel logout])
|
|
5
5
|
* absorvendo o boilerplate que cada app reescrevia no OidcSessionController: PKCE +
|
|
@@ -29,25 +29,14 @@ export function registerOidcClient(router, options = {}) {
|
|
|
29
29
|
// arrasta o code antigo para o authorize seguinte.
|
|
30
30
|
const loginRoute = router.get(`${prefix}/login`, async (ctx) => {
|
|
31
31
|
const manager = await ctx.containerResolver.make('authkit.client');
|
|
32
|
-
const cfg = manager.clientConfig;
|
|
33
|
-
const { verifier, challenge } = await generatePkce();
|
|
34
|
-
const state = randomUUID();
|
|
35
|
-
ctx.session?.put(pkceKey, { verifier, state });
|
|
36
32
|
const extraParams = {};
|
|
37
33
|
for (const param of passthrough) {
|
|
38
34
|
const v = ctx.request.input(param);
|
|
39
35
|
if (v !== undefined && v !== null && v !== '')
|
|
40
36
|
extraParams[param] = String(v);
|
|
41
37
|
}
|
|
42
|
-
const url =
|
|
43
|
-
|
|
44
|
-
clientId: cfg.clientId,
|
|
45
|
-
redirectUri: cfg.redirectUri,
|
|
46
|
-
scopes: cfg.scopes,
|
|
47
|
-
state,
|
|
48
|
-
codeChallenge: challenge,
|
|
49
|
-
...(Object.keys(extraParams).length ? { extraParams } : {}),
|
|
50
|
-
});
|
|
38
|
+
const { url, pending } = await manager.createAuthorizationRequest({ extraParams });
|
|
39
|
+
ctx.session?.put(pkceKey, pending);
|
|
51
40
|
return ctx.response.redirect().withQs(false).toPath(url);
|
|
52
41
|
});
|
|
53
42
|
if (options.loginMiddleware)
|
|
@@ -56,26 +45,28 @@ export function registerOidcClient(router, options = {}) {
|
|
|
56
45
|
router
|
|
57
46
|
.get(`${prefix}/callback`, async (ctx) => {
|
|
58
47
|
const manager = await ctx.containerResolver.make('authkit.client');
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
const pending = ctx.session?.get(pkceKey);
|
|
49
|
+
// Single use, whatever the outcome.
|
|
50
|
+
ctx.session?.forget(pkceKey);
|
|
51
|
+
let tokenSet;
|
|
52
|
+
try {
|
|
53
|
+
({ tokens: tokenSet } = await manager.completeAuthorization({
|
|
54
|
+
pending,
|
|
55
|
+
callbackParams: ctx.request.qs(),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Stale/expired callback (lost state, no code) or the IdP sent the user back
|
|
60
|
+
// with an error (access_denied…) → restart the login, as before. A failed
|
|
61
|
+
// id_token check or token exchange is a real failure and propagates.
|
|
62
|
+
if (error instanceof OidcCallbackError && error.restartable) {
|
|
63
|
+
return ctx.response.redirect().withQs(false).toPath(`${prefix}/login`);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
66
|
}
|
|
67
|
-
const tokenSet = await exchangeCode({
|
|
68
|
-
issuer: cfg.issuer,
|
|
69
|
-
clientId: cfg.clientId,
|
|
70
|
-
clientSecret: cfg.clientSecret,
|
|
71
|
-
redirectUri: cfg.redirectUri,
|
|
72
|
-
code,
|
|
73
|
-
codeVerifier: pkce.verifier,
|
|
74
|
-
});
|
|
75
67
|
// startSession, não `put(sessionKey)`: um login novo no mesmo cookie jar não pode
|
|
76
68
|
// herdar a credencial de impersonação parqueada pela sessão anterior.
|
|
77
69
|
manager.startSession(ctx, tokenSet);
|
|
78
|
-
ctx.session?.forget(pkceKey);
|
|
79
70
|
const authenticator = await manager.createAuthenticator(ctx);
|
|
80
71
|
const identity = await authenticator.getIdentity();
|
|
81
72
|
// 1) Hook do app tem prioridade quando retorna uma rota.
|
|
@@ -98,11 +89,19 @@ export function registerOidcClient(router, options = {}) {
|
|
|
98
89
|
const postLogoutRedirectUri = typeof options.postLogoutRedirect === 'function'
|
|
99
90
|
? options.postLogoutRedirect(ctx)
|
|
100
91
|
: (options.postLogoutRedirect ?? `${new URL(cfg.redirectUri).origin}/`);
|
|
92
|
+
// The local session is already gone. With no end_session_endpoint to send the
|
|
93
|
+
// browser to — the IdP publishes none (some Auth0 tenants) or discovery is
|
|
94
|
+
// failing right now — go straight to the destination instead of erroring.
|
|
95
|
+
const endSessionEndpoint = await manager.endpoints().then((endpoints) => endpoints.endSessionEndpoint, () => undefined);
|
|
96
|
+
if (!endSessionEndpoint) {
|
|
97
|
+
return ctx.response.redirect().withQs(false).toPath(postLogoutRedirectUri);
|
|
98
|
+
}
|
|
101
99
|
const endSessionUrl = buildEndSessionUrl({
|
|
102
100
|
issuer: cfg.issuer,
|
|
103
101
|
idToken,
|
|
104
102
|
postLogoutRedirectUri,
|
|
105
103
|
clientId: cfg.clientId,
|
|
104
|
+
endSessionEndpoint,
|
|
106
105
|
});
|
|
107
106
|
return ctx.response.redirect().withQs(false).toPath(endSessionUrl);
|
|
108
107
|
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { SessionResolver } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { type OidcEndpoints } from '../discovery.js';
|
|
2
3
|
import { type TokenSource } from '../token_source.js';
|
|
3
4
|
export interface JwtResolverFactoryConfig {
|
|
4
5
|
tokenSource?: TokenSource;
|
|
5
|
-
/** jwks_uri
|
|
6
|
+
/** Explicit jwks_uri. Default: the discovered `jwks_uri` (`${issuer}/jwks` with discovery off). */
|
|
6
7
|
jwksUri?: string;
|
|
7
8
|
}
|
|
8
9
|
export interface PatResolverFactoryConfig {
|
|
@@ -15,7 +16,7 @@ export interface OpaqueResolverFactoryConfig {
|
|
|
15
16
|
* guardado no TokenSet; `bearer` introspecta o token do header Authorization (APIs).
|
|
16
17
|
*/
|
|
17
18
|
tokenSource?: TokenSource;
|
|
18
|
-
/**
|
|
19
|
+
/** Introspection endpoint. Default: the discovered one (`${issuer}/token/introspection` with discovery off). */
|
|
19
20
|
introspectionUrl?: string;
|
|
20
21
|
/**
|
|
21
22
|
* TTL (ms) do cache de respostas `active:true`. Default 0 (revogação imediata).
|
|
@@ -30,6 +31,12 @@ export interface ResolverContext {
|
|
|
30
31
|
clientSecret?: string;
|
|
31
32
|
sessionKey: string;
|
|
32
33
|
globalRolesClaim: string;
|
|
34
|
+
/** The IdP endpoints (discovery + overrides), cached by the caller — call it per request. */
|
|
35
|
+
endpoints: () => Promise<OidcEndpoints>;
|
|
36
|
+
/** Where a resolver reports that the IdP cannot serve it (e.g. no introspection). */
|
|
37
|
+
logger: {
|
|
38
|
+
warn(obj: object, msg: string): void;
|
|
39
|
+
};
|
|
33
40
|
}
|
|
34
41
|
export interface ResolverFactory {
|
|
35
42
|
resolver(ctx: ResolverContext): Promise<SessionResolver>;
|
|
@@ -1,16 +1,36 @@
|
|
|
1
|
+
import { OidcDiscoveryError } from '../discovery.js';
|
|
1
2
|
import { getTokenFromSource } from '../token_source.js';
|
|
2
3
|
import { JwtResolver } from './jwt_resolver.js';
|
|
3
4
|
import { OpaqueResolver } from './opaque_resolver.js';
|
|
4
5
|
import { PatResolver } from './pat_resolver.js';
|
|
6
|
+
/**
|
|
7
|
+
* The current endpoints, or `null` while discovery fails (and nothing was ever
|
|
8
|
+
* discovered): a request then resolves to no identity — the same outcome as an
|
|
9
|
+
* unreachable JWKS — instead of failing every request, public pages included via
|
|
10
|
+
* silent auth. Any other error is a configuration error and throws.
|
|
11
|
+
*/
|
|
12
|
+
async function currentEndpoints(rc) {
|
|
13
|
+
try {
|
|
14
|
+
return await rc.endpoints();
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof OidcDiscoveryError)
|
|
18
|
+
return null;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
5
22
|
export const resolvers = {
|
|
6
23
|
jwt(config = {}) {
|
|
7
24
|
const tokenSource = config.tokenSource ?? 'session';
|
|
8
25
|
return {
|
|
9
26
|
async resolver(rc) {
|
|
10
|
-
const jwksUri = config.jwksUri ?? `${rc.issuer}/jwks`;
|
|
11
27
|
return new JwtResolver({
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
endpoints: async () => {
|
|
29
|
+
const endpoints = await currentEndpoints(rc);
|
|
30
|
+
if (!endpoints)
|
|
31
|
+
return null;
|
|
32
|
+
return { issuer: endpoints.issuer, jwksUri: config.jwksUri ?? endpoints.jwksUri };
|
|
33
|
+
},
|
|
14
34
|
audience: rc.clientId,
|
|
15
35
|
globalRolesClaim: rc.globalRolesClaim,
|
|
16
36
|
getToken: (httpCtx) => getTokenFromSource(httpCtx, tokenSource, rc.sessionKey),
|
|
@@ -35,9 +55,21 @@ export const resolvers = {
|
|
|
35
55
|
if (!rc.clientSecret) {
|
|
36
56
|
throw new Error('resolvers.opaque requer um client confidencial (clientSecret) para introspection');
|
|
37
57
|
}
|
|
38
|
-
|
|
58
|
+
let warned = false;
|
|
39
59
|
return new OpaqueResolver({
|
|
40
|
-
introspectionUrl
|
|
60
|
+
introspectionUrl: async () => {
|
|
61
|
+
if (config.introspectionUrl)
|
|
62
|
+
return config.introspectionUrl;
|
|
63
|
+
const endpoints = await currentEndpoints(rc);
|
|
64
|
+
if (!endpoints)
|
|
65
|
+
return null;
|
|
66
|
+
if (!endpoints.introspectionEndpoint && !warned) {
|
|
67
|
+
warned = true;
|
|
68
|
+
rc.logger.warn({ issuer: rc.issuer }, 'authkit-client: the IdP publishes no introspection_endpoint, so resolvers.opaque ' +
|
|
69
|
+
'resolves every request to no identity — set introspectionUrl');
|
|
70
|
+
}
|
|
71
|
+
return endpoints.introspectionEndpoint ?? null;
|
|
72
|
+
},
|
|
41
73
|
clientId: rc.clientId,
|
|
42
74
|
clientSecret: rc.clientSecret,
|
|
43
75
|
tokenSource,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Identity, SessionResolver } from '@adonis-agora/authkit-core';
|
|
2
2
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
import type { OidcEndpoints } from '../discovery.js';
|
|
3
4
|
export interface JwtResolverConfig {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Issuer and jwks_uri to verify against, read on every token so a rotated
|
|
7
|
+
* `jwks_uri` / `issuer` takes effect without a restart. `null` (discovery is
|
|
8
|
+
* failing) resolves to no identity.
|
|
9
|
+
*/
|
|
10
|
+
endpoints: () => Promise<Pick<OidcEndpoints, 'issuer' | 'jwksUri'> | null>;
|
|
6
11
|
audience: string;
|
|
7
12
|
globalRolesClaim: string;
|
|
8
13
|
/** algoritmos de assinatura aceitos (defesa contra alg-confusion). Default: asimétricos. */
|
|
@@ -11,7 +16,6 @@ export interface JwtResolverConfig {
|
|
|
11
16
|
getToken?: (ctx: HttpContext) => string | null;
|
|
12
17
|
}
|
|
13
18
|
export declare class JwtResolver implements SessionResolver {
|
|
14
|
-
#private;
|
|
15
19
|
private config;
|
|
16
20
|
constructor(config: JwtResolverConfig);
|
|
17
21
|
/** Valida um JWT cru e monta a Identity (ou null se inválido). */
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jwtVerify } from 'jose';
|
|
2
|
+
import { DEFAULT_ALGS, getJwks } from '../jwks.js';
|
|
2
3
|
import { buildIdentityFromClaims } from './identity.js';
|
|
3
|
-
const DEFAULT_ALGS = [
|
|
4
|
-
'RS256',
|
|
5
|
-
'RS384',
|
|
6
|
-
'RS512',
|
|
7
|
-
'PS256',
|
|
8
|
-
'PS384',
|
|
9
|
-
'PS512',
|
|
10
|
-
'ES256',
|
|
11
|
-
'ES384',
|
|
12
|
-
'ES512',
|
|
13
|
-
'EdDSA',
|
|
14
|
-
];
|
|
15
4
|
export class JwtResolver {
|
|
16
5
|
config;
|
|
17
|
-
#jwks;
|
|
18
6
|
constructor(config) {
|
|
19
7
|
this.config = config;
|
|
20
|
-
this.#jwks = createRemoteJWKSet(new URL(config.jwksUri));
|
|
21
8
|
}
|
|
22
9
|
/** Valida um JWT cru e monta a Identity (ou null se inválido). */
|
|
23
10
|
async resolveToken(token) {
|
|
11
|
+
const endpoints = await this.config.endpoints();
|
|
12
|
+
if (!endpoints)
|
|
13
|
+
return null;
|
|
14
|
+
const keys = getJwks(endpoints.jwksUri);
|
|
24
15
|
try {
|
|
25
|
-
const { payload } = await jwtVerify(token,
|
|
26
|
-
issuer:
|
|
16
|
+
const { payload } = await jwtVerify(token, keys, {
|
|
17
|
+
issuer: endpoints.issuer,
|
|
27
18
|
audience: this.config.audience,
|
|
28
19
|
algorithms: this.config.algorithms ?? DEFAULT_ALGS,
|
|
29
20
|
});
|
|
@@ -6,8 +6,11 @@ type FetchImpl = (url: string, init: any) => Promise<{
|
|
|
6
6
|
json: () => Promise<any>;
|
|
7
7
|
}>;
|
|
8
8
|
export interface OpaqueResolverConfig {
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Endpoint de introspection do IdP (RFC 7662), lido a cada request. `null` (o IdP
|
|
11
|
+
* não publica um, ou o discovery está falhando) resolve para nenhuma identidade.
|
|
12
|
+
*/
|
|
13
|
+
introspectionUrl: () => Promise<string | null>;
|
|
11
14
|
/** client_id usado para autenticar a introspection (client confidencial). */
|
|
12
15
|
clientId: string;
|
|
13
16
|
/** client_secret correspondente (Basic auth). */
|
|
@@ -33,8 +33,11 @@ export class OpaqueResolver {
|
|
|
33
33
|
if (hit && this.#now() - hit.storedAt < ttl)
|
|
34
34
|
return hit.identity;
|
|
35
35
|
}
|
|
36
|
+
const introspectionUrl = await this.config.introspectionUrl();
|
|
37
|
+
if (!introspectionUrl)
|
|
38
|
+
return null;
|
|
36
39
|
const basic = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
|
|
37
|
-
const data = await introspectToken(
|
|
40
|
+
const data = await introspectToken(introspectionUrl, token, { type: 'basic', value: basic }, { tokenTypeHint: 'access_token', fetchImpl: this.config.fetchImpl });
|
|
38
41
|
if (!data) {
|
|
39
42
|
this.#cache.delete(token);
|
|
40
43
|
return null;
|
|
@@ -33,5 +33,3 @@ export interface JwtAccessTokenClaims extends JWTPayload {
|
|
|
33
33
|
* claims validadas no caminho feliz.
|
|
34
34
|
*/
|
|
35
35
|
export declare function verifyJwtAccessToken(token: string, options: VerifyJwtAccessTokenOptions): Promise<JwtAccessTokenClaims>;
|
|
36
|
-
/** Limpa o cache de JWKS remotos (útil em testes). */
|
|
37
|
-
export declare function clearJwksCache(): void;
|
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
'RS256',
|
|
4
|
-
'RS384',
|
|
5
|
-
'RS512',
|
|
6
|
-
'PS256',
|
|
7
|
-
'PS384',
|
|
8
|
-
'PS512',
|
|
9
|
-
'ES256',
|
|
10
|
-
'ES384',
|
|
11
|
-
'ES512',
|
|
12
|
-
'EdDSA',
|
|
13
|
-
];
|
|
14
|
-
// Cache de keysets remotos por jwks_uri — evita refazer fetch/parse por request.
|
|
15
|
-
const jwksCache = new Map();
|
|
16
|
-
function getJwks(uri) {
|
|
17
|
-
let set = jwksCache.get(uri);
|
|
18
|
-
if (!set) {
|
|
19
|
-
set = createRemoteJWKSet(new URL(uri));
|
|
20
|
-
jwksCache.set(uri, set);
|
|
21
|
-
}
|
|
22
|
-
return set;
|
|
23
|
-
}
|
|
1
|
+
import { jwtVerify } from 'jose';
|
|
2
|
+
import { DEFAULT_ALGS, getJwks } from './jwks.js';
|
|
24
3
|
/**
|
|
25
4
|
* Verifica um JWT AT (RFC 9068). Lança (via jose) se a assinatura/claims forem
|
|
26
5
|
* inválidas ou se o `typ` não for `at+jwt` (a menos que `allowAnyTyp`). Retorna as
|
|
@@ -37,7 +16,3 @@ export async function verifyJwtAccessToken(token, options) {
|
|
|
37
16
|
});
|
|
38
17
|
return payload;
|
|
39
18
|
}
|
|
40
|
-
/** Limpa o cache de JWKS remotos (útil em testes). */
|
|
41
|
-
export function clearJwksCache() {
|
|
42
|
-
jwksCache.clear();
|
|
43
|
-
}
|
|
@@ -1,42 +1,47 @@
|
|
|
1
1
|
{{{
|
|
2
2
|
exports({ to: app.makePath('app/controllers/oidc_session_controller.ts') })
|
|
3
3
|
}}}
|
|
4
|
+
import { inject } from '@adonisjs/core'
|
|
4
5
|
import type { HttpContext } from '@adonisjs/core/http'
|
|
5
|
-
import {
|
|
6
|
-
import { randomUUID } from 'node:crypto'
|
|
6
|
+
import { AuthkitClientManager, OidcCallbackError } from '@adonis-agora/authkit-client'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Login, callback and logout written out by hand. If you do not need to customise
|
|
10
|
+
* them, `registerOidcClient(router)` registers the same three routes for you.
|
|
11
|
+
*/
|
|
12
|
+
@inject()
|
|
8
13
|
export default class OidcSessionController {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
issuer: cfg.issuer, clientId: cfg.clientId, redirectUri: cfg.redirectUri,
|
|
17
|
-
scopes: cfg.scopes, state, codeChallenge: challenge,
|
|
18
|
-
})
|
|
19
|
-
return ctx.response.redirect(url)
|
|
14
|
+
constructor(protected authkit: AuthkitClientManager) {}
|
|
15
|
+
|
|
16
|
+
async login({ session, response }: HttpContext) {
|
|
17
|
+
// Fresh state + nonce + PKCE, against the endpoints discovered from the issuer.
|
|
18
|
+
const { url, pending } = await this.authkit.createAuthorizationRequest()
|
|
19
|
+
session.put('authkit_pkce', pending)
|
|
20
|
+
return response.redirect(url)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async callback(ctx: HttpContext) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
const pending = ctx.session.pull('authkit_pkce')
|
|
25
|
+
try {
|
|
26
|
+
// Checks state and iss, exchanges the code and validates the id_token (nonce included).
|
|
27
|
+
const { tokens: tokenSet } = await this.authkit.completeAuthorization({
|
|
28
|
+
pending,
|
|
29
|
+
callbackParams: ctx.request.qs(),
|
|
30
|
+
})
|
|
31
|
+
// startSession (not session.put) drops any impersonation parked by a previous session.
|
|
32
|
+
this.authkit.startSession(ctx, tokenSet)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Stale callback, or the IdP sent the user back with an error (access_denied…).
|
|
35
|
+
if (error instanceof OidcCallbackError && error.restartable) {
|
|
36
|
+
return ctx.response.redirect('/auth/login')
|
|
37
|
+
}
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
34
40
|
return ctx.response.redirect('/')
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
async logout(ctx: HttpContext) {
|
|
38
|
-
|
|
39
|
-
manager.endSession(ctx)
|
|
44
|
+
this.authkit.endSession(ctx)
|
|
40
45
|
return ctx.response.redirect('/')
|
|
41
46
|
}
|
|
42
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 relying-party (client) toolkit: session-based authentication against an OpenID Connect identity provider, with JWT/PAT user resolvers and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@adonis-agora/resilience": ">=0.1.0 <1.0.0",
|
|
65
|
-
"@adonisjs/auth": "10.1.0",
|
|
66
|
-
"@adonisjs/core": "7.5.0",
|
|
67
|
-
"@adonisjs/lucid": "22.4.2",
|
|
68
|
-
"@adonisjs/session": "8.1.0"
|
|
65
|
+
"@adonisjs/auth": "^10.1.0",
|
|
66
|
+
"@adonisjs/core": "^7.5.0",
|
|
67
|
+
"@adonisjs/lucid": "^22.4.2",
|
|
68
|
+
"@adonisjs/session": "^8.1.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@adonisjs/auth": {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"jose": "6.2.12"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@adonis-agora/authkit-server": "0.
|
|
86
|
+
"@adonis-agora/authkit-server": "0.80.1",
|
|
87
87
|
"@adonis-agora/resilience": "0.3.7",
|
|
88
88
|
"@adonisjs/auth": "10.1.0",
|
|
89
89
|
"@adonisjs/core": "7.5.0",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"@types/node": "26.5.1",
|
|
98
98
|
"ioredis-mock": "8.13.1",
|
|
99
99
|
"tempura": "0.4.1",
|
|
100
|
+
"testcontainers": "12.2.1",
|
|
100
101
|
"typescript": "7.0.2"
|
|
101
102
|
},
|
|
102
103
|
"scripts": {
|