@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.
@@ -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
  }
@@ -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
- throw new Error(`Token endpoint returned ${res.status}`);
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 { randomUUID } from 'node:crypto';
2
- import { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, generatePkce } from './oidc_login.js';
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 = buildAuthorizeUrl({
43
- issuer: cfg.issuer,
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 cfg = manager.clientConfig;
60
- const { code, state } = ctx.request.qs();
61
- const pkce = ctx.session?.get(pkceKey);
62
- // Callback velho/expirado (state perdido ou code ausente) → recomeça o login.
63
- if (!pkce || pkce.state !== state || !code) {
64
- ctx.session?.forget(pkceKey);
65
- return ctx.response.redirect().withQs(false).toPath(`${prefix}/login`);
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 explícito; se omitido, é `${issuer}/jwks` (rota padrão do oidc-provider) */
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
- /** Endpoint de introspection; default `${issuer}/token/introspection` (oidc-provider). */
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
- issuer: rc.issuer,
13
- jwksUri,
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
- const introspectionUrl = config.introspectionUrl ?? `${rc.issuer}/token/introspection`;
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
- issuer: string;
5
- jwksUri: string;
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 { createRemoteJWKSet, jwtVerify } from 'jose';
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, this.#jwks, {
26
- issuer: this.config.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
- /** Endpoint de introspection do IdP (RFC 7662). Ex.: `${issuer}/token/introspection`. */
10
- introspectionUrl: string;
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(this.config.introspectionUrl, token, { type: 'basic', value: basic }, { tokenTypeHint: 'access_token', fetchImpl: this.config.fetchImpl });
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 { createRemoteJWKSet, jwtVerify } from 'jose';
2
- const DEFAULT_ALGS = [
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 { buildAuthorizeUrl, exchangeCode, generatePkce } from '@adonis-agora/authkit-client'
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
- async login(ctx: HttpContext) {
10
- const manager = await ctx.containerResolver.make('authkit.client')
11
- const cfg = manager.clientConfig
12
- const { verifier, challenge } = await generatePkce()
13
- const state = randomUUID()
14
- ctx.session.put('authkit_pkce', { verifier, state })
15
- const url = buildAuthorizeUrl({
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 manager = await ctx.containerResolver.make('authkit.client')
24
- const cfg = manager.clientConfig
25
- const { code, state } = ctx.request.qs()
26
- const pkce = ctx.session.get('authkit_pkce')
27
- if (!pkce || pkce.state !== state) return ctx.response.badRequest({ error: 'state inválido' })
28
- const tokenSet = await exchangeCode({
29
- issuer: cfg.issuer, clientId: cfg.clientId, clientSecret: cfg.clientSecret,
30
- redirectUri: cfg.redirectUri, code, codeVerifier: pkce.verifier,
31
- })
32
- manager.startSession(ctx, tokenSet)
33
- ctx.session.forget('authkit_pkce')
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
- const manager = await ctx.containerResolver.make('authkit.client')
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.18.8",
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.74.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": {