@adonis-agora/authkit-server 1.0.0-rc.1 → 1.0.0-rc.11
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/CHANGELOG.md +107 -0
- package/README.md +4 -5
- package/build/commands/agora_doctor.d.ts +22 -0
- package/build/commands/agora_doctor.js +109 -0
- package/build/commands/clients_create.d.ts +4 -0
- package/build/commands/clients_create.js +97 -0
- package/build/commands/commands.json +78 -0
- package/build/commands/doctor.js +8 -74
- package/build/host/views/account/apps.edge +1 -5
- package/build/host/views/account/extension.edge +77 -0
- package/build/host/views/account/mfa.edge +2 -0
- package/build/host/views/account/partials/extension_actions.edge +14 -0
- package/build/host/views/account/partials/extension_block.edge +197 -0
- package/build/host/views/account/partials/extension_value.edge +8 -0
- package/build/host/views/account/partials/nav.edge +14 -0
- package/build/host/views/account/security.edge +1 -5
- package/build/host/views/account/tokens.edge +2 -2
- package/build/host/views/agents/consent.edge +11 -2
- package/build/host/views/agents/done.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +12 -12
- package/build/index.js +7 -9
- package/build/providers/authkit_server_provider.js +29 -8
- package/build/src/account/extension.d.ts +10 -0
- package/build/src/account/extension.js +9 -0
- package/build/src/account/http.d.ts +77 -0
- package/build/src/account/http.js +381 -0
- package/build/src/account/mount.d.ts +40 -0
- package/build/src/account/mount.js +171 -0
- package/build/src/account/registry.d.ts +41 -0
- package/build/src/account/registry.js +81 -0
- package/build/src/account/remote.d.ts +72 -0
- package/build/src/account/remote.js +406 -0
- package/build/src/account/runtime.d.ts +289 -0
- package/build/src/account/runtime.js +641 -0
- package/build/src/accounts/account_store.d.ts +13 -7
- package/build/src/accounts/lucid_account_store.d.ts +3 -3
- package/build/src/accounts/lucid_account_store.js +42 -17
- package/build/src/accounts/lucid_store/mfa.js +3 -3
- package/build/src/accounts/lucid_store/organization_models.d.ts +14 -3
- package/build/src/accounts/lucid_store/organization_models.js +31 -3
- package/build/src/accounts/lucid_store/organizations.d.ts +2 -2
- package/build/src/accounts/lucid_store/organizations.js +40 -31
- package/build/src/accounts/lucid_store/shared.d.ts +11 -3
- package/build/src/accounts/lucid_store/shared.js +7 -5
- package/build/src/accounts/lucid_store/webauthn.js +1 -1
- package/build/src/adapters/factory.d.ts +13 -2
- package/build/src/adapters/factory.js +2 -2
- package/build/src/agents/config.d.ts +9 -13
- package/build/src/agents/config.js +17 -11
- package/build/src/agents/delegation_service.js +3 -3
- package/build/src/agents/delegation_store.d.ts +3 -2
- package/build/src/agents/delegation_store.js +6 -5
- package/build/src/agents/extension.d.ts +261 -0
- package/build/src/agents/extension.js +40 -0
- package/build/src/agents/middleware.js +1 -1
- package/build/src/agents/protocol.d.ts +4 -4
- package/build/src/agents/registry_store.d.ts +3 -2
- package/build/src/agents/registry_store.js +1 -1
- package/build/src/agents/runtime.d.ts +26 -7
- package/build/src/agents/runtime.js +110 -75
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +10 -1
- package/build/src/commands/expire_scan_command.js +1 -1
- package/build/src/commands/settings_commands.js +2 -2
- package/build/src/console_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/define_config.d.ts +38 -13
- package/build/src/define_config.js +32 -10
- package/build/src/dev_tools/contract.d.ts +72 -0
- package/build/src/dev_tools/contract.js +10 -0
- package/build/src/dev_tools/gate.d.ts +17 -0
- package/build/src/dev_tools/gate.js +69 -0
- package/build/src/dev_tools/register.d.ts +11 -0
- package/build/src/dev_tools/register.js +25 -0
- package/build/src/dev_tools/tools.d.ts +24 -0
- package/build/src/dev_tools/tools.js +384 -0
- package/build/src/doctor/agora_checks.d.ts +15 -0
- package/build/src/doctor/agora_checks.js +150 -0
- package/build/src/doctor/agora_doctor.d.ts +115 -0
- package/build/src/doctor/agora_doctor.js +692 -0
- package/build/src/doctor/checks.js +2 -2
- package/build/src/doctor/doctor_input.d.ts +27 -0
- package/build/src/doctor/doctor_input.js +86 -0
- package/build/src/doctor/index.d.ts +5 -0
- package/build/src/doctor/index.js +4 -0
- package/build/src/host/access_token_verifier.js +7 -0
- package/build/src/host/account_api/account_api_controller.js +8 -6
- package/build/src/host/account_session_sign_in.d.ts +12 -0
- package/build/src/host/account_session_sign_in.js +19 -0
- package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
- package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
- package/build/src/host/admin_api/api_clients_controller.js +31 -1
- package/build/src/host/admin_api/dto.d.ts +3 -1
- package/build/src/host/admin_api/dto.js +2 -0
- package/build/src/host/admin_authorize.d.ts +8 -0
- package/build/src/host/admin_authorize.js +25 -0
- package/build/src/host/admin_clients_service.d.ts +20 -0
- package/build/src/host/admin_clients_service.js +55 -0
- package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
- package/build/src/host/admin_console/admin_console_entry.js +80 -0
- package/build/src/host/admin_console/admin_shell_controller.js +8 -0
- package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
- package/build/src/host/admin_console/console_clients_controller.js +34 -1
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
- package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
- package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
- package/build/src/host/admin_console/console_switcher_controller.js +23 -0
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/admin_validators.d.ts +29 -0
- package/build/src/host/admin_validators.js +30 -0
- package/build/src/host/auth_host_config.d.ts +9 -2
- package/build/src/host/controllers/account_apps_controller.js +15 -11
- package/build/src/host/csrf.d.ts +3 -3
- package/build/src/host/csrf.js +3 -13
- package/build/src/host/default_mailer.d.ts +11 -6
- package/build/src/host/default_mailer.js +16 -12
- package/build/src/host/i18n.d.ts +41 -14
- package/build/src/host/i18n.js +55 -13
- package/build/src/host/rate_limit.d.ts +8 -0
- package/build/src/host/rate_limit.js +9 -0
- package/build/src/host/register_auth_host.d.ts +14 -0
- package/build/src/host/register_auth_host.js +167 -106
- package/build/src/host/renderers/edge_renderer.js +5 -0
- package/build/src/host/renderers/inertia_renderer.js +7 -1
- package/build/src/host/runtime_settings.d.ts +6 -6
- package/build/src/host/runtime_settings.js +4 -3
- package/build/src/host/screen_props.d.ts +39 -8
- package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
- package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
- package/build/src/host/ui-dist/index.html +2 -2
- package/build/src/provider/build_provider.js +37 -6
- package/build/src/provider/oidc_service.d.ts +1 -1
- package/build/src/provider/oidc_service.js +1 -1
- package/build/src/schema/ensure.d.ts +24 -1
- package/build/src/schema/ensure.js +44 -154
- package/build/src/schema/tables_location.d.ts +27 -0
- package/build/src/schema/tables_location.js +22 -0
- package/build/src/service_tenants/client_metadata.d.ts +39 -0
- package/build/src/service_tenants/client_metadata.js +80 -0
- package/build/src/service_tenants/config.d.ts +44 -0
- package/build/src/service_tenants/config.js +44 -0
- package/build/src/service_tenants/provider_hooks.d.ts +52 -0
- package/build/src/service_tenants/provider_hooks.js +124 -0
- package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
- package/build/src/service_tenants/tenant_verifier.js +104 -0
- package/build/src/tenancy/tenant_labels.d.ts +30 -0
- package/build/src/tenancy/tenant_labels.js +64 -0
- package/build/types.d.ts +3 -1
- package/package.json +9 -6
- package/build/src/agents/poppy/client_registry.d.ts +0 -50
- package/build/src/agents/poppy/client_registry.js +0 -260
- package/build/src/agents/poppy/config.d.ts +0 -396
- package/build/src/agents/poppy/config.js +0 -228
- package/build/src/agents/poppy/dpop.d.ts +0 -61
- package/build/src/agents/poppy/dpop.js +0 -158
- package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
- package/build/src/agents/poppy/email_code_sign_in.js +0 -140
- package/build/src/agents/poppy/errors.d.ts +0 -25
- package/build/src/agents/poppy/errors.js +0 -25
- package/build/src/agents/poppy/replay.d.ts +0 -25
- package/build/src/agents/poppy/replay.js +0 -106
- package/build/src/agents/poppy/resource.d.ts +0 -100
- package/build/src/agents/poppy/resource.js +0 -129
- package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
- package/build/src/agents/poppy/safe_fetch.js +0 -262
- package/build/src/agents/poppy/service.d.ts +0 -293
- package/build/src/agents/poppy/service.js +0 -1458
- package/build/src/agents/poppy/session_jwt.d.ts +0 -30
- package/build/src/agents/poppy/session_jwt.js +0 -133
- package/build/src/agents/poppy/store.d.ts +0 -106
- package/build/src/agents/poppy/store.js +0 -261
- package/build/src/agents/poppy/web_session.d.ts +0 -39
- package/build/src/agents/poppy/web_session.js +0 -122
- package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
- package/build/src/host/controllers/poppy_consent_controller.js +0 -199
- package/build/src/host/controllers/poppy_controller.d.ts +0 -33
- package/build/src/host/controllers/poppy_controller.js +0 -219
- package/build/src/host/poppy_session_key.d.ts +0 -9
- package/build/src/host/poppy_session_key.js +0 -9
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.11",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -41,12 +41,14 @@
|
|
|
41
41
|
"exports": {
|
|
42
42
|
".": "./build/index.js",
|
|
43
43
|
"./types": "./build/types.js",
|
|
44
|
+
"./account-extension": "./build/src/account/extension.js",
|
|
44
45
|
"./services/main": "./build/services/main.js",
|
|
45
46
|
"./durable": "./build/src/host/durable/index.js",
|
|
46
47
|
"./telescope": "./build/src/observability/telescope/index.js",
|
|
47
48
|
"./authkit_server_provider": "./build/providers/authkit_server_provider.js",
|
|
48
49
|
"./commands": "./build/commands/main.js",
|
|
49
|
-
"./commands/configure": "./build/commands/configure.js"
|
|
50
|
+
"./commands/configure": "./build/commands/configure.js",
|
|
51
|
+
"./doctor": "./build/src/doctor/index.js"
|
|
50
52
|
},
|
|
51
53
|
"adonisjs": {
|
|
52
54
|
"types": "@adonis-agora/authkit-server",
|
|
@@ -106,10 +108,10 @@
|
|
|
106
108
|
}
|
|
107
109
|
},
|
|
108
110
|
"dependencies": {
|
|
109
|
-
"@adonis-agora/authkit-core": "1.0.0-rc.
|
|
111
|
+
"@adonis-agora/authkit-core": "1.0.0-rc.1",
|
|
110
112
|
"@simplewebauthn/browser": "14.0.0",
|
|
111
113
|
"@simplewebauthn/server": "14.1.0",
|
|
112
|
-
"jose": "6.2.
|
|
114
|
+
"jose": "6.2.16",
|
|
113
115
|
"koa": "3.3.0",
|
|
114
116
|
"koa-mount": "4.2.0",
|
|
115
117
|
"oidc-provider": "9.13.3",
|
|
@@ -117,8 +119,8 @@
|
|
|
117
119
|
"qrcode": "1.5.4"
|
|
118
120
|
},
|
|
119
121
|
"devDependencies": {
|
|
120
|
-
"@adonis-agora/authkit-react": "1.0.0-rc.
|
|
121
|
-
"@adonis-agora/durable": "0.
|
|
122
|
+
"@adonis-agora/authkit-react": "1.0.0-rc.4",
|
|
123
|
+
"@adonis-agora/durable": "0.60.0",
|
|
122
124
|
"@adonis-agora/telescope": "0.27.1",
|
|
123
125
|
"@adonisjs/ally": "6.3.2",
|
|
124
126
|
"@adonisjs/auth": "10.1.0",
|
|
@@ -169,6 +171,7 @@
|
|
|
169
171
|
"build": "node scripts/build_host_css.mjs && node scripts/build_webauthn.mjs && node scripts/build_ui.mjs && node -e \"const fs=require('node:fs');for(const d of ['build/stubs','build/host/views'])fs.rmSync(d,{recursive:true,force:true})\" && tsc && node -e \"require('node:fs').cpSync('src/host/assets','build/src/host/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/src/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/src/password/common_passwords.txt')\"",
|
|
170
172
|
"build:ui": "node scripts/build_ui.mjs",
|
|
171
173
|
"build:webauthn": "node scripts/build_webauthn.mjs",
|
|
174
|
+
"check:agora-doctor": "node ../../scripts/check-agora-doctor.mjs .",
|
|
172
175
|
"check:host-css": "node scripts/check_host_css.mjs",
|
|
173
176
|
"check:webauthn-bundle": "node scripts/check_webauthn_bundle.mjs",
|
|
174
177
|
"typecheck": "tsc --noEmit && node scripts/typecheck_ui.mjs && node scripts/typecheck_tests.mjs",
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Identidade do personal agent no Poppy (§4.1): o `client_id` é uma URL HTTPS
|
|
3
|
-
* que devolve o documento de metadata do agente (OAuth Client ID Metadata
|
|
4
|
-
* Document). Buscamos (à prova de SSRF), validamos, guardamos em cache e
|
|
5
|
-
* importamos as chaves públicas do `jwks_uri`.
|
|
6
|
-
*/
|
|
7
|
-
import { type JWTVerifyGetKey } from 'jose';
|
|
8
|
-
import type { ResolvedPoppyConfig } from './config.js';
|
|
9
|
-
import { type JsonFetcher } from './safe_fetch.js';
|
|
10
|
-
export interface PoppyClient {
|
|
11
|
-
clientId: string;
|
|
12
|
-
/** `client_name`, ou o host do `client_id`. */
|
|
13
|
-
name: string;
|
|
14
|
-
logoUri: string | null;
|
|
15
|
-
jwksUri: string;
|
|
16
|
-
redirectUris: string[];
|
|
17
|
-
extensions: Record<string, unknown>;
|
|
18
|
-
/** Chaves públicas do `jwks_uri`. */
|
|
19
|
-
keys: JWTVerifyGetKey;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* `redirect_uri` igual a uma entrada de `redirect_uris`; numa loopback redirect URI a porta não
|
|
23
|
-
* conta (§4.2, RFC 8252 §7.3).
|
|
24
|
-
*/
|
|
25
|
-
export declare function redirectUriAllowed(registered: string[], redirectUri: string): boolean;
|
|
26
|
-
/** Valida um documento de metadata e devolve os campos usados. Lança `invalid_client`. */
|
|
27
|
-
export declare function validateClientMetadata(fetchedFrom: string, doc: unknown, options?: {
|
|
28
|
-
allowHttp?: boolean;
|
|
29
|
-
}): Omit<PoppyClient, 'keys'>;
|
|
30
|
-
export interface PoppyClientRegistryOptions {
|
|
31
|
-
/** Busca de JSON. Default: {@link safeFetchJson}. */
|
|
32
|
-
fetchJson?: JsonFetcher;
|
|
33
|
-
now?: () => number;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Resolve e valida agentes pelo `client_id`, com cache (respeita `max-age`,
|
|
37
|
-
* entre 1 min e 24 h; falhas ficam 1 min em cache para que o mesmo `client_id`
|
|
38
|
-
* não vire uma busca de saída por request).
|
|
39
|
-
*/
|
|
40
|
-
export declare class PoppyClientRegistry {
|
|
41
|
-
#private;
|
|
42
|
-
constructor(cfg: ResolvedPoppyConfig, options?: PoppyClientRegistryOptions);
|
|
43
|
-
/**
|
|
44
|
-
* O agente, ou `invalid_client`: `client_id` não é HTTPS, bloqueado, não
|
|
45
|
-
* registrado (quando o app exige registro), metadata inválida/inalcançável.
|
|
46
|
-
*/
|
|
47
|
-
resolve(clientId: unknown): Promise<PoppyClient>;
|
|
48
|
-
/** Nome de exibição sem falhar (console de conta): metadata em cache/buscada, ou o host. */
|
|
49
|
-
displayName(clientId: string): Promise<string>;
|
|
50
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Identidade do personal agent no Poppy (§4.1): o `client_id` é uma URL HTTPS
|
|
3
|
-
* que devolve o documento de metadata do agente (OAuth Client ID Metadata
|
|
4
|
-
* Document). Buscamos (à prova de SSRF), validamos, guardamos em cache e
|
|
5
|
-
* importamos as chaves públicas do `jwks_uri`.
|
|
6
|
-
*/
|
|
7
|
-
import { createLocalJWKSet } from 'jose';
|
|
8
|
-
import { PoppyError } from './errors.js';
|
|
9
|
-
import { insecureDevFetchJson, SafeFetchError, safeFetchJson, } from './safe_fetch.js';
|
|
10
|
-
const MIN_TTL_MS = 60 * 1000;
|
|
11
|
-
const MAX_TTL_MS = 24 * 3600 * 1000;
|
|
12
|
-
const DEFAULT_TTL_MS = 3600 * 1000;
|
|
13
|
-
const FAILURE_TTL_MS = 60 * 1000;
|
|
14
|
-
const MAX_CACHED = 1000;
|
|
15
|
-
/** Releitura do JWKS quando o `kid` não está no conjunto em cache — no máximo a cada 30 s. */
|
|
16
|
-
const JWKS_REFRESH_COOLDOWN_MS = 30 * 1000;
|
|
17
|
-
function hostOf(url) {
|
|
18
|
-
try {
|
|
19
|
-
return new URL(url).host.toLowerCase();
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function isHttpsUrl(value, allowHttp = false) {
|
|
26
|
-
if (typeof value !== 'string')
|
|
27
|
-
return false;
|
|
28
|
-
try {
|
|
29
|
-
const url = new URL(value);
|
|
30
|
-
const schemeOk = url.protocol === 'https:' || (allowHttp && url.protocol === 'http:');
|
|
31
|
-
return schemeOk && !url.username && !url.password && !url.hash;
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/** Loopback redirect URI (RFC 8252 §7.3): `http`, IP de loopback literal, sem fragmento. */
|
|
38
|
-
function isLoopbackRedirect(value) {
|
|
39
|
-
if (typeof value !== 'string')
|
|
40
|
-
return false;
|
|
41
|
-
try {
|
|
42
|
-
const url = new URL(value);
|
|
43
|
-
return (url.protocol === 'http:' &&
|
|
44
|
-
(url.hostname === '127.0.0.1' || url.hostname === '[::1]') &&
|
|
45
|
-
!url.username &&
|
|
46
|
-
!url.password &&
|
|
47
|
-
!url.hash);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* `redirect_uri` igual a uma entrada de `redirect_uris`; numa loopback redirect URI a porta não
|
|
55
|
-
* conta (§4.2, RFC 8252 §7.3).
|
|
56
|
-
*/
|
|
57
|
-
export function redirectUriAllowed(registered, redirectUri) {
|
|
58
|
-
if (registered.includes(redirectUri))
|
|
59
|
-
return true;
|
|
60
|
-
if (!isLoopbackRedirect(redirectUri))
|
|
61
|
-
return false;
|
|
62
|
-
const withoutPort = (value) => {
|
|
63
|
-
const url = new URL(value);
|
|
64
|
-
url.port = '';
|
|
65
|
-
return url.toString();
|
|
66
|
-
};
|
|
67
|
-
return registered.some((r) => isLoopbackRedirect(r) && withoutPort(r) === withoutPort(redirectUri));
|
|
68
|
-
}
|
|
69
|
-
function ttlFrom(maxAge) {
|
|
70
|
-
if (maxAge === null)
|
|
71
|
-
return DEFAULT_TTL_MS;
|
|
72
|
-
return Math.min(MAX_TTL_MS, Math.max(MIN_TTL_MS, maxAge * 1000));
|
|
73
|
-
}
|
|
74
|
-
function setBounded(map, key, value) {
|
|
75
|
-
map.delete(key);
|
|
76
|
-
map.set(key, value);
|
|
77
|
-
if (map.size > MAX_CACHED)
|
|
78
|
-
map.delete(map.keys().next().value);
|
|
79
|
-
}
|
|
80
|
-
/** Valida um documento de metadata e devolve os campos usados. Lança `invalid_client`. */
|
|
81
|
-
export function validateClientMetadata(fetchedFrom, doc, options = {}) {
|
|
82
|
-
const allowHttp = options.allowHttp === true;
|
|
83
|
-
const bad = (why) => new PoppyError('invalid_client', `client metadata: ${why}`);
|
|
84
|
-
if (!doc || typeof doc !== 'object' || Array.isArray(doc))
|
|
85
|
-
throw bad('not a JSON object');
|
|
86
|
-
const d = doc;
|
|
87
|
-
if (d.client_id !== fetchedFrom)
|
|
88
|
-
throw bad('client_id must equal the URL it was fetched from');
|
|
89
|
-
const host = hostOf(fetchedFrom);
|
|
90
|
-
if (!isHttpsUrl(d.jwks_uri, allowHttp) || hostOf(d.jwks_uri) !== host) {
|
|
91
|
-
throw bad('jwks_uri must be an HTTPS URL on the same domain as client_id');
|
|
92
|
-
}
|
|
93
|
-
const redirects = d.redirect_uris ?? [];
|
|
94
|
-
if (!Array.isArray(redirects))
|
|
95
|
-
throw bad('redirect_uris must be an array');
|
|
96
|
-
for (const uri of redirects) {
|
|
97
|
-
if (isLoopbackRedirect(uri))
|
|
98
|
-
continue;
|
|
99
|
-
if (!isHttpsUrl(uri, allowHttp) || hostOf(uri) !== host) {
|
|
100
|
-
throw bad('every redirect_uri must be an HTTPS URL on the same domain as client_id, or a loopback URI');
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (d.token_endpoint_auth_method !== 'private_key_jwt') {
|
|
104
|
-
throw bad('token_endpoint_auth_method must be private_key_jwt');
|
|
105
|
-
}
|
|
106
|
-
if (d.jwks !== undefined)
|
|
107
|
-
throw bad('keys are published at jwks_uri only');
|
|
108
|
-
const name = typeof d.client_name === 'string' && d.client_name.trim() ? d.client_name : host;
|
|
109
|
-
const extensions = d.extensions && typeof d.extensions === 'object' && !Array.isArray(d.extensions)
|
|
110
|
-
? d.extensions
|
|
111
|
-
: {};
|
|
112
|
-
return {
|
|
113
|
-
clientId: fetchedFrom,
|
|
114
|
-
name: name.slice(0, 200),
|
|
115
|
-
logoUri: isHttpsUrl(d.logo_uri, allowHttp) ? d.logo_uri : null,
|
|
116
|
-
jwksUri: d.jwks_uri,
|
|
117
|
-
redirectUris: redirects,
|
|
118
|
-
extensions,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
/** Só chaves públicas (§4.1) e pelo menos uma. */
|
|
122
|
-
function validateJwks(doc) {
|
|
123
|
-
const keys = doc?.keys;
|
|
124
|
-
if (!Array.isArray(keys) || keys.length === 0) {
|
|
125
|
-
throw new PoppyError('invalid_client', 'jwks_uri must serve a non-empty JWK set');
|
|
126
|
-
}
|
|
127
|
-
for (const key of keys) {
|
|
128
|
-
if (!key || typeof key !== 'object')
|
|
129
|
-
throw new PoppyError('invalid_client', 'invalid JWK');
|
|
130
|
-
const k = key;
|
|
131
|
-
if ('d' in k || 'p' in k || 'q' in k || k.kty === 'oct') {
|
|
132
|
-
throw new PoppyError('invalid_client', 'jwks_uri must contain only public keys');
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return { keys: keys };
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Resolve e valida agentes pelo `client_id`, com cache (respeita `max-age`,
|
|
139
|
-
* entre 1 min e 24 h; falhas ficam 1 min em cache para que o mesmo `client_id`
|
|
140
|
-
* não vire uma busca de saída por request).
|
|
141
|
-
*/
|
|
142
|
-
export class PoppyClientRegistry {
|
|
143
|
-
#cfg;
|
|
144
|
-
#fetch;
|
|
145
|
-
#now;
|
|
146
|
-
#clients = new Map();
|
|
147
|
-
#jwks = new Map();
|
|
148
|
-
constructor(cfg, options = {}) {
|
|
149
|
-
this.#cfg = cfg;
|
|
150
|
-
this.#fetch =
|
|
151
|
-
options.fetchJson ??
|
|
152
|
-
(cfg.allowInsecureClientIds
|
|
153
|
-
? (url) => insecureDevFetchJson(url)
|
|
154
|
-
: (url) => safeFetchJson(url));
|
|
155
|
-
this.#now = options.now ?? Date.now;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* O agente, ou `invalid_client`: `client_id` não é HTTPS, bloqueado, não
|
|
159
|
-
* registrado (quando o app exige registro), metadata inválida/inalcançável.
|
|
160
|
-
*/
|
|
161
|
-
async resolve(clientId) {
|
|
162
|
-
if (!isHttpsUrl(clientId, this.#cfg.allowInsecureClientIds)) {
|
|
163
|
-
throw new PoppyError('invalid_client', 'client_id must be an HTTPS URL');
|
|
164
|
-
}
|
|
165
|
-
if (await this.#cfg.clients.isBlocked(clientId)) {
|
|
166
|
-
throw new PoppyError('invalid_client', 'client_id is blocked');
|
|
167
|
-
}
|
|
168
|
-
if (this.#cfg.clients.requireRegistration &&
|
|
169
|
-
!(await this.#cfg.clients.isRegistered(clientId))) {
|
|
170
|
-
throw new PoppyError('invalid_client', 'client_id is not registered');
|
|
171
|
-
}
|
|
172
|
-
const meta = await this.#metadata(clientId);
|
|
173
|
-
return { ...meta, keys: this.#keySet(meta.jwksUri) };
|
|
174
|
-
}
|
|
175
|
-
/** Nome de exibição sem falhar (console de conta): metadata em cache/buscada, ou o host. */
|
|
176
|
-
async displayName(clientId) {
|
|
177
|
-
try {
|
|
178
|
-
return (await this.#metadata(clientId)).name;
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return hostOf(clientId) ?? clientId;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
#metadata(clientId) {
|
|
185
|
-
const now = this.#now();
|
|
186
|
-
const cached = this.#clients.get(clientId);
|
|
187
|
-
if (cached && cached.expiresAt > now)
|
|
188
|
-
return cached.value;
|
|
189
|
-
const entry = {
|
|
190
|
-
value: Promise.resolve(null),
|
|
191
|
-
expiresAt: now + FAILURE_TTL_MS,
|
|
192
|
-
};
|
|
193
|
-
entry.value = (async () => {
|
|
194
|
-
let fetched;
|
|
195
|
-
try {
|
|
196
|
-
fetched = await this.#fetch(clientId);
|
|
197
|
-
}
|
|
198
|
-
catch (error) {
|
|
199
|
-
const why = error instanceof SafeFetchError ? error.message : 'unreachable';
|
|
200
|
-
throw new PoppyError('invalid_client', `client metadata: ${why}`);
|
|
201
|
-
}
|
|
202
|
-
const meta = validateClientMetadata(clientId, fetched.body, {
|
|
203
|
-
allowHttp: this.#cfg.allowInsecureClientIds,
|
|
204
|
-
});
|
|
205
|
-
entry.expiresAt = this.#now() + ttlFrom(fetched.maxAge);
|
|
206
|
-
return meta;
|
|
207
|
-
})();
|
|
208
|
-
entry.value.catch(() => {
|
|
209
|
-
entry.expiresAt = this.#now() + FAILURE_TTL_MS;
|
|
210
|
-
});
|
|
211
|
-
setBounded(this.#clients, clientId, entry);
|
|
212
|
-
return entry.value;
|
|
213
|
-
}
|
|
214
|
-
#fetchJwks(jwksUri, force) {
|
|
215
|
-
const now = this.#now();
|
|
216
|
-
const cached = this.#jwks.get(jwksUri);
|
|
217
|
-
if (cached && cached.expiresAt > now && !force)
|
|
218
|
-
return cached.value;
|
|
219
|
-
if (cached && force && now - cached.fetchedAt < JWKS_REFRESH_COOLDOWN_MS)
|
|
220
|
-
return cached.value;
|
|
221
|
-
const entry = {
|
|
222
|
-
value: Promise.resolve({ keys: [] }),
|
|
223
|
-
expiresAt: now + FAILURE_TTL_MS,
|
|
224
|
-
fetchedAt: now,
|
|
225
|
-
};
|
|
226
|
-
entry.value = (async () => {
|
|
227
|
-
let fetched;
|
|
228
|
-
try {
|
|
229
|
-
fetched = await this.#fetch(jwksUri);
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
const why = error instanceof SafeFetchError ? error.message : 'unreachable';
|
|
233
|
-
throw new PoppyError('invalid_client', `jwks_uri: ${why}`);
|
|
234
|
-
}
|
|
235
|
-
const jwks = validateJwks(fetched.body);
|
|
236
|
-
entry.expiresAt = this.#now() + ttlFrom(fetched.maxAge);
|
|
237
|
-
return jwks;
|
|
238
|
-
})();
|
|
239
|
-
entry.value.catch(() => {
|
|
240
|
-
entry.expiresAt = this.#now() + FAILURE_TTL_MS;
|
|
241
|
-
});
|
|
242
|
-
setBounded(this.#jwks, jwksUri, entry);
|
|
243
|
-
return entry.value;
|
|
244
|
-
}
|
|
245
|
-
/** `JWTVerifyGetKey` que relê o JWKS uma vez quando o `kid` é desconhecido (rotação). */
|
|
246
|
-
#keySet(jwksUri) {
|
|
247
|
-
return async (header, token) => {
|
|
248
|
-
const jwks = await this.#fetchJwks(jwksUri, false);
|
|
249
|
-
try {
|
|
250
|
-
return await createLocalJWKSet(jwks)(header, token);
|
|
251
|
-
}
|
|
252
|
-
catch (error) {
|
|
253
|
-
const refreshed = await this.#fetchJwks(jwksUri, true);
|
|
254
|
-
if (refreshed === jwks)
|
|
255
|
-
throw error;
|
|
256
|
-
return createLocalJWKSet(refreshed)(header, token);
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
}
|