@fluixi/oauth2 1.0.0-alpha.1
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/LICENSE +21 -0
- package/dist/api-guard.cjs +185 -0
- package/dist/api-guard.d.ts +94 -0
- package/dist/api-guard.d.ts.map +1 -0
- package/dist/api-guard.js +127 -0
- package/dist/api-guard.mjs +141 -0
- package/dist/browser-FDUOEUM2.mjs +3544 -0
- package/dist/browser-ONMHX3Z3.mjs +3545 -0
- package/dist/browser.cjs +735 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +448 -0
- package/dist/browser.mjs +704 -0
- package/dist/callback.cjs +125 -0
- package/dist/callback.d.ts +113 -0
- package/dist/callback.d.ts.map +1 -0
- package/dist/callback.js +183 -0
- package/dist/callback.mjs +106 -0
- package/dist/chunk-25WE6UWI.mjs +10 -0
- package/dist/chunk-4NSOGPLM.mjs +15 -0
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-OGSFNTKA.mjs +13 -0
- package/dist/client-auth.cjs +115 -0
- package/dist/client-auth.d.ts +43 -0
- package/dist/client-auth.d.ts.map +1 -0
- package/dist/client-auth.js +112 -0
- package/dist/client-auth.mjs +84 -0
- package/dist/discovery.cjs +86 -0
- package/dist/discovery.d.ts +15 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +51 -0
- package/dist/discovery.mjs +63 -0
- package/dist/dpop-LVPWWADA.mjs +80 -0
- package/dist/dpop-NS7NKOWM.mjs +79 -0
- package/dist/dpop-store.cjs +152 -0
- package/dist/dpop-store.d.ts +37 -0
- package/dist/dpop-store.d.ts.map +1 -0
- package/dist/dpop-store.js +119 -0
- package/dist/dpop-store.mjs +121 -0
- package/dist/dpop.cjs +117 -0
- package/dist/dpop.d.ts +72 -0
- package/dist/dpop.d.ts.map +1 -0
- package/dist/dpop.js +114 -0
- package/dist/dpop.mjs +86 -0
- package/dist/env-config.cjs +366 -0
- package/dist/env-config.d.ts +103 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +184 -0
- package/dist/env-config.mjs +343 -0
- package/dist/env-credentials.cjs +82 -0
- package/dist/env-credentials.d.ts +68 -0
- package/dist/env-credentials.d.ts.map +1 -0
- package/dist/env-credentials.js +76 -0
- package/dist/env-credentials.mjs +51 -0
- package/dist/index.cjs +940 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.mjs +909 -0
- package/dist/pkce.cjs +87 -0
- package/dist/pkce.d.ts +58 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +112 -0
- package/dist/pkce.mjs +66 -0
- package/dist/presets.cjs +240 -0
- package/dist/presets.d.ts +140 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +182 -0
- package/dist/presets.mjs +217 -0
- package/dist/return-to.cjs +46 -0
- package/dist/return-to.d.ts +49 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +71 -0
- package/dist/return-to.mjs +25 -0
- package/dist/roles.cjs +56 -0
- package/dist/roles.d.ts +35 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +37 -0
- package/dist/roles.mjs +35 -0
- package/dist/server-client.cjs +134 -0
- package/dist/server-client.d.ts +12 -0
- package/dist/server-client.d.ts.map +1 -0
- package/dist/server-client.js +107 -0
- package/dist/server-client.mjs +111 -0
- package/dist/server.cjs +470 -0
- package/dist/server.d.ts +89 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +360 -0
- package/dist/server.mjs +441 -0
- package/dist/service-client.cjs +244 -0
- package/dist/service-client.d.ts +89 -0
- package/dist/service-client.d.ts.map +1 -0
- package/dist/service-client.js +112 -0
- package/dist/service-client.mjs +213 -0
- package/dist/service.cjs +903 -0
- package/dist/service.d.ts +96 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +129 -0
- package/dist/service.mjs +874 -0
- package/dist/tokens.cjs +29 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +21 -0
- package/dist/tokens.mjs +8 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/types.cjs +33 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.mjs +12 -0
- package/dist/verify.cjs +724 -0
- package/dist/verify.d.ts +205 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +371 -0
- package/dist/verify.mjs +590 -0
- package/package.json +121 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Providers, as configuration rather than prose.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here is required. A provider is `issuer` plus discovery, and that is the whole
|
|
5
|
+
* contract: `presets.oidc({ issuer })` is the agnostic path and covers anything publishing a
|
|
6
|
+
* discovery document, `presets.oauth2({ endpoints })` covers a plain OAuth2 server that
|
|
7
|
+
* publishes none. The named presets exist because each of the five cost a round trip to find
|
|
8
|
+
* out, and a fact learned from a live tenant belongs in code rather than in a handoff nobody
|
|
9
|
+
* reads at the right moment.
|
|
10
|
+
*
|
|
11
|
+
* What a preset does not do is decide anything an app owns. No client id, no redirect URI, no
|
|
12
|
+
* secret. It contributes the parts that follow from *which provider this is*: how the issuer
|
|
13
|
+
* is spelled, where roles live, whether an access token carries an `aud`, and what the
|
|
14
|
+
* provider will refuse outright.
|
|
15
|
+
*
|
|
16
|
+
* Every preset is a plain object. Read one, disagree with it, spread it and override the
|
|
17
|
+
* field. That is the intended use as much as taking it whole.
|
|
18
|
+
*/
|
|
19
|
+
import type { TokenClaims } from './verify.js';
|
|
20
|
+
import type { Endpoints } from './types.js';
|
|
21
|
+
/** Verifier options that follow from the provider, given the client this app is. */
|
|
22
|
+
export interface VerifierShape {
|
|
23
|
+
issuer: string | string[];
|
|
24
|
+
/** `false` for a provider that puts no `aud` on an access token. */
|
|
25
|
+
audience: string | string[] | false;
|
|
26
|
+
/** Set when `audience` is false, so the check moves rather than disappearing. */
|
|
27
|
+
authorizedParty?: string | string[];
|
|
28
|
+
}
|
|
29
|
+
/** What a provider contributes to a client config, and what it cannot do. */
|
|
30
|
+
export interface ProviderPreset {
|
|
31
|
+
/** A name for messages and logs. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Spread into `createBrowserClient` or `oauthRoutes`. */
|
|
34
|
+
client: {
|
|
35
|
+
issuer?: string;
|
|
36
|
+
endpoints?: Endpoints;
|
|
37
|
+
scopes?: string[];
|
|
38
|
+
dpop?: boolean;
|
|
39
|
+
extraParams?: Record<string, string>;
|
|
40
|
+
tokenAuthMethod?: 'client_secret_basic' | 'client_secret_post' | 'private_key_jwt' | 'none';
|
|
41
|
+
};
|
|
42
|
+
/** Where this provider puts roles, for `createApiGuard`. */
|
|
43
|
+
roles: (claims: TokenClaims) => string[];
|
|
44
|
+
/** Verifier options, given the client id this app authenticates as. */
|
|
45
|
+
verifier: (clientId: string) => VerifierShape;
|
|
46
|
+
/**
|
|
47
|
+
* What this provider will refuse, learned live rather than from documentation.
|
|
48
|
+
*
|
|
49
|
+
* Worth reading before debugging: each line here was a failure that looked like a bug in
|
|
50
|
+
* this package.
|
|
51
|
+
*/
|
|
52
|
+
constraints: string[];
|
|
53
|
+
}
|
|
54
|
+
export declare const presets: {
|
|
55
|
+
/**
|
|
56
|
+
* Any OIDC provider publishing a discovery document.
|
|
57
|
+
*
|
|
58
|
+
* The agnostic path, and the one to reach for first. A provider not listed below almost
|
|
59
|
+
* certainly works through this.
|
|
60
|
+
*/
|
|
61
|
+
oidc(options: {
|
|
62
|
+
issuer: string;
|
|
63
|
+
scopes?: string[];
|
|
64
|
+
/** `false` for a provider that puts no `aud` on an access token. */
|
|
65
|
+
audience?: string | false;
|
|
66
|
+
roles?: (claims: TokenClaims) => string[];
|
|
67
|
+
}): ProviderPreset;
|
|
68
|
+
/**
|
|
69
|
+
* A plain OAuth2 server with no discovery document.
|
|
70
|
+
*
|
|
71
|
+
* Every endpoint is given, because there is nothing to ask. Token verification needs an
|
|
72
|
+
* issuer regardless, since a verifier will not accept a token without one.
|
|
73
|
+
*/
|
|
74
|
+
oauth2(options: {
|
|
75
|
+
endpoints: Endpoints;
|
|
76
|
+
issuer: string;
|
|
77
|
+
scopes?: string[];
|
|
78
|
+
audience?: string | false;
|
|
79
|
+
roles?: (claims: TokenClaims) => string[];
|
|
80
|
+
}): ProviderPreset;
|
|
81
|
+
/** Keycloak. A realm is its own issuer. */
|
|
82
|
+
keycloak(options: {
|
|
83
|
+
baseUrl: string;
|
|
84
|
+
realm: string;
|
|
85
|
+
scopes?: string[];
|
|
86
|
+
}): ProviderPreset;
|
|
87
|
+
/**
|
|
88
|
+
* Auth0.
|
|
89
|
+
*
|
|
90
|
+
* `audience` is the field that decides what kind of token comes back. Without it Auth0
|
|
91
|
+
* issues an opaque token for its own userinfo endpoint and the JWT checks have nothing to
|
|
92
|
+
* work with.
|
|
93
|
+
*/
|
|
94
|
+
auth0(options: {
|
|
95
|
+
domain: string;
|
|
96
|
+
/** An API identifier. Required for a JWT access token, and for refresh tokens. */
|
|
97
|
+
audience?: string;
|
|
98
|
+
scopes?: string[];
|
|
99
|
+
/** The namespaced claim an action adds. Auth0 has no built-in roles claim. */
|
|
100
|
+
rolesClaim?: string;
|
|
101
|
+
}): ProviderPreset;
|
|
102
|
+
/**
|
|
103
|
+
* Okta.
|
|
104
|
+
*
|
|
105
|
+
* Two authorization servers that are not interchangeable: a custom one per application,
|
|
106
|
+
* and the org itself for Okta's own management API.
|
|
107
|
+
*/
|
|
108
|
+
okta(options: {
|
|
109
|
+
domain: string;
|
|
110
|
+
/** The custom server, default `default`. Pass `null` for the org server. */
|
|
111
|
+
authorizationServer?: string | null;
|
|
112
|
+
scopes?: string[];
|
|
113
|
+
/** Okta turns DPoP on for new applications. Default true, because that is the default there. */
|
|
114
|
+
dpop?: boolean;
|
|
115
|
+
}): ProviderPreset;
|
|
116
|
+
/**
|
|
117
|
+
* Google.
|
|
118
|
+
*
|
|
119
|
+
* Refuses a public client outright, so server mode is the only option: the token endpoint
|
|
120
|
+
* checks for a client secret before it looks at PKCE.
|
|
121
|
+
*/
|
|
122
|
+
google(options?: {
|
|
123
|
+
scopes?: string[];
|
|
124
|
+
hostedDomain?: string;
|
|
125
|
+
}): ProviderPreset;
|
|
126
|
+
/**
|
|
127
|
+
* Dex.
|
|
128
|
+
*
|
|
129
|
+
* Sends no CORS headers, on discovery or the token endpoint, so a browser cannot talk to it
|
|
130
|
+
* at all. Server mode only, and giving endpoints explicitly does not help: the token request
|
|
131
|
+
* is blocked too.
|
|
132
|
+
*/
|
|
133
|
+
dex(options: {
|
|
134
|
+
baseUrl: string;
|
|
135
|
+
scopes?: string[];
|
|
136
|
+
}): ProviderPreset;
|
|
137
|
+
};
|
|
138
|
+
/** The preset names, for an app offering a choice. */
|
|
139
|
+
export type PresetName = keyof typeof presets;
|
|
140
|
+
//# sourceMappingURL=presets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"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"}
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { rolesFrom } from './roles.js';
|
|
2
|
+
/** Trailing slashes matter to an issuer comparison, so they are normalised once. */
|
|
3
|
+
const trim = (url) => url.replace(/\/+$/, '');
|
|
4
|
+
export const presets = {
|
|
5
|
+
/**
|
|
6
|
+
* Any OIDC provider publishing a discovery document.
|
|
7
|
+
*
|
|
8
|
+
* The agnostic path, and the one to reach for first. A provider not listed below almost
|
|
9
|
+
* certainly works through this.
|
|
10
|
+
*/
|
|
11
|
+
oidc(options) {
|
|
12
|
+
return {
|
|
13
|
+
label: options.issuer,
|
|
14
|
+
client: { issuer: trim(options.issuer), scopes: options.scopes },
|
|
15
|
+
roles: options.roles ?? rolesFrom.scope,
|
|
16
|
+
verifier: (clientId) => ({
|
|
17
|
+
issuer: trim(options.issuer),
|
|
18
|
+
audience: options.audience ?? clientId,
|
|
19
|
+
...(options.audience === false ? { authorizedParty: clientId } : {}),
|
|
20
|
+
}),
|
|
21
|
+
constraints: [],
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
/**
|
|
25
|
+
* A plain OAuth2 server with no discovery document.
|
|
26
|
+
*
|
|
27
|
+
* Every endpoint is given, because there is nothing to ask. Token verification needs an
|
|
28
|
+
* issuer regardless, since a verifier will not accept a token without one.
|
|
29
|
+
*/
|
|
30
|
+
oauth2(options) {
|
|
31
|
+
return {
|
|
32
|
+
label: options.issuer,
|
|
33
|
+
client: { endpoints: options.endpoints, scopes: options.scopes },
|
|
34
|
+
roles: options.roles ?? rolesFrom.scope,
|
|
35
|
+
verifier: (clientId) => ({
|
|
36
|
+
issuer: trim(options.issuer),
|
|
37
|
+
audience: options.audience ?? clientId,
|
|
38
|
+
...(options.audience === false ? { authorizedParty: clientId } : {}),
|
|
39
|
+
}),
|
|
40
|
+
constraints: ['Publishes no discovery document, so every endpoint is configured by hand.'],
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
/** Keycloak. A realm is its own issuer. */
|
|
44
|
+
keycloak(options) {
|
|
45
|
+
const issuer = `${trim(options.baseUrl)}/realms/${options.realm}`;
|
|
46
|
+
return {
|
|
47
|
+
label: 'Keycloak',
|
|
48
|
+
client: {
|
|
49
|
+
issuer,
|
|
50
|
+
scopes: options.scopes ?? ['openid', 'profile', 'email'],
|
|
51
|
+
},
|
|
52
|
+
roles: rolesFrom.keycloakRealm,
|
|
53
|
+
// No `aud` on an access token until an audience mapper is configured, and the client is
|
|
54
|
+
// named in `azp` instead. So the check moves to `authorizedParty` rather than being
|
|
55
|
+
// dropped: `audience: false` alone would accept a token minted for any client of the
|
|
56
|
+
// realm, which is the check being skipped.
|
|
57
|
+
verifier: (clientId) => ({ issuer, audience: false, authorizedParty: clientId }),
|
|
58
|
+
constraints: [
|
|
59
|
+
'Puts no `aud` on an access token until a dedicated audience mapper is configured. Configure one and switch the verifier to `audience: clientId`.',
|
|
60
|
+
'Revokes the whole session when an authorization code is replayed, so a replay test has to run last.',
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
/**
|
|
65
|
+
* Auth0.
|
|
66
|
+
*
|
|
67
|
+
* `audience` is the field that decides what kind of token comes back. Without it Auth0
|
|
68
|
+
* issues an opaque token for its own userinfo endpoint and the JWT checks have nothing to
|
|
69
|
+
* work with.
|
|
70
|
+
*/
|
|
71
|
+
auth0(options) {
|
|
72
|
+
const issuer = `https://${options.domain.replace(/^https?:\/\//, '').replace(/\/+$/, '')}/`;
|
|
73
|
+
return {
|
|
74
|
+
label: 'Auth0',
|
|
75
|
+
client: {
|
|
76
|
+
issuer,
|
|
77
|
+
scopes: options.scopes ?? ['openid', 'profile', 'email', 'offline_access'],
|
|
78
|
+
...(options.audience ? { extraParams: { audience: options.audience } } : {}),
|
|
79
|
+
},
|
|
80
|
+
roles: options.rolesClaim ? rolesFrom.namespaced(options.rolesClaim) : rolesFrom.scope,
|
|
81
|
+
verifier: (clientId) => ({
|
|
82
|
+
issuer,
|
|
83
|
+
// The API identifier is the audience of an access token. Without one the token is
|
|
84
|
+
// opaque and there is nothing for a verifier to check.
|
|
85
|
+
audience: options.audience ?? false,
|
|
86
|
+
...(options.audience ? {} : { authorizedParty: clientId }),
|
|
87
|
+
}),
|
|
88
|
+
constraints: [
|
|
89
|
+
'Issues an opaque access token unless `audience` names an API, so token verification needs it.',
|
|
90
|
+
'A refresh token needs three things together: `offline_access` in the scopes, an audience whose API has Allow Offline Access on, and Refresh Token Rotation enabled on the application. Any one missing and the scope is ignored silently.',
|
|
91
|
+
'Has no roles claim of its own. An action must add a namespaced one, and Auth0 drops a custom claim that is not a URI.',
|
|
92
|
+
'The issuer carries a trailing slash. A verifier pinned without it rejects every token.',
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Okta.
|
|
98
|
+
*
|
|
99
|
+
* Two authorization servers that are not interchangeable: a custom one per application,
|
|
100
|
+
* and the org itself for Okta's own management API.
|
|
101
|
+
*/
|
|
102
|
+
okta(options) {
|
|
103
|
+
const host = `https://${options.domain.replace(/^https?:\/\//, '').replace(/\/+$/, '')}`;
|
|
104
|
+
const server = options.authorizationServer === undefined ? 'default' : options.authorizationServer;
|
|
105
|
+
const issuer = server === null ? host : `${host}/oauth2/${server}`;
|
|
106
|
+
return {
|
|
107
|
+
label: 'Okta',
|
|
108
|
+
client: {
|
|
109
|
+
issuer,
|
|
110
|
+
scopes: options.scopes ?? ['openid', 'profile', 'email', 'offline_access'],
|
|
111
|
+
dpop: options.dpop ?? true,
|
|
112
|
+
},
|
|
113
|
+
roles: rolesFrom.groups,
|
|
114
|
+
verifier: (clientId) => ({
|
|
115
|
+
issuer,
|
|
116
|
+
// An access token from a custom server is minted for that server's audience, named in
|
|
117
|
+
// the console and `api://default` by default. It does not carry the client id.
|
|
118
|
+
audience: false,
|
|
119
|
+
authorizedParty: clientId,
|
|
120
|
+
}),
|
|
121
|
+
constraints: [
|
|
122
|
+
'Two authorization servers: /oauth2/<name> for an application, the bare domain for the management API. They are not interchangeable.',
|
|
123
|
+
'Turns DPoP on by default for a new application, and an integrator org will not let you untick it.',
|
|
124
|
+
'Binds the refresh token to the DPoP key as well as the access token, so a refresh must be proved with the key that obtained it.',
|
|
125
|
+
'A public SPA cannot introspect at all. An API Services client with `private_key_jwt` has to ask instead.',
|
|
126
|
+
'Client credentials against the org server must use `private_key_jwt`, whatever the application is set to.',
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* Google.
|
|
132
|
+
*
|
|
133
|
+
* Refuses a public client outright, so server mode is the only option: the token endpoint
|
|
134
|
+
* checks for a client secret before it looks at PKCE.
|
|
135
|
+
*/
|
|
136
|
+
google(options = {}) {
|
|
137
|
+
return {
|
|
138
|
+
label: 'Google',
|
|
139
|
+
client: {
|
|
140
|
+
issuer: 'https://accounts.google.com',
|
|
141
|
+
scopes: options.scopes ?? ['openid', 'profile', 'email'],
|
|
142
|
+
...(options.hostedDomain ? { extraParams: { hd: options.hostedDomain } } : {}),
|
|
143
|
+
},
|
|
144
|
+
roles: rolesFrom.scope,
|
|
145
|
+
verifier: (clientId) => ({
|
|
146
|
+
// Names itself two ways, and a service pinned to one rejects tokens carrying the
|
|
147
|
+
// other. Discovery uses the first.
|
|
148
|
+
issuer: ['https://accounts.google.com', 'accounts.google.com'],
|
|
149
|
+
audience: clientId,
|
|
150
|
+
}),
|
|
151
|
+
constraints: [
|
|
152
|
+
'Refuses a public client: the token endpoint reports `client_secret is missing` before checking PKCE, so browser mode cannot work.',
|
|
153
|
+
'Publishes its issuer both with and without a scheme, so a verifier needs both spellings.',
|
|
154
|
+
'Issues a refresh token only on the first consent unless `prompt=consent` and `access_type=offline` are sent.',
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* Dex.
|
|
160
|
+
*
|
|
161
|
+
* Sends no CORS headers, on discovery or the token endpoint, so a browser cannot talk to it
|
|
162
|
+
* at all. Server mode only, and giving endpoints explicitly does not help: the token request
|
|
163
|
+
* is blocked too.
|
|
164
|
+
*/
|
|
165
|
+
dex(options) {
|
|
166
|
+
const issuer = trim(options.baseUrl);
|
|
167
|
+
return {
|
|
168
|
+
label: 'Dex',
|
|
169
|
+
client: {
|
|
170
|
+
issuer,
|
|
171
|
+
scopes: options.scopes ?? ['openid', 'profile', 'email', 'offline_access'],
|
|
172
|
+
},
|
|
173
|
+
roles: rolesFrom.groups,
|
|
174
|
+
verifier: (clientId) => ({ issuer, audience: clientId }),
|
|
175
|
+
constraints: [
|
|
176
|
+
'Sends no CORS headers on discovery or the token endpoint, so browser mode cannot work. Naming the endpoints explicitly only moves the failure to the token request.',
|
|
177
|
+
'Has no admin interface. Users are static entries in its config file, and a password is a bcrypt hash.',
|
|
178
|
+
'Publishes no revocation endpoint.',
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
package/dist/presets.mjs
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// src/roles.ts
|
|
2
|
+
var rolesFrom = {
|
|
3
|
+
/** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
|
|
4
|
+
keycloakRealm: (claims) => {
|
|
5
|
+
const realm = claims.realm_access;
|
|
6
|
+
return realm?.roles ?? [];
|
|
7
|
+
},
|
|
8
|
+
/** Keycloak, roles scoped to one client rather than the realm. */
|
|
9
|
+
keycloakClient: (clientId) => (claims) => {
|
|
10
|
+
const all = claims.resource_access;
|
|
11
|
+
return all?.[clientId]?.roles ?? [];
|
|
12
|
+
},
|
|
13
|
+
/** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
|
|
14
|
+
groups: (claims) => Array.isArray(claims.groups) ? claims.groups : [],
|
|
15
|
+
/** Cognito, which namespaces the claim. */
|
|
16
|
+
cognito: (claims) => {
|
|
17
|
+
const groups = claims["cognito:groups"];
|
|
18
|
+
return Array.isArray(groups) ? groups : [];
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* Auth0, which requires a namespaced custom claim added by an action.
|
|
22
|
+
*
|
|
23
|
+
* `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
|
|
24
|
+
* that is not a URI, so an empty list here usually means the namespace, not the action.
|
|
25
|
+
*/
|
|
26
|
+
namespaced: (claim) => (claims) => {
|
|
27
|
+
const value = claims[claim];
|
|
28
|
+
return Array.isArray(value) ? value : [];
|
|
29
|
+
},
|
|
30
|
+
/** The `scope` claim read as a role list, for a provider issuing no roles at all. */
|
|
31
|
+
scope: (claims) => typeof claims.scope === "string" ? claims.scope.split(" ") : []
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/presets.ts
|
|
35
|
+
var trim = (url) => url.replace(/\/+$/, "");
|
|
36
|
+
var presets = {
|
|
37
|
+
/**
|
|
38
|
+
* Any OIDC provider publishing a discovery document.
|
|
39
|
+
*
|
|
40
|
+
* The agnostic path, and the one to reach for first. A provider not listed below almost
|
|
41
|
+
* certainly works through this.
|
|
42
|
+
*/
|
|
43
|
+
oidc(options) {
|
|
44
|
+
return {
|
|
45
|
+
label: options.issuer,
|
|
46
|
+
client: { issuer: trim(options.issuer), scopes: options.scopes },
|
|
47
|
+
roles: options.roles ?? rolesFrom.scope,
|
|
48
|
+
verifier: (clientId) => ({
|
|
49
|
+
issuer: trim(options.issuer),
|
|
50
|
+
audience: options.audience ?? clientId,
|
|
51
|
+
...options.audience === false ? { authorizedParty: clientId } : {}
|
|
52
|
+
}),
|
|
53
|
+
constraints: []
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* A plain OAuth2 server with no discovery document.
|
|
58
|
+
*
|
|
59
|
+
* Every endpoint is given, because there is nothing to ask. Token verification needs an
|
|
60
|
+
* issuer regardless, since a verifier will not accept a token without one.
|
|
61
|
+
*/
|
|
62
|
+
oauth2(options) {
|
|
63
|
+
return {
|
|
64
|
+
label: options.issuer,
|
|
65
|
+
client: { endpoints: options.endpoints, scopes: options.scopes },
|
|
66
|
+
roles: options.roles ?? rolesFrom.scope,
|
|
67
|
+
verifier: (clientId) => ({
|
|
68
|
+
issuer: trim(options.issuer),
|
|
69
|
+
audience: options.audience ?? clientId,
|
|
70
|
+
...options.audience === false ? { authorizedParty: clientId } : {}
|
|
71
|
+
}),
|
|
72
|
+
constraints: ["Publishes no discovery document, so every endpoint is configured by hand."]
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
/** Keycloak. A realm is its own issuer. */
|
|
76
|
+
keycloak(options) {
|
|
77
|
+
const issuer = `${trim(options.baseUrl)}/realms/${options.realm}`;
|
|
78
|
+
return {
|
|
79
|
+
label: "Keycloak",
|
|
80
|
+
client: {
|
|
81
|
+
issuer,
|
|
82
|
+
scopes: options.scopes ?? ["openid", "profile", "email"]
|
|
83
|
+
},
|
|
84
|
+
roles: rolesFrom.keycloakRealm,
|
|
85
|
+
// No `aud` on an access token until an audience mapper is configured, and the client is
|
|
86
|
+
// named in `azp` instead. So the check moves to `authorizedParty` rather than being
|
|
87
|
+
// dropped: `audience: false` alone would accept a token minted for any client of the
|
|
88
|
+
// realm, which is the check being skipped.
|
|
89
|
+
verifier: (clientId) => ({ issuer, audience: false, authorizedParty: clientId }),
|
|
90
|
+
constraints: [
|
|
91
|
+
"Puts no `aud` on an access token until a dedicated audience mapper is configured. Configure one and switch the verifier to `audience: clientId`.",
|
|
92
|
+
"Revokes the whole session when an authorization code is replayed, so a replay test has to run last."
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Auth0.
|
|
98
|
+
*
|
|
99
|
+
* `audience` is the field that decides what kind of token comes back. Without it Auth0
|
|
100
|
+
* issues an opaque token for its own userinfo endpoint and the JWT checks have nothing to
|
|
101
|
+
* work with.
|
|
102
|
+
*/
|
|
103
|
+
auth0(options) {
|
|
104
|
+
const issuer = `https://${options.domain.replace(/^https?:\/\//, "").replace(/\/+$/, "")}/`;
|
|
105
|
+
return {
|
|
106
|
+
label: "Auth0",
|
|
107
|
+
client: {
|
|
108
|
+
issuer,
|
|
109
|
+
scopes: options.scopes ?? ["openid", "profile", "email", "offline_access"],
|
|
110
|
+
...options.audience ? { extraParams: { audience: options.audience } } : {}
|
|
111
|
+
},
|
|
112
|
+
roles: options.rolesClaim ? rolesFrom.namespaced(options.rolesClaim) : rolesFrom.scope,
|
|
113
|
+
verifier: (clientId) => ({
|
|
114
|
+
issuer,
|
|
115
|
+
// The API identifier is the audience of an access token. Without one the token is
|
|
116
|
+
// opaque and there is nothing for a verifier to check.
|
|
117
|
+
audience: options.audience ?? false,
|
|
118
|
+
...options.audience ? {} : { authorizedParty: clientId }
|
|
119
|
+
}),
|
|
120
|
+
constraints: [
|
|
121
|
+
"Issues an opaque access token unless `audience` names an API, so token verification needs it.",
|
|
122
|
+
"A refresh token needs three things together: `offline_access` in the scopes, an audience whose API has Allow Offline Access on, and Refresh Token Rotation enabled on the application. Any one missing and the scope is ignored silently.",
|
|
123
|
+
"Has no roles claim of its own. An action must add a namespaced one, and Auth0 drops a custom claim that is not a URI.",
|
|
124
|
+
"The issuer carries a trailing slash. A verifier pinned without it rejects every token."
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* Okta.
|
|
130
|
+
*
|
|
131
|
+
* Two authorization servers that are not interchangeable: a custom one per application,
|
|
132
|
+
* and the org itself for Okta's own management API.
|
|
133
|
+
*/
|
|
134
|
+
okta(options) {
|
|
135
|
+
const host = `https://${options.domain.replace(/^https?:\/\//, "").replace(/\/+$/, "")}`;
|
|
136
|
+
const server = options.authorizationServer === void 0 ? "default" : options.authorizationServer;
|
|
137
|
+
const issuer = server === null ? host : `${host}/oauth2/${server}`;
|
|
138
|
+
return {
|
|
139
|
+
label: "Okta",
|
|
140
|
+
client: {
|
|
141
|
+
issuer,
|
|
142
|
+
scopes: options.scopes ?? ["openid", "profile", "email", "offline_access"],
|
|
143
|
+
dpop: options.dpop ?? true
|
|
144
|
+
},
|
|
145
|
+
roles: rolesFrom.groups,
|
|
146
|
+
verifier: (clientId) => ({
|
|
147
|
+
issuer,
|
|
148
|
+
// An access token from a custom server is minted for that server's audience, named in
|
|
149
|
+
// the console and `api://default` by default. It does not carry the client id.
|
|
150
|
+
audience: false,
|
|
151
|
+
authorizedParty: clientId
|
|
152
|
+
}),
|
|
153
|
+
constraints: [
|
|
154
|
+
"Two authorization servers: /oauth2/<name> for an application, the bare domain for the management API. They are not interchangeable.",
|
|
155
|
+
"Turns DPoP on by default for a new application, and an integrator org will not let you untick it.",
|
|
156
|
+
"Binds the refresh token to the DPoP key as well as the access token, so a refresh must be proved with the key that obtained it.",
|
|
157
|
+
"A public SPA cannot introspect at all. An API Services client with `private_key_jwt` has to ask instead.",
|
|
158
|
+
"Client credentials against the org server must use `private_key_jwt`, whatever the application is set to."
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
/**
|
|
163
|
+
* Google.
|
|
164
|
+
*
|
|
165
|
+
* Refuses a public client outright, so server mode is the only option: the token endpoint
|
|
166
|
+
* checks for a client secret before it looks at PKCE.
|
|
167
|
+
*/
|
|
168
|
+
google(options = {}) {
|
|
169
|
+
return {
|
|
170
|
+
label: "Google",
|
|
171
|
+
client: {
|
|
172
|
+
issuer: "https://accounts.google.com",
|
|
173
|
+
scopes: options.scopes ?? ["openid", "profile", "email"],
|
|
174
|
+
...options.hostedDomain ? { extraParams: { hd: options.hostedDomain } } : {}
|
|
175
|
+
},
|
|
176
|
+
roles: rolesFrom.scope,
|
|
177
|
+
verifier: (clientId) => ({
|
|
178
|
+
// Names itself two ways, and a service pinned to one rejects tokens carrying the
|
|
179
|
+
// other. Discovery uses the first.
|
|
180
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
181
|
+
audience: clientId
|
|
182
|
+
}),
|
|
183
|
+
constraints: [
|
|
184
|
+
"Refuses a public client: the token endpoint reports `client_secret is missing` before checking PKCE, so browser mode cannot work.",
|
|
185
|
+
"Publishes its issuer both with and without a scheme, so a verifier needs both spellings.",
|
|
186
|
+
"Issues a refresh token only on the first consent unless `prompt=consent` and `access_type=offline` are sent."
|
|
187
|
+
]
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
/**
|
|
191
|
+
* Dex.
|
|
192
|
+
*
|
|
193
|
+
* Sends no CORS headers, on discovery or the token endpoint, so a browser cannot talk to it
|
|
194
|
+
* at all. Server mode only, and giving endpoints explicitly does not help: the token request
|
|
195
|
+
* is blocked too.
|
|
196
|
+
*/
|
|
197
|
+
dex(options) {
|
|
198
|
+
const issuer = trim(options.baseUrl);
|
|
199
|
+
return {
|
|
200
|
+
label: "Dex",
|
|
201
|
+
client: {
|
|
202
|
+
issuer,
|
|
203
|
+
scopes: options.scopes ?? ["openid", "profile", "email", "offline_access"]
|
|
204
|
+
},
|
|
205
|
+
roles: rolesFrom.groups,
|
|
206
|
+
verifier: (clientId) => ({ issuer, audience: clientId }),
|
|
207
|
+
constraints: [
|
|
208
|
+
"Sends no CORS headers on discovery or the token endpoint, so browser mode cannot work. Naming the endpoints explicitly only moves the failure to the token request.",
|
|
209
|
+
"Has no admin interface. Users are static entries in its config file, and a password is a bcrypt hash.",
|
|
210
|
+
"Publishes no revocation endpoint."
|
|
211
|
+
]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
export {
|
|
216
|
+
presets
|
|
217
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/return-to.ts
|
|
21
|
+
var return_to_exports = {};
|
|
22
|
+
__export(return_to_exports, {
|
|
23
|
+
safeReturnTo: () => safeReturnTo
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(return_to_exports);
|
|
26
|
+
function safeReturnTo(candidate, options = {}) {
|
|
27
|
+
const fallback = options.fallback ?? "/";
|
|
28
|
+
if (!candidate || typeof candidate !== "string") return fallback;
|
|
29
|
+
const value = candidate.trim();
|
|
30
|
+
if (!value) return fallback;
|
|
31
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) return fallback;
|
|
32
|
+
if (/^[/\\]{2}/.test(value)) return fallback;
|
|
33
|
+
if (value.startsWith("/")) return value;
|
|
34
|
+
const here = options.origin ?? (typeof location === "undefined" ? void 0 : location.origin);
|
|
35
|
+
const allowed = [here, ...options.allowedOrigins ?? []].filter(Boolean);
|
|
36
|
+
if (!allowed.length) return fallback;
|
|
37
|
+
let url;
|
|
38
|
+
try {
|
|
39
|
+
url = new URL(value);
|
|
40
|
+
} catch {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
if (!allowed.includes(url.origin)) return fallback;
|
|
44
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return fallback;
|
|
45
|
+
return url.toString();
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a completed login is allowed to land.
|
|
3
|
+
*
|
|
4
|
+
* A login flow carries a destination so a visitor resumes where they were interrupted,
|
|
5
|
+
* and that destination arrives from outside: a query parameter, a caller's argument, a
|
|
6
|
+
* value read back out of storage. Redirecting to it unchecked is an open redirect, and a
|
|
7
|
+
* login is the worst place to have one. The visitor has just authenticated, so the page
|
|
8
|
+
* they land on is trusted by them and reached with a live session.
|
|
9
|
+
*
|
|
10
|
+
* Sealing the value does not help. A signed cookie cannot be tampered with, and an
|
|
11
|
+
* attacker putting `?returnTo=https://evil.example/` on a link has no need to tamper: the
|
|
12
|
+
* application seals the attacker's value itself and honours it afterwards.
|
|
13
|
+
*
|
|
14
|
+
* So the rule is what the value is allowed to be, rather than where it came from. A path
|
|
15
|
+
* on this origin, and nothing else.
|
|
16
|
+
*/
|
|
17
|
+
/** What a destination has to look like to be used. */
|
|
18
|
+
export interface ReturnToOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Where to go when the destination is missing or refused. Default `/`.
|
|
21
|
+
*
|
|
22
|
+
* A refusal falls back rather than failing: someone who followed a poisoned link should
|
|
23
|
+
* still end up signed in, on a page of this application's choosing.
|
|
24
|
+
*/
|
|
25
|
+
fallback?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Origins allowed in addition to this one, for an application that really does span
|
|
28
|
+
* several. Each must be an exact origin, `https://app.example.com`, and is compared as
|
|
29
|
+
* one rather than by prefix, so `https://app.example.com.evil.test` cannot pass.
|
|
30
|
+
*/
|
|
31
|
+
allowedOrigins?: string[];
|
|
32
|
+
/** This origin, when it cannot be read from `location`. Required on a server. */
|
|
33
|
+
origin?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The destination to use, which is `candidate` when it is safe and the fallback when not.
|
|
37
|
+
*
|
|
38
|
+
* Refused, with reasons:
|
|
39
|
+
*
|
|
40
|
+
* - an absolute URL on another origin, which is the open redirect itself
|
|
41
|
+
* - `//host`, which a browser reads as protocol-relative and therefore another origin
|
|
42
|
+
* - `\\host`, which some browsers normalise to the same thing
|
|
43
|
+
* - a scheme of any kind, so `javascript:` and `data:` cannot be reached
|
|
44
|
+
* - a control character, which can be used to smuggle a newline past a naive check
|
|
45
|
+
* - anything that is not a path, since a bare `dashboard` resolves differently depending
|
|
46
|
+
* on where it is used
|
|
47
|
+
*/
|
|
48
|
+
export declare function safeReturnTo(candidate: string | null | undefined, options?: ReturnToOptions): string;
|
|
49
|
+
//# sourceMappingURL=return-to.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"return-to.d.ts","sourceRoot":"","sources":["../src/return-to.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,OAAO,GAAE,eAAoB,GAC5B,MAAM,CA0CR"}
|