@adonis-agora/authkit-client 0.14.0 → 0.15.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/commands/configure.js +5 -3
- package/build/index.d.ts +32 -28
- package/build/index.js +18 -15
- package/build/providers/authkit_client_provider.d.ts +6 -6
- package/build/providers/authkit_client_provider.js +19 -15
- package/build/services/main.d.ts +1 -1
- package/build/services/main.js +2 -2
- package/build/src/authenticator.d.ts +3 -3
- package/build/src/authenticator.js +4 -4
- package/build/src/define_config.d.ts +5 -5
- package/build/src/define_config.js +5 -5
- package/build/src/discovery.d.ts +1 -1
- package/build/src/discovery.js +5 -5
- package/build/src/dpop.js +1 -1
- package/build/src/host/authkit_client_guard.d.ts +100 -0
- package/build/src/host/authkit_client_guard.js +153 -0
- package/build/src/host/request_authenticator.d.ts +31 -0
- package/build/src/host/request_authenticator.js +24 -0
- package/build/src/middleware/authkit_context_middleware.d.ts +28 -0
- package/build/src/middleware/authkit_context_middleware.js +30 -0
- package/build/src/middleware/backchannel_revocation_middleware.js +0 -1
- package/build/src/observability/context_bridge.d.ts +1 -1
- package/build/src/observability/context_bridge.js +8 -8
- package/build/src/oidc_login.d.ts +2 -2
- package/build/src/oidc_login.js +29 -31
- package/build/src/register_oidc_client.d.ts +1 -1
- package/build/src/register_oidc_client.js +1 -2
- package/build/src/resolvers/factory.js +2 -2
- package/build/src/resolvers/jwt_resolver.d.ts +1 -1
- package/build/src/resolvers/jwt_resolver.js +12 -1
- package/build/src/resolvers/opaque_resolver.d.ts +1 -1
- package/build/src/resolvers/pat_resolver.d.ts +1 -1
- package/build/src/revocation/revocation_store.js +0 -0
- package/build/types.d.ts +1 -1
- package/package.json +10 -3
- package/stubs/main.ts +2 -2
|
@@ -4,13 +4,15 @@ export async function configure(command) {
|
|
|
4
4
|
await codemods.makeUsingStub(stubsRoot, 'config/authkit_client.stub', {});
|
|
5
5
|
await codemods.makeUsingStub(stubsRoot, 'controllers/oidc_session_controller.stub', {});
|
|
6
6
|
await codemods.updateRcFile((rc) => rc.addProvider('@adonis-agora/authkit-client/authkit_client_provider'));
|
|
7
|
-
await codemods.registerMiddleware('router', [
|
|
7
|
+
await codemods.registerMiddleware('router', [
|
|
8
|
+
{ path: '@adonis-agora/authkit-client/authkit_middleware' },
|
|
9
|
+
]);
|
|
8
10
|
await codemods.defineEnvValidations({
|
|
9
11
|
leadingComment: 'Variáveis do @adonis-agora/authkit-client (OIDC client)',
|
|
10
12
|
variables: {
|
|
11
13
|
AUTHKIT_ISSUER: `Env.schema.string({ format: 'url', tld: false })`,
|
|
12
|
-
AUTHKIT_CLIENT_ID:
|
|
13
|
-
AUTHKIT_CLIENT_SECRET:
|
|
14
|
+
AUTHKIT_CLIENT_ID: 'Env.schema.string()',
|
|
15
|
+
AUTHKIT_CLIENT_SECRET: 'Env.schema.string.optional()',
|
|
14
16
|
AUTHKIT_REDIRECT_URI: `Env.schema.string({ format: 'url', tld: false })`,
|
|
15
17
|
},
|
|
16
18
|
});
|
package/build/index.d.ts
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
export { defineConfig, resolvers } from
|
|
2
|
-
export { Authenticator } from
|
|
3
|
-
export { identityToUser, createUserinfoResolver, } from
|
|
4
|
-
export type { ClaimsUser, ResolveUserContext, UserinfoResolverOptions, } from
|
|
5
|
-
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from
|
|
6
|
-
export type { EndSessionParams, RefreshParams } from
|
|
7
|
-
export { discoverEndpoints, conventionEndpoints } from
|
|
8
|
-
export type { OidcEndpoints, DiscoverOptions } from
|
|
9
|
-
export { resilientFetch } from
|
|
10
|
-
export type { ResiliencePolicy } from
|
|
11
|
-
export { AuthkitClientManager } from
|
|
12
|
-
export type { ResolvedClientConfig, ClientConfigInput, BackchannelLogoutCallback, BackchannelLogoutInput, } from
|
|
13
|
-
export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from
|
|
14
|
-
export type { RevocationStore, LucidRevocationStoreOptions, } from
|
|
15
|
-
export { default as BackchannelRevocationMiddleware } from
|
|
16
|
-
export {
|
|
17
|
-
export type {
|
|
18
|
-
export {
|
|
19
|
-
export
|
|
20
|
-
export {
|
|
21
|
-
export type {
|
|
22
|
-
export {
|
|
23
|
-
export type {
|
|
24
|
-
export {
|
|
25
|
-
export type {
|
|
26
|
-
export
|
|
27
|
-
export type {
|
|
28
|
-
export {
|
|
1
|
+
export { defineConfig, resolvers } from './src/define_config.js';
|
|
2
|
+
export { Authenticator } from './src/authenticator.js';
|
|
3
|
+
export { identityToUser, createUserinfoResolver, } from './src/user_resolvers.js';
|
|
4
|
+
export type { ClaimsUser, ResolveUserContext, UserinfoResolverOptions, } from './src/user_resolvers.js';
|
|
5
|
+
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from './src/oidc_login.js';
|
|
6
|
+
export type { EndSessionParams, RefreshParams } from './src/oidc_login.js';
|
|
7
|
+
export { discoverEndpoints, conventionEndpoints } from './src/discovery.js';
|
|
8
|
+
export type { OidcEndpoints, DiscoverOptions } from './src/discovery.js';
|
|
9
|
+
export { resilientFetch } from './src/http/resilient_fetch.js';
|
|
10
|
+
export type { ResiliencePolicy } from './src/http/resilient_fetch.js';
|
|
11
|
+
export { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
12
|
+
export type { ResolvedClientConfig, ClientConfigInput, BackchannelLogoutCallback, BackchannelLogoutInput, } from './src/define_config.js';
|
|
13
|
+
export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from './src/revocation/revocation_store.js';
|
|
14
|
+
export type { RevocationStore, LucidRevocationStoreOptions, } from './src/revocation/revocation_store.js';
|
|
15
|
+
export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
|
|
16
|
+
export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_client_guard.js';
|
|
17
|
+
export type { UnauthorizedAccessConstructor } from './src/host/authkit_client_guard.js';
|
|
18
|
+
export { getAuthkit } from './src/host/request_authenticator.js';
|
|
19
|
+
export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
|
|
20
|
+
export { lucidMirror } from './src/lucid_mirror.js';
|
|
21
|
+
export type { LucidMirrorOptions } from './src/lucid_mirror.js';
|
|
22
|
+
export { registerOidcClient } from './src/register_oidc_client.js';
|
|
23
|
+
export type { RegisterOidcClientOptions, PostLoginRedirects, } from './src/register_oidc_client.js';
|
|
24
|
+
export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from './src/backchannel_logout.js';
|
|
25
|
+
export type { ValidateLogoutTokenOptions, ValidatedLogoutToken, SessionIndex, SessionIndexEntry, } from './src/backchannel_logout.js';
|
|
26
|
+
export { generateDpopKeyPair, createDpopProof, dpopJwkThumbprint, } from './src/dpop.js';
|
|
27
|
+
export type { DpopKeyPair, CreateDpopProofInput } from './src/dpop.js';
|
|
28
|
+
export { verifyJwtAccessToken, clearJwksCache, } from './src/verify_access_token.js';
|
|
29
|
+
export type { VerifyJwtAccessTokenOptions, JwtAccessTokenClaims, } from './src/verify_access_token.js';
|
|
30
|
+
export type { TokenSet } from './src/types.js';
|
|
31
|
+
export type { Identity } from '@adonis-agora/authkit-core';
|
|
32
|
+
export { configure } from './commands/configure.js';
|
package/build/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
export { defineConfig, resolvers } from
|
|
2
|
-
export { Authenticator } from
|
|
3
|
-
export { identityToUser, createUserinfoResolver, } from
|
|
4
|
-
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from
|
|
5
|
-
export { discoverEndpoints, conventionEndpoints } from
|
|
6
|
-
export { resilientFetch } from
|
|
7
|
-
export { AuthkitClientManager } from
|
|
8
|
-
export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from
|
|
9
|
-
export { default as BackchannelRevocationMiddleware } from
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
1
|
+
export { defineConfig, resolvers } from './src/define_config.js';
|
|
2
|
+
export { Authenticator } from './src/authenticator.js';
|
|
3
|
+
export { identityToUser, createUserinfoResolver, } from './src/user_resolvers.js';
|
|
4
|
+
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from './src/oidc_login.js';
|
|
5
|
+
export { discoverEndpoints, conventionEndpoints } from './src/discovery.js';
|
|
6
|
+
export { resilientFetch } from './src/http/resilient_fetch.js';
|
|
7
|
+
export { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
8
|
+
export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from './src/revocation/revocation_store.js';
|
|
9
|
+
export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
|
|
10
|
+
export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_client_guard.js';
|
|
11
|
+
export { getAuthkit } from './src/host/request_authenticator.js';
|
|
12
|
+
export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
|
|
13
|
+
export { lucidMirror } from './src/lucid_mirror.js';
|
|
14
|
+
export { registerOidcClient } from './src/register_oidc_client.js';
|
|
15
|
+
export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from './src/backchannel_logout.js';
|
|
16
|
+
export { generateDpopKeyPair, createDpopProof, dpopJwkThumbprint, } from './src/dpop.js';
|
|
17
|
+
export { verifyJwtAccessToken, clearJwksCache, } from './src/verify_access_token.js';
|
|
18
|
+
export { configure } from './commands/configure.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { HttpContext } from
|
|
2
|
-
import type { ApplicationService } from
|
|
3
|
-
import { Authenticator } from
|
|
4
|
-
import type { ResolvedClientConfig } from
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
3
|
+
import { Authenticator } from '../src/authenticator.js';
|
|
4
|
+
import type { ResolvedClientConfig } from '../src/define_config.js';
|
|
5
5
|
/** Deps injetáveis (relógio/fetch) para testar `maybeRefresh` sem rede/tempo real. */
|
|
6
6
|
export interface RefreshDeps {
|
|
7
7
|
now?: () => number;
|
|
@@ -49,9 +49,9 @@ export declare class AuthkitClientManager {
|
|
|
49
49
|
isImpersonating(ctx: HttpContext): boolean;
|
|
50
50
|
createAuthenticator(ctx: HttpContext): Promise<Authenticator>;
|
|
51
51
|
}
|
|
52
|
-
declare module
|
|
52
|
+
declare module '@adonisjs/core/types' {
|
|
53
53
|
interface ContainerBindings {
|
|
54
|
-
|
|
54
|
+
'authkit.client': AuthkitClientManager;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
export default class AuthkitClientProvider {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { configProvider } from
|
|
2
|
-
import { RuntimeException } from
|
|
3
|
-
import { Authenticator } from
|
|
4
|
-
import { validateLogoutToken } from
|
|
5
|
-
import { exchangeToken, refreshTokens } from
|
|
1
|
+
import { configProvider } from '@adonisjs/core';
|
|
2
|
+
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
3
|
+
import { Authenticator } from '../src/authenticator.js';
|
|
4
|
+
import { validateLogoutToken } from '../src/backchannel_logout.js';
|
|
5
|
+
import { exchangeToken, refreshTokens } from '../src/oidc_login.js';
|
|
6
6
|
/** Margem (ms) antes do `expiresAt` em que o access token é renovado proativamente. */
|
|
7
7
|
const REFRESH_SKEW_MS = 60_000;
|
|
8
8
|
export class AuthkitClientManager {
|
|
@@ -84,10 +84,10 @@ export class AuthkitClientManager {
|
|
|
84
84
|
*/
|
|
85
85
|
async handleBackchannelLogout(ctx) {
|
|
86
86
|
const { request, response } = ctx;
|
|
87
|
-
response.header(
|
|
88
|
-
const token = request.input(
|
|
89
|
-
if (!token || typeof token !==
|
|
90
|
-
return response.badRequest({ error:
|
|
87
|
+
response.header('Cache-Control', 'no-store');
|
|
88
|
+
const token = request.input('logout_token');
|
|
89
|
+
if (!token || typeof token !== 'string') {
|
|
90
|
+
return response.badRequest({ error: 'invalid_request' });
|
|
91
91
|
}
|
|
92
92
|
let validated;
|
|
93
93
|
try {
|
|
@@ -97,7 +97,7 @@ export class AuthkitClientManager {
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
100
|
-
return response.badRequest({ error:
|
|
100
|
+
return response.badRequest({ error: 'invalid_request' });
|
|
101
101
|
}
|
|
102
102
|
// Atualiza o índice local (se configurado) — revoga as sessões mapeadas.
|
|
103
103
|
const index = this.config.sessionIndex;
|
|
@@ -126,9 +126,11 @@ export class AuthkitClientManager {
|
|
|
126
126
|
*/
|
|
127
127
|
async impersonate(ctx, requestedSubject) {
|
|
128
128
|
const session = ctx.session;
|
|
129
|
-
|
|
129
|
+
if (!session)
|
|
130
|
+
throw new Error('Sem sessão ativa para impersonar a partir dela');
|
|
131
|
+
const current = session.get(this.config.sessionKey);
|
|
130
132
|
if (!current?.accessToken) {
|
|
131
|
-
throw new Error(
|
|
133
|
+
throw new Error('Sem sessão ativa para impersonar a partir dela');
|
|
132
134
|
}
|
|
133
135
|
const impersonated = await exchangeToken({
|
|
134
136
|
issuer: this.config.issuer,
|
|
@@ -144,7 +146,9 @@ export class AuthkitClientManager {
|
|
|
144
146
|
/** Encerra a impersonação restaurando o token set original. Retorna false se não havia impersonação. */
|
|
145
147
|
async stopImpersonating(ctx) {
|
|
146
148
|
const session = ctx.session;
|
|
147
|
-
|
|
149
|
+
if (!session)
|
|
150
|
+
return false;
|
|
151
|
+
const original = session.get(this.#impersonatorKey);
|
|
148
152
|
if (!original)
|
|
149
153
|
return false;
|
|
150
154
|
session.put(this.config.sessionKey, original);
|
|
@@ -174,8 +178,8 @@ export default class AuthkitClientProvider {
|
|
|
174
178
|
this.app = app;
|
|
175
179
|
}
|
|
176
180
|
register() {
|
|
177
|
-
this.app.container.singleton(
|
|
178
|
-
const value = this.app.config.get(
|
|
181
|
+
this.app.container.singleton('authkit.client', async () => {
|
|
182
|
+
const value = this.app.config.get('authkit_client');
|
|
179
183
|
const config = (await configProvider.resolve(this.app, value));
|
|
180
184
|
if (!config) {
|
|
181
185
|
throw new RuntimeException('Config inválido em "config/authkit_client.ts". Use defineConfig de @adonis-agora/authkit-client.');
|
package/build/services/main.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthkitClientManager } from
|
|
1
|
+
import type { AuthkitClientManager } from '../providers/authkit_client_provider.js';
|
|
2
2
|
/**
|
|
3
3
|
* Singleton accessor for the {@link AuthkitClientManager}, following the Adonis `services/main`
|
|
4
4
|
* convention (like `db`, `mail`, `drive`). Lets an app write
|
package/build/services/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import app from
|
|
1
|
+
import app from '@adonisjs/core/services/app';
|
|
2
2
|
/**
|
|
3
3
|
* Singleton accessor for the {@link AuthkitClientManager}, following the Adonis `services/main`
|
|
4
4
|
* convention (like `db`, `mail`, `drive`). Lets an app write
|
|
@@ -8,6 +8,6 @@ import app from "@adonisjs/core/services/app";
|
|
|
8
8
|
*/
|
|
9
9
|
let manager;
|
|
10
10
|
await app.booted(async () => {
|
|
11
|
-
manager = await app.container.make(
|
|
11
|
+
manager = await app.container.make('authkit.client');
|
|
12
12
|
});
|
|
13
13
|
export { manager as default };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Identity, SessionResolver } from
|
|
2
|
-
import { type MetricsRecorder } from
|
|
3
|
-
import type { HttpContext } from
|
|
1
|
+
import type { Identity, SessionResolver } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { type MetricsRecorder } from '@adonis-agora/authkit-core';
|
|
3
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
4
4
|
/** Contexto opcional repassado a `resolveUser` (extensão backward-compatible). */
|
|
5
5
|
export interface ResolveUserContext {
|
|
6
6
|
accessToken?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AUTHKIT_METRICS, NoopRecorder
|
|
2
|
-
import { populateContext } from
|
|
1
|
+
import { AUTHKIT_METRICS, NoopRecorder } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { populateContext } from './observability/context_bridge.js';
|
|
3
3
|
/**
|
|
4
4
|
* Autenticador do request. O parâmetro `TUser` é o tipo do usuário do app que `resolveUser`
|
|
5
5
|
* devolve — o app o fixa via augmentation de `HttpContext.auth` (`auth: Authenticator<AppUser>`),
|
|
@@ -47,7 +47,7 @@ export class Authenticator {
|
|
|
47
47
|
async authenticate() {
|
|
48
48
|
const identity = await this.getIdentity();
|
|
49
49
|
if (!identity)
|
|
50
|
-
throw new Error(
|
|
50
|
+
throw new Error('Not authenticated');
|
|
51
51
|
return identity;
|
|
52
52
|
}
|
|
53
53
|
async check() {
|
|
@@ -84,7 +84,7 @@ export class Authenticator {
|
|
|
84
84
|
async getUserOrFail() {
|
|
85
85
|
const user = await this.getUser();
|
|
86
86
|
if (user === null) {
|
|
87
|
-
throw new Error(
|
|
87
|
+
throw new Error('Not authenticated: no user for the current session');
|
|
88
88
|
}
|
|
89
89
|
return user;
|
|
90
90
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Identity } from
|
|
2
|
-
import type { SessionIndex } from
|
|
3
|
-
import type { ResiliencePolicy } from
|
|
4
|
-
import { type ResolverFactory, resolvers } from
|
|
5
|
-
import type { RevocationStore } from
|
|
1
|
+
import type { Identity } from '@adonis-agora/authkit-core';
|
|
2
|
+
import type { SessionIndex } from './backchannel_logout.js';
|
|
3
|
+
import type { ResiliencePolicy } from './http/resilient_fetch.js';
|
|
4
|
+
import { type ResolverFactory, resolvers } from './resolvers/factory.js';
|
|
5
|
+
import type { RevocationStore } from './revocation/revocation_store.js';
|
|
6
6
|
export { resolvers };
|
|
7
7
|
/** Callback invocado quando um logout_token válido chega via Back-Channel Logout. */
|
|
8
8
|
export type BackchannelLogoutCallback = (event: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { configProvider } from
|
|
2
|
-
import { resolvers } from
|
|
1
|
+
import { configProvider } from '@adonisjs/core';
|
|
2
|
+
import { resolvers } from './resolvers/factory.js';
|
|
3
3
|
export { resolvers };
|
|
4
4
|
export function defineConfig(config) {
|
|
5
5
|
return configProvider.create(async () => {
|
|
@@ -22,9 +22,9 @@ export function defineConfig(config) {
|
|
|
22
22
|
redirectUri: config.redirectUri,
|
|
23
23
|
resolverFactory: config.resolver,
|
|
24
24
|
resolveUser: config.resolveUser,
|
|
25
|
-
sessionKey: config.sessionKey ??
|
|
26
|
-
scopes: config.scopes ?? [
|
|
27
|
-
globalRolesClaim: config.globalRolesClaim ??
|
|
25
|
+
sessionKey: config.sessionKey ?? 'authkit',
|
|
26
|
+
scopes: config.scopes ?? ['openid', 'profile', 'email', 'offline_access', 'roles'],
|
|
27
|
+
globalRolesClaim: config.globalRolesClaim ?? 'roles',
|
|
28
28
|
onBackchannelLogout,
|
|
29
29
|
sessionIndex: config.sessionIndex,
|
|
30
30
|
backchannelStore: store,
|
package/build/src/discovery.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* const tokens = await exchangeCode({ issuer, tokenEndpoint: endpoints.tokenEndpoint, ... })
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
|
-
import { type ResiliencePolicy } from
|
|
19
|
+
import { type ResiliencePolicy } from './http/resilient_fetch.js';
|
|
20
20
|
export interface OidcEndpoints {
|
|
21
21
|
authorizationEndpoint: string;
|
|
22
22
|
tokenEndpoint: string;
|
package/build/src/discovery.js
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* const tokens = await exchangeCode({ issuer, tokenEndpoint: endpoints.tokenEndpoint, ... })
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
|
-
import { resilientFetch
|
|
19
|
+
import { resilientFetch } from './http/resilient_fetch.js';
|
|
20
20
|
/**
|
|
21
21
|
* Convenção do oidc-provider (authkit-server) — usada como fallback quando o
|
|
22
22
|
* documento de discovery não está acessível.
|
|
23
23
|
*/
|
|
24
24
|
export function conventionEndpoints(issuer) {
|
|
25
|
-
const base = issuer.replace(/\/$/,
|
|
25
|
+
const base = issuer.replace(/\/$/, '');
|
|
26
26
|
return {
|
|
27
27
|
authorizationEndpoint: `${base}/auth`,
|
|
28
28
|
tokenEndpoint: `${base}/token`,
|
|
@@ -43,7 +43,7 @@ const cache = new Map();
|
|
|
43
43
|
* - `overrides` sempre vencem, campo a campo.
|
|
44
44
|
*/
|
|
45
45
|
export async function discoverEndpoints(issuer, options = {}) {
|
|
46
|
-
const base = issuer.replace(/\/$/,
|
|
46
|
+
const base = issuer.replace(/\/$/, '');
|
|
47
47
|
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
|
|
48
48
|
const cached = cache.get(base);
|
|
49
49
|
let endpoints;
|
|
@@ -66,11 +66,11 @@ export async function discoverEndpoints(issuer, options = {}) {
|
|
|
66
66
|
async function fetchDiscovery(base, fetchImpl, resilience) {
|
|
67
67
|
const fallback = conventionEndpoints(base);
|
|
68
68
|
try {
|
|
69
|
-
const res = await resilientFetch(`${base}/.well-known/openid-configuration`, { headers: { accept:
|
|
69
|
+
const res = await resilientFetch(`${base}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } }, resilience, fetchImpl);
|
|
70
70
|
if (!res.ok)
|
|
71
71
|
return fallback;
|
|
72
72
|
const doc = (await res.json());
|
|
73
|
-
const str = (v) => typeof v ===
|
|
73
|
+
const str = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
74
74
|
return {
|
|
75
75
|
authorizationEndpoint: str(doc.authorization_endpoint) ?? fallback.authorizationEndpoint,
|
|
76
76
|
tokenEndpoint: str(doc.token_endpoint) ?? fallback.tokenEndpoint,
|
package/build/src/dpop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SignJWT, exportJWK, generateKeyPair, calculateJwkThumbprint, base64url, } from 'jose';
|
|
2
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { SignJWT, base64url, calculateJwkThumbprint, exportJWK, generateKeyPair, } from 'jose';
|
|
3
3
|
/**
|
|
4
4
|
* Gera um par de chaves DPoP ES256 (P-256), exportável como JWK. ES256 é o alg
|
|
5
5
|
* recomendado pelo RFC 9449 e o que o oidc-provider do authkit-server aceita.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { symbols } from '@adonisjs/auth';
|
|
2
|
+
import type { AuthClientResponse, GuardConfigProvider } from '@adonisjs/auth/types';
|
|
3
|
+
import type { SessionUserProviderContract } from '@adonisjs/auth/types/session';
|
|
4
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
5
|
+
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
6
|
+
import type { Authenticator } from '../authenticator.js';
|
|
7
|
+
/**
|
|
8
|
+
* Construtor do `E_UNAUTHORIZED_ACCESS` do `@adonisjs/auth`, capturado no boot
|
|
9
|
+
* (ver {@link authkitClientGuard}) para que `getUserOrFail()` — que o contrato
|
|
10
|
+
* exige SÍNCRONO — possa lançá-lo sem um `await import()`.
|
|
11
|
+
*/
|
|
12
|
+
export type UnauthorizedAccessConstructor = new (message: string, options: {
|
|
13
|
+
guardDriverName: string;
|
|
14
|
+
redirectTo?: string;
|
|
15
|
+
}) => Error;
|
|
16
|
+
/** O tipo de usuário REAL por trás de um user provider do `@adonisjs/auth`. */
|
|
17
|
+
type RealUser<UserProvider> = UserProvider extends SessionUserProviderContract<infer User> ? User : never;
|
|
18
|
+
/**
|
|
19
|
+
* Guard de `@adonisjs/auth` apoiado na sessão OIDC do `@adonis-agora/authkit-client`.
|
|
20
|
+
*
|
|
21
|
+
* A fonte da verdade de "quem está logado" é o `SessionResolver` do authkit
|
|
22
|
+
* (JWT/opaco/PAT, conforme `config/authkit_client.ts`) — o guard NÃO reimplementa
|
|
23
|
+
* resolução de sessão nem lê a sessão por conta própria. Ele só faz a ponte:
|
|
24
|
+
* pega o `sub` da `Identity` (`identity.userId`) e pede o usuário do app ao
|
|
25
|
+
* `provider`, exatamente como o `sessionGuard` nativo faria com o id que ele
|
|
26
|
+
* guarda na sessão.
|
|
27
|
+
*
|
|
28
|
+
* Consequência: `middleware.auth()`, `ctx.auth.user`, `auth.check()` e o Bouncer
|
|
29
|
+
* passam a funcionar sobre uma sessão do authkit.
|
|
30
|
+
*/
|
|
31
|
+
export declare class AuthkitClientGuard<UserProvider extends SessionUserProviderContract<unknown>> {
|
|
32
|
+
#private;
|
|
33
|
+
/** Eventos do guard. Não emitimos nenhum (ainda) — o slot existe pro contrato. */
|
|
34
|
+
[symbols.GUARD_KNOWN_EVENTS]: unknown;
|
|
35
|
+
readonly driverName = "authkit_client";
|
|
36
|
+
authenticationAttempted: boolean;
|
|
37
|
+
isAuthenticated: boolean;
|
|
38
|
+
user?: RealUser<UserProvider>;
|
|
39
|
+
constructor(_name: string, ctx: HttpContext, userProvider: UserProvider, unauthorized: UnauthorizedAccessConstructor);
|
|
40
|
+
/**
|
|
41
|
+
* O {@link Authenticator} do authkit desta request (a `Identity` crua,
|
|
42
|
+
* `hasGlobalRole`, `toSharedProps`). Só existe depois de uma tentativa de
|
|
43
|
+
* autenticação; use `ctx.authkit` (via `authkit_context_middleware`) quando
|
|
44
|
+
* precisar dele em rota pública.
|
|
45
|
+
*/
|
|
46
|
+
get authkit(): Authenticator | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Autentica a request: resolve a sessão do authkit e mapeia o `sub` para o
|
|
49
|
+
* usuário do app via `provider.findById`. Lança `E_UNAUTHORIZED_ACCESS` (o
|
|
50
|
+
* MESMO erro dos guards nativos, então o `middleware.auth()` e o handler de
|
|
51
|
+
* exceção do host tratam igual) quando não há sessão OU quando a sessão é
|
|
52
|
+
* válida mas não existe usuário local correspondente (fail-closed).
|
|
53
|
+
*/
|
|
54
|
+
authenticate(): Promise<RealUser<UserProvider>>;
|
|
55
|
+
/** Igual aos guards nativos: `authenticate()` sem lançar. */
|
|
56
|
+
check(): Promise<boolean>;
|
|
57
|
+
/** O usuário autenticado, ou `E_UNAUTHORIZED_ACCESS`. */
|
|
58
|
+
getUserOrFail(): RealUser<UserProvider>;
|
|
59
|
+
/**
|
|
60
|
+
* Não implementado de propósito: a sessão deste guard é um TokenSet OIDC, e
|
|
61
|
+
* forjar um `id_token` assinado pelo IdP não é algo que a lib possa fazer a
|
|
62
|
+
* partir de um objeto de usuário. Use `@adonis-agora/authkit-testing`
|
|
63
|
+
* (`mintTestIdToken`) e plante o TokenSet na sessão, ou injete um
|
|
64
|
+
* `fakeAuthenticator()` em `ctx.authkit`.
|
|
65
|
+
*/
|
|
66
|
+
authenticateAsClient(): Promise<AuthClientResponse>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Configura o {@link AuthkitClientGuard} em `config/auth.ts`:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { defineConfig } from '@adonisjs/auth'
|
|
73
|
+
* import { sessionUserProvider } from '@adonisjs/auth/session'
|
|
74
|
+
* import { authkitClientGuard } from '@adonis-agora/authkit-client'
|
|
75
|
+
*
|
|
76
|
+
* export default defineConfig({
|
|
77
|
+
* default: 'web',
|
|
78
|
+
* guards: {
|
|
79
|
+
* web: authkitClientGuard({
|
|
80
|
+
* provider: sessionUserProvider({ model: () => import('#models/user') }),
|
|
81
|
+
* }),
|
|
82
|
+
* },
|
|
83
|
+
* })
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* O `provider` é o MESMO contrato do `sessionGuard` nativo, então qualquer user
|
|
87
|
+
* provider de session serve. O `findById` recebe `identity.userId` (a claim
|
|
88
|
+
* `sub`) — a coluna de id do model precisa ser essa (é o que o `lucidMirror()`
|
|
89
|
+
* grava).
|
|
90
|
+
*
|
|
91
|
+
* Precedência com `resolveUser` de `config/authkit_client.ts`: são superfícies
|
|
92
|
+
* SEPARADAS e ambas continuam valendo. `ctx.auth.user` (framework) vem SEMPRE do
|
|
93
|
+
* `provider`; `ctx.authkit.getUser()` (authkit) continua vindo do `resolveUser`.
|
|
94
|
+
* O guard nunca chama `resolveUser`. Com o guard configurado, o normal é apagar
|
|
95
|
+
* o `resolveUser` e deixar só o `provider`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function authkitClientGuard<UserProvider extends SessionUserProviderContract<unknown>>(config: {
|
|
98
|
+
provider: UserProvider | ConfigProvider<UserProvider>;
|
|
99
|
+
}): GuardConfigProvider<(ctx: HttpContext) => AuthkitClientGuard<UserProvider>>;
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
2
|
+
import { getAuthkit } from './request_authenticator.js';
|
|
3
|
+
/**
|
|
4
|
+
* Guard de `@adonisjs/auth` apoiado na sessão OIDC do `@adonis-agora/authkit-client`.
|
|
5
|
+
*
|
|
6
|
+
* A fonte da verdade de "quem está logado" é o `SessionResolver` do authkit
|
|
7
|
+
* (JWT/opaco/PAT, conforme `config/authkit_client.ts`) — o guard NÃO reimplementa
|
|
8
|
+
* resolução de sessão nem lê a sessão por conta própria. Ele só faz a ponte:
|
|
9
|
+
* pega o `sub` da `Identity` (`identity.userId`) e pede o usuário do app ao
|
|
10
|
+
* `provider`, exatamente como o `sessionGuard` nativo faria com o id que ele
|
|
11
|
+
* guarda na sessão.
|
|
12
|
+
*
|
|
13
|
+
* Consequência: `middleware.auth()`, `ctx.auth.user`, `auth.check()` e o Bouncer
|
|
14
|
+
* passam a funcionar sobre uma sessão do authkit.
|
|
15
|
+
*/
|
|
16
|
+
export class AuthkitClientGuard {
|
|
17
|
+
#ctx;
|
|
18
|
+
#userProvider;
|
|
19
|
+
#unauthorized;
|
|
20
|
+
#authkit;
|
|
21
|
+
driverName = 'authkit_client';
|
|
22
|
+
authenticationAttempted = false;
|
|
23
|
+
isAuthenticated = false;
|
|
24
|
+
user;
|
|
25
|
+
constructor(_name, ctx, userProvider, unauthorized) {
|
|
26
|
+
this.#ctx = ctx;
|
|
27
|
+
this.#userProvider = userProvider;
|
|
28
|
+
this.#unauthorized = unauthorized;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* O {@link Authenticator} do authkit desta request (a `Identity` crua,
|
|
32
|
+
* `hasGlobalRole`, `toSharedProps`). Só existe depois de uma tentativa de
|
|
33
|
+
* autenticação; use `ctx.authkit` (via `authkit_context_middleware`) quando
|
|
34
|
+
* precisar dele em rota pública.
|
|
35
|
+
*/
|
|
36
|
+
get authkit() {
|
|
37
|
+
return this.#authkit;
|
|
38
|
+
}
|
|
39
|
+
#failed() {
|
|
40
|
+
this.isAuthenticated = false;
|
|
41
|
+
this.user = undefined;
|
|
42
|
+
return new this.#unauthorized('Invalid or expired user session', {
|
|
43
|
+
guardDriverName: this.driverName,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Autentica a request: resolve a sessão do authkit e mapeia o `sub` para o
|
|
48
|
+
* usuário do app via `provider.findById`. Lança `E_UNAUTHORIZED_ACCESS` (o
|
|
49
|
+
* MESMO erro dos guards nativos, então o `middleware.auth()` e o handler de
|
|
50
|
+
* exceção do host tratam igual) quando não há sessão OU quando a sessão é
|
|
51
|
+
* válida mas não existe usuário local correspondente (fail-closed).
|
|
52
|
+
*/
|
|
53
|
+
async authenticate() {
|
|
54
|
+
if (this.authenticationAttempted)
|
|
55
|
+
return this.getUserOrFail();
|
|
56
|
+
this.authenticationAttempted = true;
|
|
57
|
+
const authkit = await getAuthkit(this.#ctx);
|
|
58
|
+
this.#authkit = authkit;
|
|
59
|
+
const identity = await authkit.getIdentity();
|
|
60
|
+
if (!identity)
|
|
61
|
+
throw this.#failed();
|
|
62
|
+
// `identity.userId` É a claim `sub` — o único id estável por conta que a
|
|
63
|
+
// `Identity` tem. (Ela NÃO tem `id`.)
|
|
64
|
+
const providerUser = await this.#userProvider.findById(identity.userId);
|
|
65
|
+
if (!providerUser)
|
|
66
|
+
throw this.#failed();
|
|
67
|
+
this.user = providerUser.getOriginal();
|
|
68
|
+
this.isAuthenticated = true;
|
|
69
|
+
return this.user;
|
|
70
|
+
}
|
|
71
|
+
/** Igual aos guards nativos: `authenticate()` sem lançar. */
|
|
72
|
+
async check() {
|
|
73
|
+
try {
|
|
74
|
+
await this.authenticate();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof this.#unauthorized)
|
|
79
|
+
return false;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** O usuário autenticado, ou `E_UNAUTHORIZED_ACCESS`. */
|
|
84
|
+
getUserOrFail() {
|
|
85
|
+
if (!this.user) {
|
|
86
|
+
throw new this.#unauthorized('Invalid or expired user session', {
|
|
87
|
+
guardDriverName: this.driverName,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return this.user;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Não implementado de propósito: a sessão deste guard é um TokenSet OIDC, e
|
|
94
|
+
* forjar um `id_token` assinado pelo IdP não é algo que a lib possa fazer a
|
|
95
|
+
* partir de um objeto de usuário. Use `@adonis-agora/authkit-testing`
|
|
96
|
+
* (`mintTestIdToken`) e plante o TokenSet na sessão, ou injete um
|
|
97
|
+
* `fakeAuthenticator()` em `ctx.authkit`.
|
|
98
|
+
*/
|
|
99
|
+
async authenticateAsClient() {
|
|
100
|
+
throw new RuntimeException('authkitClientGuard não suporta "loginAs" nos plugins de teste: a sessão é um TokenSet OIDC ' +
|
|
101
|
+
'assinado pelo IdP. Use mintTestIdToken() de @adonis-agora/authkit-testing e plante o ' +
|
|
102
|
+
'TokenSet na sessão, ou injete fakeAuthenticator() em ctx.authkit.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Configura o {@link AuthkitClientGuard} em `config/auth.ts`:
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* import { defineConfig } from '@adonisjs/auth'
|
|
110
|
+
* import { sessionUserProvider } from '@adonisjs/auth/session'
|
|
111
|
+
* import { authkitClientGuard } from '@adonis-agora/authkit-client'
|
|
112
|
+
*
|
|
113
|
+
* export default defineConfig({
|
|
114
|
+
* default: 'web',
|
|
115
|
+
* guards: {
|
|
116
|
+
* web: authkitClientGuard({
|
|
117
|
+
* provider: sessionUserProvider({ model: () => import('#models/user') }),
|
|
118
|
+
* }),
|
|
119
|
+
* },
|
|
120
|
+
* })
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* O `provider` é o MESMO contrato do `sessionGuard` nativo, então qualquer user
|
|
124
|
+
* provider de session serve. O `findById` recebe `identity.userId` (a claim
|
|
125
|
+
* `sub`) — a coluna de id do model precisa ser essa (é o que o `lucidMirror()`
|
|
126
|
+
* grava).
|
|
127
|
+
*
|
|
128
|
+
* Precedência com `resolveUser` de `config/authkit_client.ts`: são superfícies
|
|
129
|
+
* SEPARADAS e ambas continuam valendo. `ctx.auth.user` (framework) vem SEMPRE do
|
|
130
|
+
* `provider`; `ctx.authkit.getUser()` (authkit) continua vindo do `resolveUser`.
|
|
131
|
+
* O guard nunca chama `resolveUser`. Com o guard configurado, o normal é apagar
|
|
132
|
+
* o `resolveUser` e deixar só o `provider`.
|
|
133
|
+
*/
|
|
134
|
+
export function authkitClientGuard(config) {
|
|
135
|
+
return {
|
|
136
|
+
async resolver(name, app) {
|
|
137
|
+
let unauthorized;
|
|
138
|
+
try {
|
|
139
|
+
const auth = (await import('@adonisjs/auth'));
|
|
140
|
+
unauthorized = auth.errors.E_UNAUTHORIZED_ACCESS;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw new RuntimeException('authkitClientGuard() precisa de "@adonisjs/auth" instalado (é um peer opcional do ' +
|
|
144
|
+
'@adonis-agora/authkit-client, só necessário se você plugar este guard em config/auth.ts). ' +
|
|
145
|
+
'Rode `npm i @adonisjs/auth` (ou pnpm/yarn).', { cause: error });
|
|
146
|
+
}
|
|
147
|
+
const provider = 'resolver' in config.provider
|
|
148
|
+
? (await config.provider.resolver(app))
|
|
149
|
+
: config.provider;
|
|
150
|
+
return (ctx) => new AuthkitClientGuard(name, ctx, provider, unauthorized);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { Authenticator } from '../authenticator.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve (uma vez por request) o {@link Authenticator} do authkit e o memoiza
|
|
5
|
+
* em `ctx.authkit`.
|
|
6
|
+
*
|
|
7
|
+
* É o ÚNICO caminho de resolução de sessão da lib: passa pelo
|
|
8
|
+
* `AuthkitClientManager`, então o refresh proativo do token (`maybeRefresh`), o
|
|
9
|
+
* `SessionResolver` configurado, as métricas e a ponte do `@agora/context`
|
|
10
|
+
* continuam valendo — tanto para quem usa `authkit_middleware` (legado) quanto
|
|
11
|
+
* para quem usa o `authkitClientGuard` de `@adonisjs/auth`.
|
|
12
|
+
*
|
|
13
|
+
* Memoiza em `ctx.authkit` de propósito: quando o host registra o
|
|
14
|
+
* `authkit_context_middleware` E o guard, os dois compartilham a MESMA
|
|
15
|
+
* instância, e a sessão é resolvida (e o token renovado) uma vez só.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getAuthkit(ctx: HttpContext): Promise<Authenticator>;
|
|
18
|
+
declare module '@adonisjs/core/http' {
|
|
19
|
+
interface HttpContext {
|
|
20
|
+
/**
|
|
21
|
+
* Superfície específica do authkit (`getIdentity`, `hasGlobalRole`, a
|
|
22
|
+
* `Identity` crua). Populada pelo `authkit_context_middleware` e, no
|
|
23
|
+
* caminho do guard, pelo próprio `authkitClientGuard` ao autenticar.
|
|
24
|
+
*
|
|
25
|
+
* Existe para que o `@adonisjs/auth` possa ser o dono de `ctx.auth` sem que
|
|
26
|
+
* o host perca o que só o authkit sabe. Quem NÃO usa o guard continua com o
|
|
27
|
+
* `Authenticator` do authkit direto em `ctx.auth` (ver `types.ts`).
|
|
28
|
+
*/
|
|
29
|
+
authkit: Authenticator;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve (uma vez por request) o {@link Authenticator} do authkit e o memoiza
|
|
3
|
+
* em `ctx.authkit`.
|
|
4
|
+
*
|
|
5
|
+
* É o ÚNICO caminho de resolução de sessão da lib: passa pelo
|
|
6
|
+
* `AuthkitClientManager`, então o refresh proativo do token (`maybeRefresh`), o
|
|
7
|
+
* `SessionResolver` configurado, as métricas e a ponte do `@agora/context`
|
|
8
|
+
* continuam valendo — tanto para quem usa `authkit_middleware` (legado) quanto
|
|
9
|
+
* para quem usa o `authkitClientGuard` de `@adonisjs/auth`.
|
|
10
|
+
*
|
|
11
|
+
* Memoiza em `ctx.authkit` de propósito: quando o host registra o
|
|
12
|
+
* `authkit_context_middleware` E o guard, os dois compartilham a MESMA
|
|
13
|
+
* instância, e a sessão é resolvida (e o token renovado) uma vez só.
|
|
14
|
+
*/
|
|
15
|
+
export async function getAuthkit(ctx) {
|
|
16
|
+
const existing = ctx.authkit;
|
|
17
|
+
if (existing)
|
|
18
|
+
return existing;
|
|
19
|
+
const manager = await ctx.containerResolver.make('authkit.client');
|
|
20
|
+
await manager.maybeRefresh(ctx);
|
|
21
|
+
const authenticator = await manager.createAuthenticator(ctx);
|
|
22
|
+
ctx.authkit = authenticator;
|
|
23
|
+
return authenticator;
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { NextFn } from '@adonisjs/core/types/http';
|
|
3
|
+
/**
|
|
4
|
+
* Popula `ctx.authkit` (o `Authenticator` do authkit) e renova o token
|
|
5
|
+
* proativamente — SEM tocar em `ctx.auth`.
|
|
6
|
+
*
|
|
7
|
+
* É o par do `authkitClientGuard`: quem adota o guard entrega `ctx.auth` ao
|
|
8
|
+
* `@adonisjs/auth` (via `@adonisjs/auth/initialize_auth_middleware`) e registra
|
|
9
|
+
* ESTE middleware para continuar alcançando a superfície específica do authkit
|
|
10
|
+
* (`hasGlobalRole`, a `Identity` crua, `toSharedProps`) em qualquer rota,
|
|
11
|
+
* inclusive nas públicas.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* // start/kernel.ts
|
|
15
|
+
* router.use([
|
|
16
|
+
* () => import('@adonisjs/auth/initialize_auth_middleware'),
|
|
17
|
+
* () => import('@adonis-agora/authkit-client/authkit_context_middleware'),
|
|
18
|
+
* ])
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Não substitui o `authkit_middleware`: quem NÃO usa `@adonisjs/auth` continua
|
|
22
|
+
* com o `authkit_middleware`, que segue colocando o `Authenticator` em
|
|
23
|
+
* `ctx.auth` como sempre. Registrar os dois não faz sentido (e resolveria a
|
|
24
|
+
* sessão duas vezes em objetos distintos).
|
|
25
|
+
*/
|
|
26
|
+
export default class AuthkitContextMiddleware {
|
|
27
|
+
handle(ctx: HttpContext, next: NextFn): Promise<any>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getAuthkit } from '../host/request_authenticator.js';
|
|
2
|
+
/**
|
|
3
|
+
* Popula `ctx.authkit` (o `Authenticator` do authkit) e renova o token
|
|
4
|
+
* proativamente — SEM tocar em `ctx.auth`.
|
|
5
|
+
*
|
|
6
|
+
* É o par do `authkitClientGuard`: quem adota o guard entrega `ctx.auth` ao
|
|
7
|
+
* `@adonisjs/auth` (via `@adonisjs/auth/initialize_auth_middleware`) e registra
|
|
8
|
+
* ESTE middleware para continuar alcançando a superfície específica do authkit
|
|
9
|
+
* (`hasGlobalRole`, a `Identity` crua, `toSharedProps`) em qualquer rota,
|
|
10
|
+
* inclusive nas públicas.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // start/kernel.ts
|
|
14
|
+
* router.use([
|
|
15
|
+
* () => import('@adonisjs/auth/initialize_auth_middleware'),
|
|
16
|
+
* () => import('@adonis-agora/authkit-client/authkit_context_middleware'),
|
|
17
|
+
* ])
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Não substitui o `authkit_middleware`: quem NÃO usa `@adonisjs/auth` continua
|
|
21
|
+
* com o `authkit_middleware`, que segue colocando o `Authenticator` em
|
|
22
|
+
* `ctx.auth` como sempre. Registrar os dois não faz sentido (e resolveria a
|
|
23
|
+
* sessão duas vezes em objetos distintos).
|
|
24
|
+
*/
|
|
25
|
+
export default class AuthkitContextMiddleware {
|
|
26
|
+
async handle(ctx, next) {
|
|
27
|
+
await getAuthkit(ctx);
|
|
28
|
+
return next();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -26,7 +26,6 @@ export default class BackchannelRevocationMiddleware {
|
|
|
26
26
|
const authTime = typeof claims?.iat === 'number' ? claims.iat : undefined;
|
|
27
27
|
if ((sid || sub) && (await store.isRevoked({ sid, sub, authTime }))) {
|
|
28
28
|
// session é augmentado por @adonisjs/session no app host; na lib usamos cast.
|
|
29
|
-
;
|
|
30
29
|
ctx.session?.forget(sessionKey);
|
|
31
30
|
}
|
|
32
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Identity } from
|
|
1
|
+
import type { Identity } from '@adonis-agora/authkit-core';
|
|
2
2
|
/**
|
|
3
3
|
* Popula o contexto do Agora a partir de uma identidade recém-resolvida:
|
|
4
4
|
* `userRef` ({ type: 'user', id: userId }), os `globalRoles` da identidade e,
|
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
* nunca importa `@adonis-agora/context` — e degrada para no-op quando o context
|
|
5
5
|
* não está instalado. Mesmo idioma da `diagnostics_bridge`.
|
|
6
6
|
*/
|
|
7
|
-
const SET_SLOT = Symbol.for(
|
|
7
|
+
const SET_SLOT = Symbol.for('@agora/context:set');
|
|
8
8
|
/**
|
|
9
9
|
* Claims OIDC candidatas a "organização/tenant ativo". A primeira presente (e
|
|
10
10
|
* string não vazia) vira o `tenantId`. Não falha se nenhuma existir.
|
|
11
11
|
*/
|
|
12
12
|
const TENANT_CLAIMS = [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
'active_organization_id',
|
|
14
|
+
'org_id',
|
|
15
|
+
'organization_id',
|
|
16
|
+
'tenant_id',
|
|
17
|
+
'tid',
|
|
18
18
|
];
|
|
19
19
|
/** Deriva o tenant ativo das claims cruas, ou `undefined` quando ausente. */
|
|
20
20
|
function deriveTenant(raw) {
|
|
@@ -22,7 +22,7 @@ function deriveTenant(raw) {
|
|
|
22
22
|
return undefined;
|
|
23
23
|
for (const claim of TENANT_CLAIMS) {
|
|
24
24
|
const value = raw[claim];
|
|
25
|
-
if (typeof value ===
|
|
25
|
+
if (typeof value === 'string' && value.length > 0)
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
28
|
return undefined;
|
|
@@ -40,7 +40,7 @@ export function populateContext(identity) {
|
|
|
40
40
|
return;
|
|
41
41
|
const tenant = deriveTenant(identity.raw);
|
|
42
42
|
set({
|
|
43
|
-
userRef: { type:
|
|
43
|
+
userRef: { type: 'user', id: identity.userId },
|
|
44
44
|
globalRoles: identity.globalRoles,
|
|
45
45
|
...(tenant ? { tenantId: tenant } : {}),
|
|
46
46
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { type ResiliencePolicy } from './http/resilient_fetch.js';
|
|
2
|
+
import type { TokenSet } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Gera um par PKCE (code_verifier + code_challenge) usando o método S256.
|
|
5
5
|
*
|
package/build/src/oidc_login.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createHash, randomBytes } from
|
|
2
|
-
import { base64url } from
|
|
3
|
-
import { resilientFetch
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { base64url } from 'jose';
|
|
3
|
+
import { resilientFetch } from './http/resilient_fetch.js';
|
|
4
4
|
/**
|
|
5
5
|
* Gera um par PKCE (code_verifier + code_challenge) usando o método S256.
|
|
6
6
|
*
|
|
@@ -11,18 +11,18 @@ import { resilientFetch, } from "./http/resilient_fetch.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export async function generatePkce() {
|
|
13
13
|
const verifier = base64url.encode(randomBytes(32));
|
|
14
|
-
const challenge = base64url.encode(createHash(
|
|
15
|
-
return { verifier, challenge, method:
|
|
14
|
+
const challenge = base64url.encode(createHash('sha256').update(verifier).digest());
|
|
15
|
+
return { verifier, challenge, method: 'S256' };
|
|
16
16
|
}
|
|
17
17
|
export function buildAuthorizeUrl(p) {
|
|
18
18
|
const url = new URL(p.authorizationEndpoint ?? `${p.issuer}/auth`);
|
|
19
|
-
url.searchParams.set(
|
|
20
|
-
url.searchParams.set(
|
|
21
|
-
url.searchParams.set(
|
|
22
|
-
url.searchParams.set(
|
|
23
|
-
url.searchParams.set(
|
|
24
|
-
url.searchParams.set(
|
|
25
|
-
url.searchParams.set(
|
|
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
26
|
if (p.extraParams) {
|
|
27
27
|
for (const [key, value] of Object.entries(p.extraParams)) {
|
|
28
28
|
if (value !== undefined && value !== null)
|
|
@@ -41,14 +41,14 @@ export function buildAuthorizeUrl(p) {
|
|
|
41
41
|
export function buildEndSessionUrl(p) {
|
|
42
42
|
const url = new URL(p.endSessionEndpoint ?? `${p.issuer}/session/end`);
|
|
43
43
|
if (p.idToken)
|
|
44
|
-
url.searchParams.set(
|
|
44
|
+
url.searchParams.set('id_token_hint', p.idToken);
|
|
45
45
|
if (p.postLogoutRedirectUri) {
|
|
46
|
-
url.searchParams.set(
|
|
46
|
+
url.searchParams.set('post_logout_redirect_uri', p.postLogoutRedirectUri);
|
|
47
47
|
}
|
|
48
48
|
if (p.clientId)
|
|
49
|
-
url.searchParams.set(
|
|
49
|
+
url.searchParams.set('client_id', p.clientId);
|
|
50
50
|
if (p.state)
|
|
51
|
-
url.searchParams.set(
|
|
51
|
+
url.searchParams.set('state', p.state);
|
|
52
52
|
return url.toString();
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
@@ -58,32 +58,30 @@ export function buildEndSessionUrl(p) {
|
|
|
58
58
|
*/
|
|
59
59
|
async function tokenEndpoint(issuer, body, fetchImpl = fetch, endpoint, resilience) {
|
|
60
60
|
const res = await resilientFetch(endpoint ?? `${issuer}/token`, {
|
|
61
|
-
method:
|
|
62
|
-
headers: {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
63
63
|
body: body.toString(),
|
|
64
64
|
}, resilience, fetchImpl);
|
|
65
65
|
if (!res.ok)
|
|
66
66
|
throw new Error(`Token endpoint returned ${res.status}`);
|
|
67
67
|
const json = (await res.json());
|
|
68
68
|
return {
|
|
69
|
-
idToken: json.id_token ??
|
|
69
|
+
idToken: json.id_token ?? '',
|
|
70
70
|
accessToken: json.access_token,
|
|
71
71
|
refreshToken: json.refresh_token,
|
|
72
|
-
expiresAt: json.expires_in
|
|
73
|
-
? Date.now() + json.expires_in * 1000
|
|
74
|
-
: undefined,
|
|
72
|
+
expiresAt: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined,
|
|
75
73
|
};
|
|
76
74
|
}
|
|
77
75
|
export async function exchangeCode(p) {
|
|
78
76
|
const body = new URLSearchParams({
|
|
79
|
-
grant_type:
|
|
77
|
+
grant_type: 'authorization_code',
|
|
80
78
|
code: p.code,
|
|
81
79
|
redirect_uri: p.redirectUri,
|
|
82
80
|
client_id: p.clientId,
|
|
83
81
|
code_verifier: p.codeVerifier,
|
|
84
82
|
});
|
|
85
83
|
if (p.clientSecret)
|
|
86
|
-
body.set(
|
|
84
|
+
body.set('client_secret', p.clientSecret);
|
|
87
85
|
return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
|
|
88
86
|
}
|
|
89
87
|
/**
|
|
@@ -96,28 +94,28 @@ export async function exchangeCode(p) {
|
|
|
96
94
|
*/
|
|
97
95
|
export async function refreshTokens(p) {
|
|
98
96
|
const body = new URLSearchParams({
|
|
99
|
-
grant_type:
|
|
97
|
+
grant_type: 'refresh_token',
|
|
100
98
|
refresh_token: p.refreshToken,
|
|
101
99
|
client_id: p.clientId,
|
|
102
100
|
});
|
|
103
101
|
if (p.scope)
|
|
104
|
-
body.set(
|
|
102
|
+
body.set('scope', p.scope);
|
|
105
103
|
if (p.clientSecret)
|
|
106
|
-
body.set(
|
|
104
|
+
body.set('client_secret', p.clientSecret);
|
|
107
105
|
return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
|
|
108
106
|
}
|
|
109
107
|
/** Troca o token do ator por um token do usuário-alvo (RFC 8693), p/ impersonation. */
|
|
110
108
|
export async function exchangeToken(p) {
|
|
111
109
|
const body = new URLSearchParams({
|
|
112
|
-
grant_type:
|
|
110
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
113
111
|
subject_token: p.subjectToken,
|
|
114
|
-
subject_token_type:
|
|
112
|
+
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
|
115
113
|
requested_subject: p.requestedSubject,
|
|
116
114
|
client_id: p.clientId,
|
|
117
115
|
});
|
|
118
116
|
if (p.scope)
|
|
119
|
-
body.set(
|
|
117
|
+
body.set('scope', p.scope);
|
|
120
118
|
if (p.clientSecret)
|
|
121
|
-
body.set(
|
|
119
|
+
body.set('client_secret', p.clientSecret);
|
|
122
120
|
return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
|
|
123
121
|
}
|
|
@@ -17,7 +17,7 @@ export interface RegisterOidcClientOptions {
|
|
|
17
17
|
* Hook após troca de code bem-sucedida. Retorne uma STRING para redirecionar
|
|
18
18
|
* para uma rota específica; retorne void para cair em `redirects`/`/`.
|
|
19
19
|
*/
|
|
20
|
-
afterLogin?: (ctx: HttpContext, identity: Identity | null) => Promise<string |
|
|
20
|
+
afterLogin?: (ctx: HttpContext, identity: Identity | null) => Promise<string | undefined> | string | undefined;
|
|
21
21
|
/** Redirect pós-login por papel (usado quando `afterLogin` não retorna string). */
|
|
22
22
|
redirects?: PostLoginRedirects;
|
|
23
23
|
/** Destino pós-logout. Default: origem do redirectUri + '/'. */
|
|
@@ -52,7 +52,6 @@ export function registerOidcClient(router, options = {}) {
|
|
|
52
52
|
const pkce = ctx.session?.get(pkceKey);
|
|
53
53
|
// Callback velho/expirado (state perdido ou code ausente) → recomeça o login.
|
|
54
54
|
if (!pkce || pkce.state !== state || !code) {
|
|
55
|
-
;
|
|
56
55
|
ctx.session?.forget(pkceKey);
|
|
57
56
|
return ctx.response.redirect(`${prefix}/login`);
|
|
58
57
|
}
|
|
@@ -85,7 +84,7 @@ export function registerOidcClient(router, options = {}) {
|
|
|
85
84
|
ctx.session?.forget(cfg.sessionKey);
|
|
86
85
|
const postLogoutRedirectUri = typeof options.postLogoutRedirect === 'function'
|
|
87
86
|
? options.postLogoutRedirect(ctx)
|
|
88
|
-
: (options.postLogoutRedirect ?? new URL(cfg.redirectUri).origin
|
|
87
|
+
: (options.postLogoutRedirect ?? `${new URL(cfg.redirectUri).origin}/`);
|
|
89
88
|
const endSessionUrl = buildEndSessionUrl({
|
|
90
89
|
issuer: cfg.issuer,
|
|
91
90
|
idToken,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { getTokenFromSource } from '../token_source.js';
|
|
1
2
|
import { JwtResolver } from './jwt_resolver.js';
|
|
2
|
-
import { PatResolver } from './pat_resolver.js';
|
|
3
3
|
import { OpaqueResolver } from './opaque_resolver.js';
|
|
4
|
-
import {
|
|
4
|
+
import { PatResolver } from './pat_resolver.js';
|
|
5
5
|
export const resolvers = {
|
|
6
6
|
jwt(config = {}) {
|
|
7
7
|
const tokenSource = config.tokenSource ?? 'session';
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
2
|
import { buildIdentityFromClaims } from './identity.js';
|
|
3
|
-
const DEFAULT_ALGS = [
|
|
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
|
+
];
|
|
4
15
|
export class JwtResolver {
|
|
5
16
|
config;
|
|
6
17
|
#jwks;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
1
|
import type { Identity, SessionResolver } from '@adonis-agora/authkit-core';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
3
|
import { type TokenSource } from '../token_source.js';
|
|
4
4
|
type FetchImpl = (url: string, init: any) => Promise<{
|
|
5
5
|
ok: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
1
|
import type { Identity, SessionResolver } from '@adonis-agora/authkit-core';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
3
|
type FetchImpl = (url: string, init: any) => Promise<{
|
|
4
4
|
ok: boolean;
|
|
5
5
|
json: () => Promise<any>;
|
|
Binary file
|
package/build/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Authenticator } from './src/authenticator.js';
|
|
2
1
|
import type { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
2
|
+
import type { Authenticator } from './src/authenticator.js';
|
|
3
3
|
declare module '@adonisjs/core/http' {
|
|
4
4
|
interface HttpContext {
|
|
5
5
|
auth: Authenticator;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"./types": "./build/index.js",
|
|
39
39
|
"./services/main": "./build/services/main.js",
|
|
40
40
|
"./authkit_client_provider": "./build/providers/authkit_client_provider.js",
|
|
41
|
+
"./auth": "./build/src/host/authkit_client_guard.js",
|
|
41
42
|
"./authkit_middleware": "./build/src/middleware/authkit_middleware.js",
|
|
43
|
+
"./authkit_context_middleware": "./build/src/middleware/authkit_context_middleware.js",
|
|
42
44
|
"./backchannel_revocation_middleware": "./build/src/middleware/backchannel_revocation_middleware.js",
|
|
43
45
|
"./auth_middleware": "./build/src/middleware/auth_middleware.js",
|
|
44
46
|
"./silent_auth_middleware": "./build/src/middleware/silent_auth_middleware.js",
|
|
@@ -54,12 +56,16 @@
|
|
|
54
56
|
]
|
|
55
57
|
},
|
|
56
58
|
"peerDependencies": {
|
|
59
|
+
"@adonisjs/auth": "^10.1.0",
|
|
57
60
|
"@adonisjs/core": "^7.3.3",
|
|
58
61
|
"@adonisjs/session": "^8.1.0",
|
|
59
62
|
"@adonisjs/lucid": "^22.4.2",
|
|
60
63
|
"@adonis-agora/resilience": "^0.1.0"
|
|
61
64
|
},
|
|
62
65
|
"peerDependenciesMeta": {
|
|
66
|
+
"@adonisjs/auth": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
63
69
|
"@adonisjs/lucid": {
|
|
64
70
|
"optional": true
|
|
65
71
|
},
|
|
@@ -72,6 +78,7 @@
|
|
|
72
78
|
"@adonis-agora/authkit-core": "0.7.0"
|
|
73
79
|
},
|
|
74
80
|
"devDependencies": {
|
|
81
|
+
"@adonisjs/auth": "10.1.0",
|
|
75
82
|
"@adonisjs/core": "7.3.5",
|
|
76
83
|
"@adonis-agora/resilience": "^0.1.0",
|
|
77
84
|
"@adonisjs/session": "8.1.0",
|
|
@@ -83,11 +90,11 @@
|
|
|
83
90
|
"ioredis-mock": "^8.9.0",
|
|
84
91
|
"@types/node": "25.9.1",
|
|
85
92
|
"typescript": "6.0.3",
|
|
86
|
-
"@adonis-agora/authkit-server": "0.
|
|
93
|
+
"@adonis-agora/authkit-server": "0.55.0"
|
|
87
94
|
},
|
|
88
95
|
"scripts": {
|
|
89
96
|
"build": "tsc && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\"",
|
|
90
|
-
"typecheck": "tsc --noEmit",
|
|
97
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
91
98
|
"test": "node --import=@poppinss/ts-exec bin/test.ts"
|
|
92
99
|
}
|
|
93
100
|
}
|
package/stubs/main.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url'
|
|
2
|
-
export const stubsRoot = fileURLToPath(new URL('./', import.meta.url))
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
export const stubsRoot = fileURLToPath(new URL('./', import.meta.url));
|