@adonis-agora/authkit-client 0.9.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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/build/commands/configure.d.ts +2 -0
  4. package/build/commands/configure.js +17 -0
  5. package/build/index.d.ts +28 -0
  6. package/build/index.js +15 -0
  7. package/build/providers/authkit_client_provider.d.ts +61 -0
  8. package/build/providers/authkit_client_provider.js +187 -0
  9. package/build/src/authenticator.d.ts +41 -0
  10. package/build/src/authenticator.js +105 -0
  11. package/build/src/backchannel_logout.d.ts +77 -0
  12. package/build/src/backchannel_logout.js +114 -0
  13. package/build/src/define_config.d.ts +80 -0
  14. package/build/src/define_config.js +41 -0
  15. package/build/src/discovery.d.ts +55 -0
  16. package/build/src/discovery.js +90 -0
  17. package/build/src/dpop.d.ts +50 -0
  18. package/build/src/dpop.js +48 -0
  19. package/build/src/http/resilient_fetch.d.ts +30 -0
  20. package/build/src/http/resilient_fetch.js +26 -0
  21. package/build/src/lucid_mirror.d.ts +38 -0
  22. package/build/src/lucid_mirror.js +32 -0
  23. package/build/src/middleware/auth_middleware.d.ts +29 -0
  24. package/build/src/middleware/auth_middleware.js +36 -0
  25. package/build/src/middleware/authkit_middleware.d.ts +5 -0
  26. package/build/src/middleware/authkit_middleware.js +9 -0
  27. package/build/src/middleware/backchannel_revocation_middleware.d.ts +18 -0
  28. package/build/src/middleware/backchannel_revocation_middleware.js +50 -0
  29. package/build/src/middleware/silent_auth_middleware.d.ts +13 -0
  30. package/build/src/middleware/silent_auth_middleware.js +14 -0
  31. package/build/src/observability/client_metrics.d.ts +10 -0
  32. package/build/src/observability/client_metrics.js +59 -0
  33. package/build/src/observability/context_bridge.d.ts +7 -0
  34. package/build/src/observability/context_bridge.js +49 -0
  35. package/build/src/oidc_login.d.ts +113 -0
  36. package/build/src/oidc_login.js +123 -0
  37. package/build/src/register_oidc_client.d.ts +50 -0
  38. package/build/src/register_oidc_client.js +125 -0
  39. package/build/src/resolvers/factory.d.ts +41 -0
  40. package/build/src/resolvers/factory.js +51 -0
  41. package/build/src/resolvers/identity.d.ts +30 -0
  42. package/build/src/resolvers/identity.js +49 -0
  43. package/build/src/resolvers/jwt_resolver.d.ts +20 -0
  44. package/build/src/resolvers/jwt_resolver.js +34 -0
  45. package/build/src/resolvers/opaque_resolver.d.ts +43 -0
  46. package/build/src/resolvers/opaque_resolver.js +51 -0
  47. package/build/src/resolvers/pat_resolver.d.ts +18 -0
  48. package/build/src/resolvers/pat_resolver.js +24 -0
  49. package/build/src/revocation/revocation_store.d.ts +68 -0
  50. package/build/src/revocation/revocation_store.js +0 -0
  51. package/build/src/token_source.d.ts +3 -0
  52. package/build/src/token_source.js +12 -0
  53. package/build/src/types.d.ts +6 -0
  54. package/build/src/types.js +1 -0
  55. package/build/src/user_resolvers.d.ts +41 -0
  56. package/build/src/user_resolvers.js +42 -0
  57. package/build/src/verify_access_token.d.ts +37 -0
  58. package/build/src/verify_access_token.js +43 -0
  59. package/build/stubs/.gitkeep +0 -0
  60. package/build/stubs/config/authkit_client.stub +17 -0
  61. package/build/stubs/controllers/oidc_session_controller.stub +42 -0
  62. package/build/stubs/main.d.ts +1 -0
  63. package/build/stubs/main.js +2 -0
  64. package/build/types.d.ts +13 -0
  65. package/build/types.js +1 -0
  66. package/package.json +92 -0
  67. package/stubs/.gitkeep +0 -0
  68. package/stubs/config/authkit_client.stub +17 -0
  69. package/stubs/controllers/oidc_session_controller.stub +42 -0
  70. package/stubs/main.ts +2 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Middleware de autenticação pronto do AuthKit: exige sessão válida (`ctx.auth.check()`)
3
+ * e, opcionalmente, papéis (global OU app). Substitui o middleware de auth que cada
4
+ * consumidor reescrevia à mão.
5
+ *
6
+ * ```ts
7
+ * // start/kernel.ts
8
+ * export const middleware = router.named({
9
+ * auth: () => import('@adonis-agora/authkit-client/auth_middleware'),
10
+ * })
11
+ * // uso: .use(middleware.auth({ roles: ['ADVISOR'], redirectTo: '/auth/login' }))
12
+ * ```
13
+ */
14
+ export default class AuthkitAuthMiddleware {
15
+ async handle(ctx, next, options = {}) {
16
+ const redirectTo = options.redirectTo ?? '/auth/login';
17
+ const ok = await ctx.auth.check();
18
+ if (!ok)
19
+ return ctx.response.redirect(redirectTo);
20
+ if (options.roles?.length) {
21
+ const isGlobal = options.roles.some((r) => ctx.auth.hasGlobalRole(r));
22
+ let allowed = isGlobal;
23
+ if (!allowed) {
24
+ for (const r of options.roles) {
25
+ if (await ctx.auth.hasAppRole(r)) {
26
+ allowed = true;
27
+ break;
28
+ }
29
+ }
30
+ }
31
+ if (!allowed)
32
+ return ctx.response.redirect(options.unauthorizedRedirect ?? '/unauthorized');
33
+ }
34
+ return next();
35
+ }
36
+ }
@@ -0,0 +1,5 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ export default class AuthkitMiddleware {
4
+ handle(ctx: HttpContext, next: NextFn): Promise<any>;
5
+ }
@@ -0,0 +1,9 @@
1
+ export default class AuthkitMiddleware {
2
+ async handle(ctx, next) {
3
+ const manager = await ctx.containerResolver.make('authkit.client');
4
+ // Renova proativamente o TokenSet (rotação de refresh token) antes de resolver.
5
+ await manager.maybeRefresh(ctx);
6
+ ctx.auth = await manager.createAuthenticator(ctx);
7
+ return next();
8
+ }
9
+ }
@@ -0,0 +1,18 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ /**
4
+ * Aplica revogações de OIDC Back-Channel Logout sobre sessões COOKIE-BASED.
5
+ *
6
+ * Sem store server-side não dá pra destruir a sessão no instante do logout_token;
7
+ * este middleware roda em toda request COM token set na sessão, extrai sid/sub/iat
8
+ * do id_token e, se a sessão foi revogada (via {@link RevocationStore}), limpa o
9
+ * token set local — o resolver downstream trata como não-autenticado.
10
+ *
11
+ * No-op quando `backchannelLogout.store` não está configurado em `config/authkit_client.ts`.
12
+ *
13
+ * Ordem no kernel: DEPOIS do `authkit_middleware` (depende do `authkit.client`
14
+ * resolvido), antes dos named middlewares de auth.
15
+ */
16
+ export default class BackchannelRevocationMiddleware {
17
+ handle(ctx: HttpContext, next: NextFn): Promise<any>;
18
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Aplica revogações de OIDC Back-Channel Logout sobre sessões COOKIE-BASED.
3
+ *
4
+ * Sem store server-side não dá pra destruir a sessão no instante do logout_token;
5
+ * este middleware roda em toda request COM token set na sessão, extrai sid/sub/iat
6
+ * do id_token e, se a sessão foi revogada (via {@link RevocationStore}), limpa o
7
+ * token set local — o resolver downstream trata como não-autenticado.
8
+ *
9
+ * No-op quando `backchannelLogout.store` não está configurado em `config/authkit_client.ts`.
10
+ *
11
+ * Ordem no kernel: DEPOIS do `authkit_middleware` (depende do `authkit.client`
12
+ * resolvido), antes dos named middlewares de auth.
13
+ */
14
+ export default class BackchannelRevocationMiddleware {
15
+ async handle(ctx, next) {
16
+ const manager = await ctx.containerResolver.make('authkit.client');
17
+ const store = manager.clientConfig.backchannelStore;
18
+ if (!store)
19
+ return next();
20
+ const sessionKey = manager.clientConfig.sessionKey;
21
+ const idToken = manager.getIdToken(ctx);
22
+ if (idToken) {
23
+ const claims = decodeJwtPayload(idToken);
24
+ const sid = typeof claims?.sid === 'string' ? claims.sid : undefined;
25
+ const sub = typeof claims?.sub === 'string' ? claims.sub : undefined;
26
+ const authTime = typeof claims?.iat === 'number' ? claims.iat : undefined;
27
+ if ((sid || sub) && (await store.isRevoked({ sid, sub, authTime }))) {
28
+ // session é augmentado por @adonisjs/session no app host; na lib usamos cast.
29
+ ;
30
+ ctx.session?.forget(sessionKey);
31
+ }
32
+ }
33
+ return next();
34
+ }
35
+ }
36
+ /**
37
+ * Decodifica o payload de um JWT SEM verificar assinatura — seguro aqui porque o
38
+ * id_token foi validado no login e vive numa sessão assinada/encriptada pelo app.
39
+ */
40
+ function decodeJwtPayload(token) {
41
+ const parts = token.split('.');
42
+ if (parts.length !== 3)
43
+ return null;
44
+ try {
45
+ return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
@@ -0,0 +1,13 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ /**
4
+ * Resolve a identidade da sessão (popula `ctx.auth`) SEM exigir login — útil em
5
+ * rotas públicas que mostram estado logado/deslogado. Nunca redireciona.
6
+ *
7
+ * ```ts
8
+ * silentAuth: () => import('@adonis-agora/authkit-client/silent_auth_middleware'),
9
+ * ```
10
+ */
11
+ export default class AuthkitSilentAuthMiddleware {
12
+ handle(ctx: HttpContext, next: NextFn): Promise<any>;
13
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolve a identidade da sessão (popula `ctx.auth`) SEM exigir login — útil em
3
+ * rotas públicas que mostram estado logado/deslogado. Nunca redireciona.
4
+ *
5
+ * ```ts
6
+ * silentAuth: () => import('@adonis-agora/authkit-client/silent_auth_middleware'),
7
+ * ```
8
+ */
9
+ export default class AuthkitSilentAuthMiddleware {
10
+ async handle(ctx, next) {
11
+ await ctx.auth.check();
12
+ return next();
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ import { type AuthkitMetricName, type MetricsRecorder, type MetricsSnapshot } from '@adonis-agora/authkit-core';
2
+ /** Recorder que (best-effort) emite via @opentelemetry/api e SEMPRE agrega em memória. */
3
+ export declare class OtelRecorder implements MetricsRecorder {
4
+ #private;
5
+ private constructor();
6
+ static create(name: string): Promise<OtelRecorder>;
7
+ increment(name: AuthkitMetricName, attributes?: Record<string, string | number>): void;
8
+ record(name: AuthkitMetricName, value: number, attributes?: Record<string, string | number>): void;
9
+ snapshot(): MetricsSnapshot;
10
+ }
@@ -0,0 +1,59 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { InMemorySnapshot, } from '@adonis-agora/authkit-core';
10
+ /** Recorder que (best-effort) emite via @opentelemetry/api e SEMPRE agrega em memória. */
11
+ export class OtelRecorder {
12
+ #snapshot = new InMemorySnapshot();
13
+ #counters = new Map();
14
+ #histograms = new Map();
15
+ #meter;
16
+ constructor(meter) {
17
+ this.#meter = meter;
18
+ }
19
+ static async create(name) {
20
+ let meter = null;
21
+ try {
22
+ // Specifier dinâmico (não-estático) para que o TS não tente resolver o
23
+ // peer opcional @opentelemetry/api em tempo de compilação. Em runtime,
24
+ // se o pacote não estiver instalado, caímos no catch e ficamos no-op.
25
+ const moduleName = '@opentelemetry/api';
26
+ const otel = await import(__rewriteRelativeImportExtension(moduleName));
27
+ meter = otel.metrics.getMeter(name);
28
+ }
29
+ catch {
30
+ meter = null;
31
+ }
32
+ return new OtelRecorder(meter);
33
+ }
34
+ increment(name, attributes) {
35
+ this.#snapshot.bump(name);
36
+ if (!this.#meter)
37
+ return;
38
+ let c = this.#counters.get(name);
39
+ if (!c) {
40
+ c = this.#meter.createCounter(name);
41
+ this.#counters.set(name, c);
42
+ }
43
+ c.add(1, attributes);
44
+ }
45
+ record(name, value, attributes) {
46
+ this.#snapshot.observe(name, value);
47
+ if (!this.#meter)
48
+ return;
49
+ let h = this.#histograms.get(name);
50
+ if (!h) {
51
+ h = this.#meter.createHistogram(name);
52
+ this.#histograms.set(name, h);
53
+ }
54
+ h.record(value, attributes);
55
+ }
56
+ snapshot() {
57
+ return this.#snapshot.read();
58
+ }
59
+ }
@@ -0,0 +1,7 @@
1
+ import type { Identity } from "@adonis-agora/authkit-core";
2
+ /**
3
+ * Popula o contexto do Agora a partir de uma identidade recém-resolvida:
4
+ * `userRef` ({ type: 'user', id: userId }) e, quando presente nas claims,
5
+ * `tenantId`. Best-effort: qualquer erro é engolido e nada acontece sem o slot.
6
+ */
7
+ export declare function populateContext(identity: Identity): void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Ponte estrutural (best-effort) entre a sessão resolvida do authkit e o
3
+ * contexto do Agora. Lê o slot global `@agora/context:set` de forma ESTRUTURAL —
4
+ * nunca importa `@adonis-agora/context` — e degrada para no-op quando o context
5
+ * não está instalado. Mesmo idioma da `diagnostics_bridge`.
6
+ */
7
+ const SET_SLOT = Symbol.for("@agora/context:set");
8
+ /**
9
+ * Claims OIDC candidatas a "organização/tenant ativo". A primeira presente (e
10
+ * string não vazia) vira o `tenantId`. Não falha se nenhuma existir.
11
+ */
12
+ const TENANT_CLAIMS = [
13
+ "active_organization_id",
14
+ "org_id",
15
+ "organization_id",
16
+ "tenant_id",
17
+ "tid",
18
+ ];
19
+ /** Deriva o tenant ativo das claims cruas, ou `undefined` quando ausente. */
20
+ function deriveTenant(raw) {
21
+ if (!raw)
22
+ return undefined;
23
+ for (const claim of TENANT_CLAIMS) {
24
+ const value = raw[claim];
25
+ if (typeof value === "string" && value.length > 0)
26
+ return value;
27
+ }
28
+ return undefined;
29
+ }
30
+ /**
31
+ * Popula o contexto do Agora a partir de uma identidade recém-resolvida:
32
+ * `userRef` ({ type: 'user', id: userId }) e, quando presente nas claims,
33
+ * `tenantId`. Best-effort: qualquer erro é engolido e nada acontece sem o slot.
34
+ */
35
+ export function populateContext(identity) {
36
+ try {
37
+ const set = globalThis[SET_SLOT];
38
+ if (!set)
39
+ return;
40
+ const tenant = deriveTenant(identity.raw);
41
+ set({
42
+ userRef: { type: "user", id: identity.userId },
43
+ ...(tenant ? { tenantId: tenant } : {}),
44
+ });
45
+ }
46
+ catch {
47
+ // best-effort: a ponte de contexto nunca quebra o caminho da request.
48
+ }
49
+ }
@@ -0,0 +1,113 @@
1
+ import type { TokenSet } from "./types.js";
2
+ import { type ResiliencePolicy } from "./http/resilient_fetch.js";
3
+ /**
4
+ * Gera um par PKCE (code_verifier + code_challenge) usando o método S256.
5
+ *
6
+ * Observação: o `jose` v5 instalado não exporta os helpers
7
+ * `generateRandomCodeVerifier`/`calculatePKCECodeChallenge`, então usamos o
8
+ * `crypto` do Node + `jose.base64url` para produzir valores equivalentes,
9
+ * conforme RFC 7636 (verifier aleatório base64url, challenge = base64url(SHA-256(verifier))).
10
+ */
11
+ export declare function generatePkce(): Promise<{
12
+ verifier: string;
13
+ challenge: string;
14
+ method: "S256";
15
+ }>;
16
+ export interface AuthorizeParams {
17
+ issuer: string;
18
+ clientId: string;
19
+ redirectUri: string;
20
+ scopes: string[];
21
+ state: string;
22
+ codeChallenge: string;
23
+ /**
24
+ * Endpoint de autorização do IdP — obtenha via `discoverEndpoints(issuer)`
25
+ * para IdPs de terceiros. Default: `${issuer}/auth` (convenção oidc-provider).
26
+ */
27
+ authorizationEndpoint?: string;
28
+ /**
29
+ * Parâmetros extras anexados à URL de autorização (ex.: `audience`, `prompt`,
30
+ * `login_hint`, `ui_locales`, `acr_values`). Evita manipulação manual de URL no
31
+ * controller. Valores `undefined`/`null` são ignorados.
32
+ */
33
+ extraParams?: Record<string, string | number | undefined | null>;
34
+ }
35
+ export declare function buildAuthorizeUrl(p: AuthorizeParams): string;
36
+ export interface EndSessionParams {
37
+ /** Issuer do IdP (ex.: http://localhost:3333/oidc). */
38
+ issuer: string;
39
+ /** ID token atual da sessão; usado como id_token_hint para o IdP pular a confirmação. */
40
+ idToken?: string;
41
+ /** URI registrada no client onde o IdP devolve o browser após encerrar a sessão. */
42
+ postLogoutRedirectUri?: string;
43
+ /** Opcional; alguns IdPs exigem client_id quando não há id_token_hint. */
44
+ clientId?: string;
45
+ /** Opcional; ecoado de volta no post_logout_redirect_uri. */
46
+ state?: string;
47
+ /**
48
+ * Endpoint de end-session do IdP (via `discoverEndpoints`). Default:
49
+ * `${issuer}/session/end` (convenção oidc-provider).
50
+ */
51
+ endSessionEndpoint?: string;
52
+ }
53
+ /**
54
+ * Monta a URL de RP-initiated logout (OIDC) do IdP: `<issuer>/session/end`.
55
+ *
56
+ * Com `id_token_hint` + `post_logout_redirect_uri` registrada no client, o
57
+ * oidc-provider encerra a sessão SSO e redireciona o browser de volta. Sem
58
+ * o `id_token_hint`, o oidc-provider (v9) renderiza uma página de confirmação.
59
+ */
60
+ export declare function buildEndSessionUrl(p: EndSessionParams): string;
61
+ export interface ExchangeParams {
62
+ issuer: string;
63
+ clientId: string;
64
+ clientSecret?: string;
65
+ redirectUri: string;
66
+ code: string;
67
+ codeVerifier: string;
68
+ fetchImpl?: typeof fetch;
69
+ /** Token endpoint do IdP (via `discoverEndpoints`). Default: `${issuer}/token`. */
70
+ tokenEndpoint?: string;
71
+ /** Política de resiliência OPCIONAL p/ a chamada ao token endpoint. */
72
+ resilience?: ResiliencePolicy;
73
+ }
74
+ export declare function exchangeCode(p: ExchangeParams): Promise<TokenSet>;
75
+ export interface RefreshParams {
76
+ issuer: string;
77
+ clientId: string;
78
+ clientSecret?: string;
79
+ refreshToken: string;
80
+ /** Escopo opcional para reduzir o escopo do novo token. */
81
+ scope?: string;
82
+ fetchImpl?: typeof fetch;
83
+ /** Token endpoint do IdP (via `discoverEndpoints`). Default: `${issuer}/token`. */
84
+ tokenEndpoint?: string;
85
+ /** Política de resiliência OPCIONAL p/ a chamada ao token endpoint. */
86
+ resilience?: ResiliencePolicy;
87
+ }
88
+ /**
89
+ * Renova o TokenSet via `grant_type=refresh_token` (RFC 6749 §6).
90
+ *
91
+ * Com rotação habilitada no IdP (oidc-provider `rotateRefreshToken`), o endpoint
92
+ * devolve um NOVO refresh_token a cada uso e invalida o anterior — o chamador
93
+ * DEVE persistir o `refreshToken` retornado. Se o IdP não rotacionar, o campo
94
+ * pode vir vazio e o refresh token anterior continua válido.
95
+ */
96
+ export declare function refreshTokens(p: RefreshParams): Promise<TokenSet>;
97
+ export interface ExchangeTokenParams {
98
+ issuer: string;
99
+ clientId: string;
100
+ clientSecret?: string;
101
+ /** Access token atual do ator (admin). */
102
+ subjectToken: string;
103
+ /** Id do usuário-alvo a impersonar. */
104
+ requestedSubject: string;
105
+ scope?: string;
106
+ fetchImpl?: typeof fetch;
107
+ /** Token endpoint do IdP (via `discoverEndpoints`). Default: `${issuer}/token`. */
108
+ tokenEndpoint?: string;
109
+ /** Política de resiliência OPCIONAL p/ a chamada ao token endpoint. */
110
+ resilience?: ResiliencePolicy;
111
+ }
112
+ /** Troca o token do ator por um token do usuário-alvo (RFC 8693), p/ impersonation. */
113
+ export declare function exchangeToken(p: ExchangeTokenParams): Promise<TokenSet>;
@@ -0,0 +1,123 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { base64url } from "jose";
3
+ import { resilientFetch, } from "./http/resilient_fetch.js";
4
+ /**
5
+ * Gera um par PKCE (code_verifier + code_challenge) usando o método S256.
6
+ *
7
+ * Observação: o `jose` v5 instalado não exporta os helpers
8
+ * `generateRandomCodeVerifier`/`calculatePKCECodeChallenge`, então usamos o
9
+ * `crypto` do Node + `jose.base64url` para produzir valores equivalentes,
10
+ * conforme RFC 7636 (verifier aleatório base64url, challenge = base64url(SHA-256(verifier))).
11
+ */
12
+ export async function generatePkce() {
13
+ const verifier = base64url.encode(randomBytes(32));
14
+ const challenge = base64url.encode(createHash("sha256").update(verifier).digest());
15
+ return { verifier, challenge, method: "S256" };
16
+ }
17
+ export function buildAuthorizeUrl(p) {
18
+ const url = new URL(p.authorizationEndpoint ?? `${p.issuer}/auth`);
19
+ url.searchParams.set("client_id", p.clientId);
20
+ url.searchParams.set("response_type", "code");
21
+ url.searchParams.set("redirect_uri", p.redirectUri);
22
+ url.searchParams.set("scope", p.scopes.join(" "));
23
+ url.searchParams.set("state", p.state);
24
+ url.searchParams.set("code_challenge", p.codeChallenge);
25
+ url.searchParams.set("code_challenge_method", "S256");
26
+ if (p.extraParams) {
27
+ for (const [key, value] of Object.entries(p.extraParams)) {
28
+ if (value !== undefined && value !== null)
29
+ url.searchParams.set(key, String(value));
30
+ }
31
+ }
32
+ return url.toString();
33
+ }
34
+ /**
35
+ * Monta a URL de RP-initiated logout (OIDC) do IdP: `<issuer>/session/end`.
36
+ *
37
+ * Com `id_token_hint` + `post_logout_redirect_uri` registrada no client, o
38
+ * oidc-provider encerra a sessão SSO e redireciona o browser de volta. Sem
39
+ * o `id_token_hint`, o oidc-provider (v9) renderiza uma página de confirmação.
40
+ */
41
+ export function buildEndSessionUrl(p) {
42
+ const url = new URL(p.endSessionEndpoint ?? `${p.issuer}/session/end`);
43
+ if (p.idToken)
44
+ url.searchParams.set("id_token_hint", p.idToken);
45
+ if (p.postLogoutRedirectUri) {
46
+ url.searchParams.set("post_logout_redirect_uri", p.postLogoutRedirectUri);
47
+ }
48
+ if (p.clientId)
49
+ url.searchParams.set("client_id", p.clientId);
50
+ if (p.state)
51
+ url.searchParams.set("state", p.state);
52
+ return url.toString();
53
+ }
54
+ /**
55
+ * POST genérico ao token endpoint (`<issuer>/token`) compartilhado pelos fluxos
56
+ * authorization_code, refresh_token e token-exchange. Mapeia a resposta para um
57
+ * TokenSet padronizado e lança se o endpoint não responder 2xx.
58
+ */
59
+ async function tokenEndpoint(issuer, body, fetchImpl = fetch, endpoint, resilience) {
60
+ const res = await resilientFetch(endpoint ?? `${issuer}/token`, {
61
+ method: "POST",
62
+ headers: { "content-type": "application/x-www-form-urlencoded" },
63
+ body: body.toString(),
64
+ }, resilience, fetchImpl);
65
+ if (!res.ok)
66
+ throw new Error(`Token endpoint returned ${res.status}`);
67
+ const json = (await res.json());
68
+ return {
69
+ idToken: json.id_token ?? "",
70
+ accessToken: json.access_token,
71
+ refreshToken: json.refresh_token,
72
+ expiresAt: json.expires_in
73
+ ? Date.now() + json.expires_in * 1000
74
+ : undefined,
75
+ };
76
+ }
77
+ export async function exchangeCode(p) {
78
+ const body = new URLSearchParams({
79
+ grant_type: "authorization_code",
80
+ code: p.code,
81
+ redirect_uri: p.redirectUri,
82
+ client_id: p.clientId,
83
+ code_verifier: p.codeVerifier,
84
+ });
85
+ if (p.clientSecret)
86
+ body.set("client_secret", p.clientSecret);
87
+ return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
88
+ }
89
+ /**
90
+ * Renova o TokenSet via `grant_type=refresh_token` (RFC 6749 §6).
91
+ *
92
+ * Com rotação habilitada no IdP (oidc-provider `rotateRefreshToken`), o endpoint
93
+ * devolve um NOVO refresh_token a cada uso e invalida o anterior — o chamador
94
+ * DEVE persistir o `refreshToken` retornado. Se o IdP não rotacionar, o campo
95
+ * pode vir vazio e o refresh token anterior continua válido.
96
+ */
97
+ export async function refreshTokens(p) {
98
+ const body = new URLSearchParams({
99
+ grant_type: "refresh_token",
100
+ refresh_token: p.refreshToken,
101
+ client_id: p.clientId,
102
+ });
103
+ if (p.scope)
104
+ body.set("scope", p.scope);
105
+ if (p.clientSecret)
106
+ body.set("client_secret", p.clientSecret);
107
+ return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
108
+ }
109
+ /** Troca o token do ator por um token do usuário-alvo (RFC 8693), p/ impersonation. */
110
+ export async function exchangeToken(p) {
111
+ const body = new URLSearchParams({
112
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
113
+ subject_token: p.subjectToken,
114
+ subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
115
+ requested_subject: p.requestedSubject,
116
+ client_id: p.clientId,
117
+ });
118
+ if (p.scope)
119
+ body.set("scope", p.scope);
120
+ if (p.clientSecret)
121
+ body.set("client_secret", p.clientSecret);
122
+ return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
123
+ }
@@ -0,0 +1,50 @@
1
+ import type { Router } from '@adonisjs/core/http';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
+ import type { Identity } from '@adonis-agora/authkit-core';
4
+ /**
5
+ * Mapa role → rota de redirect pós-login. `byGlobalRole` checa as roles globais do
6
+ * IdP (claim); `byAppRole` checa `resolveAppRoles`. A primeira correspondência vence
7
+ * (global antes de app); sem match → `default`.
8
+ */
9
+ export interface PostLoginRedirects {
10
+ byGlobalRole?: Record<string, string>;
11
+ byAppRole?: Record<string, string>;
12
+ default?: string;
13
+ }
14
+ export interface RegisterOidcClientOptions {
15
+ /** Prefixo das rotas. Default `/auth` → `/auth/login`, `/auth/callback`, `/auth/logout`. */
16
+ prefix?: string;
17
+ /**
18
+ * Hook após troca de code bem-sucedida. Retorne uma STRING para redirecionar
19
+ * para uma rota específica; retorne void para cair em `redirects`/`/`.
20
+ */
21
+ afterLogin?: (ctx: HttpContext, identity: Identity | null) => Promise<string | void> | string | void;
22
+ /** Redirect pós-login por papel (usado quando `afterLogin` não retorna string). */
23
+ redirects?: PostLoginRedirects;
24
+ /** Destino pós-logout. Default: origem do redirectUri + '/'. */
25
+ postLogoutRedirect?: string | ((ctx: HttpContext) => string);
26
+ /** Registra também `POST {prefix}/backchannel-logout` (OIDC Back-Channel Logout). Default: true. */
27
+ backchannelLogout?: boolean;
28
+ /**
29
+ * Middleware(s) aplicados à rota de login (ex.: `middleware.guest()`). Array no
30
+ * formato aceito por `.use()` do AdonisJS.
31
+ */
32
+ loginMiddleware?: any;
33
+ /** Query param repassado como hint ao authorize (ex.: `audience`). Default: `['audience']`. */
34
+ passthroughParams?: string[];
35
+ }
36
+ /**
37
+ * Registra as rotas OIDC de CLIENT (login/callback/logout [+ back-channel logout])
38
+ * absorvendo o boilerplate que cada app reescrevia no OidcSessionController: PKCE +
39
+ * state na sessão, troca de code, validação, logout RP-initiated e redirect por papel.
40
+ *
41
+ * ```ts
42
+ * // start/routes.ts
43
+ * import { registerOidcClient } from '@adonis-agora/authkit-client'
44
+ * registerOidcClient(router, {
45
+ * loginMiddleware: middleware.guest(),
46
+ * redirects: { byGlobalRole: { ADMIN: '/admin' }, byAppRole: { ADVISOR: '/advisor' }, default: '/' },
47
+ * })
48
+ * ```
49
+ */
50
+ export declare function registerOidcClient(router: Router, options?: RegisterOidcClientOptions): void;