@starci/auth 0.5.0 → 0.6.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/CHANGELOG.md +11 -0
- package/README.md +103 -14
- package/dist/client/oidc/index.d.ts +5 -0
- package/dist/client/oidc/index.js +3 -0
- package/dist/client/oidc/oidc-client.d.ts +85 -0
- package/dist/client/oidc/oidc-client.js +304 -0
- package/dist/client/oidc/pkce.d.ts +15 -0
- package/dist/client/oidc/pkce.js +29 -0
- package/dist/contracts/index.d.ts +12 -0
- package/dist/contracts/index.js +14 -0
- package/dist/contracts/oidc.d.ts +148 -0
- package/dist/contracts/oidc.js +68 -0
- package/dist/oidc/admin.d.ts +42 -0
- package/dist/oidc/admin.js +139 -0
- package/dist/oidc/broker.d.ts +57 -0
- package/dist/oidc/broker.js +77 -0
- package/dist/oidc/client.d.ts +58 -0
- package/dist/oidc/client.js +119 -0
- package/dist/oidc/discovery.d.ts +23 -0
- package/dist/oidc/discovery.js +64 -0
- package/dist/oidc/errors.d.ts +10 -0
- package/dist/oidc/errors.js +10 -0
- package/dist/oidc/grants.d.ts +41 -0
- package/dist/oidc/grants.js +82 -0
- package/dist/oidc/index.d.ts +35 -0
- package/dist/oidc/index.js +18 -0
- package/dist/oidc/issuers/github-actions.d.ts +54 -0
- package/dist/oidc/issuers/github-actions.js +84 -0
- package/dist/oidc/issuers/google.d.ts +30 -0
- package/dist/oidc/issuers/google.js +39 -0
- package/dist/oidc/issuers/index.d.ts +8 -0
- package/dist/oidc/issuers/index.js +4 -0
- package/dist/oidc/issuers/issuer.d.ts +79 -0
- package/dist/oidc/issuers/issuer.js +39 -0
- package/dist/oidc/issuers/keycloak.d.ts +26 -0
- package/dist/oidc/issuers/keycloak.js +41 -0
- package/dist/oidc/jwks.d.ts +26 -0
- package/dist/oidc/jwks.js +80 -0
- package/dist/oidc/oidc.d.ts +32 -0
- package/dist/oidc/oidc.js +21 -0
- package/dist/oidc/oidc.module.d.ts +34 -0
- package/dist/oidc/oidc.module.js +51 -0
- package/dist/oidc/oidc.tokens.d.ts +18 -0
- package/dist/oidc/oidc.tokens.js +18 -0
- package/dist/oidc/options.d.ts +119 -0
- package/dist/oidc/options.js +98 -0
- package/dist/oidc/primitives.d.ts +15 -0
- package/dist/oidc/primitives.js +11 -0
- package/dist/oidc/refresh.d.ts +10 -0
- package/dist/oidc/refresh.js +24 -0
- package/dist/oidc/roles.d.ts +12 -0
- package/dist/oidc/roles.js +51 -0
- package/dist/oidc/tokens.d.ts +26 -0
- package/dist/oidc/tokens.js +79 -0
- package/dist/oidc/transport.d.ts +8 -0
- package/dist/oidc/transport.js +19 -0
- package/dist/oidc/verifier.d.ts +24 -0
- package/dist/oidc/verifier.js +64 -0
- package/dist/server/auth.module.d.ts +1 -1
- package/dist/server/auth.module.js +20 -6
- package/dist/server/auth.tokens.d.ts +2 -0
- package/dist/server/auth.tokens.js +2 -0
- package/dist/server/doors.d.ts +14 -8
- package/dist/server/doors.js +22 -11
- package/dist/server/flows.d.ts +13 -5
- package/dist/server/flows.js +37 -18
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +3 -4
- package/dist/server/options.d.ts +11 -13
- package/dist/server/ports.d.ts +8 -1
- package/dist/server/providers/github.d.ts +59 -0
- package/dist/server/providers/github.js +111 -0
- package/dist/server/providers/google.d.ts +56 -0
- package/dist/server/providers/google.js +87 -0
- package/dist/server/providers/index.d.ts +14 -0
- package/dist/server/providers/index.js +9 -0
- package/dist/server/providers/oauth-state.d.ts +47 -0
- package/dist/server/providers/oauth-state.js +74 -0
- package/dist/server/providers/provider.d.ts +58 -0
- package/dist/server/providers/provider.js +32 -0
- package/dist/server/providers/registry.d.ts +30 -0
- package/dist/server/providers/registry.js +37 -0
- package/dist/server/strategies.d.ts +3 -18
- package/dist/server/strategies.js +2 -41
- package/dist/server/verifier.d.ts +1 -1
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/oidc.d.ts +175 -0
- package/dist/testing/oidc.js +493 -0
- package/package.json +22 -7
- package/dist/server/google-state.d.ts +0 -33
- package/dist/server/google-state.js +0 -73
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { OIDC_CLAIMS, OIDC_ERROR_CODES, OIDC_SUBJECT_MAX_LENGTH } from "../contracts/oidc.js";
|
|
2
|
+
import { OidcError } from "./errors.js";
|
|
3
|
+
import { isRecord, revealed, sameValue } from "./primitives.js";
|
|
4
|
+
import { refreshOf } from "./refresh.js";
|
|
5
|
+
import { rolesOf } from "./roles.js";
|
|
6
|
+
const rejected = (cause) => new OidcError(OIDC_ERROR_CODES.Rejected, cause === undefined ? {} : { cause });
|
|
7
|
+
/**
|
|
8
|
+
* The authorization code flow of one registered client: it builds the authorization address (S256 PKCE, nonce, state), exchanges a
|
|
9
|
+
* code once at the token endpoint and verifies the ID token (signature, issuer, audience = the client, expiry, nonce) before it answers
|
|
10
|
+
* an identity, and builds the RP-initiated logout address. It never logs a provider token; the refresh token a code exchange answers is for the
|
|
11
|
+
* server's own store, never for a browser.
|
|
12
|
+
*/
|
|
13
|
+
export class OidcClient {
|
|
14
|
+
#options;
|
|
15
|
+
#discovery;
|
|
16
|
+
#tokens;
|
|
17
|
+
constructor(options, discovery, tokens) {
|
|
18
|
+
this.#options = options;
|
|
19
|
+
this.#discovery = discovery;
|
|
20
|
+
this.#tokens = tokens;
|
|
21
|
+
}
|
|
22
|
+
/** The address the browser goes to for the registered code flow. */
|
|
23
|
+
async authorization(input) {
|
|
24
|
+
const client = this.#registration();
|
|
25
|
+
const metadata = await this.#discovery.get();
|
|
26
|
+
const url = new URL(metadata.authorizationEndpoint);
|
|
27
|
+
url.search = new URLSearchParams({
|
|
28
|
+
response_type: "code",
|
|
29
|
+
client_id: client.clientId,
|
|
30
|
+
redirect_uri: client.callbackUri,
|
|
31
|
+
scope: client.scope ?? "openid",
|
|
32
|
+
state: input.state,
|
|
33
|
+
nonce: input.nonce,
|
|
34
|
+
code_challenge: input.challenge,
|
|
35
|
+
code_challenge_method: "S256",
|
|
36
|
+
...(input.prompt === undefined ? {} : { prompt: input.prompt }),
|
|
37
|
+
...(input.idpHint === undefined ? {} : { kc_idp_hint: input.idpHint }),
|
|
38
|
+
}).toString();
|
|
39
|
+
return url.toString();
|
|
40
|
+
}
|
|
41
|
+
/** Exchanges a claimed code once, then verifies the ID token and answers the identity it asserts. */
|
|
42
|
+
async exchange(input) {
|
|
43
|
+
const client = this.#registration();
|
|
44
|
+
if (input.issuer !== undefined && input.issuer !== this.#options.issuer)
|
|
45
|
+
throw rejected();
|
|
46
|
+
const metadata = await this.#discovery.get();
|
|
47
|
+
const response = await this.#options.http
|
|
48
|
+
.request({
|
|
49
|
+
method: "POST",
|
|
50
|
+
url: metadata.tokenEndpoint,
|
|
51
|
+
timeoutMs: this.#options.timeoutMs,
|
|
52
|
+
form: {
|
|
53
|
+
grant_type: "authorization_code",
|
|
54
|
+
client_id: client.clientId,
|
|
55
|
+
...(client.clientSecret === undefined ? {} : { client_secret: revealed(client.clientSecret) }),
|
|
56
|
+
redirect_uri: client.callbackUri,
|
|
57
|
+
code: input.code,
|
|
58
|
+
code_verifier: input.verifier,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
.catch((cause) => {
|
|
62
|
+
throw new OidcError(OIDC_ERROR_CODES.Unavailable, { cause });
|
|
63
|
+
});
|
|
64
|
+
if (response.status >= 500)
|
|
65
|
+
throw new OidcError(OIDC_ERROR_CODES.Unavailable);
|
|
66
|
+
if (response.status !== 200 || !isRecord(response.body) || typeof response.body.id_token !== "string")
|
|
67
|
+
throw rejected();
|
|
68
|
+
const identity = await this.#identityOf(response.body.id_token, input.nonce);
|
|
69
|
+
const refresh = refreshOf(response.body, this.#options.clock.now());
|
|
70
|
+
return refresh === undefined ? identity : { ...identity, ...refresh };
|
|
71
|
+
}
|
|
72
|
+
/** The RP-initiated logout address of the registered client. */
|
|
73
|
+
async logout(input = {}) {
|
|
74
|
+
const client = this.#registration();
|
|
75
|
+
const { endSessionEndpoint } = await this.#discovery.get();
|
|
76
|
+
if (endSessionEndpoint === undefined)
|
|
77
|
+
throw rejected();
|
|
78
|
+
const url = new URL(endSessionEndpoint);
|
|
79
|
+
url.search = new URLSearchParams({
|
|
80
|
+
client_id: client.clientId,
|
|
81
|
+
post_logout_redirect_uri: client.postLogoutUri,
|
|
82
|
+
...(input.idTokenHint === undefined ? {} : { id_token_hint: input.idTokenHint }),
|
|
83
|
+
...(input.state === undefined ? {} : { state: input.state }),
|
|
84
|
+
}).toString();
|
|
85
|
+
return url.toString();
|
|
86
|
+
}
|
|
87
|
+
#registration() {
|
|
88
|
+
const client = this.#options.client;
|
|
89
|
+
if (client === undefined)
|
|
90
|
+
throw new OidcError(OIDC_ERROR_CODES.ConfigurationInvalid, { params: { key: "client" } });
|
|
91
|
+
return client;
|
|
92
|
+
}
|
|
93
|
+
async #identityOf(idToken, nonce) {
|
|
94
|
+
const client = this.#registration();
|
|
95
|
+
const verified = await this.#tokens.verify(idToken, { audience: [client.clientId], requiredClaims: [OIDC_CLAIMS.nonce] });
|
|
96
|
+
if (verified.kind === "refused")
|
|
97
|
+
throw rejected();
|
|
98
|
+
const claims = verified.value;
|
|
99
|
+
const received = claims[OIDC_CLAIMS.nonce];
|
|
100
|
+
if (typeof received !== "string" || !sameValue(received, nonce))
|
|
101
|
+
throw rejected();
|
|
102
|
+
const { sub, exp } = claims;
|
|
103
|
+
if (typeof sub !== "string" || sub === "" || sub.length > OIDC_SUBJECT_MAX_LENGTH || typeof exp !== "number" || !Number.isSafeInteger(exp))
|
|
104
|
+
throw rejected();
|
|
105
|
+
const roles = rolesOf(claims, { realm: this.#options.realmRoles, clients: this.#options.roleClients, claims: this.#options.roleClaims });
|
|
106
|
+
if (roles === undefined)
|
|
107
|
+
throw rejected();
|
|
108
|
+
const email = claims[OIDC_CLAIMS.email];
|
|
109
|
+
const sessionId = claims[OIDC_CLAIMS.sessionId];
|
|
110
|
+
return {
|
|
111
|
+
issuer: this.#options.issuer,
|
|
112
|
+
subject: sub,
|
|
113
|
+
roles,
|
|
114
|
+
...(typeof email === "string" && email !== "" ? { email } : {}),
|
|
115
|
+
...(typeof sessionId === "string" && sessionId !== "" ? { sessionId } : {}),
|
|
116
|
+
expiresAt: new Date(exp * 1000),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ResolvedOidcOptions } from "./options.js";
|
|
2
|
+
/** The endpoints of the discovery document the package uses, each validated against the issuer. */
|
|
3
|
+
export interface OidcMetadata {
|
|
4
|
+
readonly issuer: string;
|
|
5
|
+
readonly authorizationEndpoint: string;
|
|
6
|
+
readonly tokenEndpoint: string;
|
|
7
|
+
readonly jwksUri: string;
|
|
8
|
+
/** Absent when the provider offers no RP-initiated logout. */
|
|
9
|
+
readonly endSessionEndpoint: string | undefined;
|
|
10
|
+
/** The RFC 7009 token revocation endpoint; absent when the provider advertises none. */
|
|
11
|
+
readonly revocationEndpoint: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** The discovery address of an issuer. */
|
|
14
|
+
export declare const discoveryUrl: (issuer: string) => string;
|
|
15
|
+
/** Narrows a discovery document: its `issuer` is the configured issuer exactly, and every endpoint passes `endpointOf`. */
|
|
16
|
+
export declare const readMetadata: (value: unknown, options: ResolvedOidcOptions) => OidcMetadata;
|
|
17
|
+
/** Fetches and caches the discovery document of the issuer; concurrent callers share one request. */
|
|
18
|
+
export declare class OidcDiscovery {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(options: ResolvedOidcOptions);
|
|
21
|
+
/** The metadata: the cached document while it is fresh, else a new one. A provider that cannot be asked throws `Unavailable`; the cached document is never used past its life, so an outage never keeps stale endpoints. */
|
|
22
|
+
get(): Promise<OidcMetadata>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { OIDC_ERROR_CODES } from "../contracts/oidc.js";
|
|
2
|
+
import { OidcError } from "./errors.js";
|
|
3
|
+
import { isAdmissibleUrl } from "./options.js";
|
|
4
|
+
import { isRecord } from "./primitives.js";
|
|
5
|
+
const DISCOVERY_PATH = "/.well-known/openid-configuration";
|
|
6
|
+
/** The discovery address of an issuer. */
|
|
7
|
+
export const discoveryUrl = (issuer) => `${issuer.replace(/\/$/u, "")}${DISCOVERY_PATH}`;
|
|
8
|
+
const rejected = () => new OidcError(OIDC_ERROR_CODES.Rejected);
|
|
9
|
+
/**
|
|
10
|
+
* An endpoint the provider advertises is trusted only on the issuer's own origin, over the admitted scheme, without credentials or a
|
|
11
|
+
* fragment: a discovery document cannot send the client secret or the browser to another host.
|
|
12
|
+
*/
|
|
13
|
+
const endpointOf = (value, options) => {
|
|
14
|
+
if (typeof value !== "string" || !isAdmissibleUrl(value, options.allowInsecureHttp))
|
|
15
|
+
throw rejected();
|
|
16
|
+
if (new URL(value).origin !== new URL(options.issuer).origin)
|
|
17
|
+
throw rejected();
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
/** Narrows a discovery document: its `issuer` is the configured issuer exactly, and every endpoint passes `endpointOf`. */
|
|
21
|
+
export const readMetadata = (value, options) => {
|
|
22
|
+
if (!isRecord(value) || value.issuer !== options.issuer)
|
|
23
|
+
throw rejected();
|
|
24
|
+
return {
|
|
25
|
+
issuer: options.issuer,
|
|
26
|
+
authorizationEndpoint: endpointOf(value.authorization_endpoint, options),
|
|
27
|
+
tokenEndpoint: endpointOf(value.token_endpoint, options),
|
|
28
|
+
jwksUri: endpointOf(value.jwks_uri, options),
|
|
29
|
+
endSessionEndpoint: value.end_session_endpoint === undefined ? undefined : endpointOf(value.end_session_endpoint, options),
|
|
30
|
+
revocationEndpoint: value.revocation_endpoint === undefined ? undefined : endpointOf(value.revocation_endpoint, options),
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** Fetches and caches the discovery document of the issuer; concurrent callers share one request. */
|
|
34
|
+
export class OidcDiscovery {
|
|
35
|
+
#options;
|
|
36
|
+
#held;
|
|
37
|
+
#pending;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.#options = options;
|
|
40
|
+
}
|
|
41
|
+
/** The metadata: the cached document while it is fresh, else a new one. A provider that cannot be asked throws `Unavailable`; the cached document is never used past its life, so an outage never keeps stale endpoints. */
|
|
42
|
+
async get() {
|
|
43
|
+
const now = this.#options.clock.now().getTime();
|
|
44
|
+
if (this.#held !== undefined && now - this.#held.at < this.#options.discoveryTtlMs)
|
|
45
|
+
return this.#held.metadata;
|
|
46
|
+
this.#pending ??= this.#fetch().finally(() => {
|
|
47
|
+
this.#pending = undefined;
|
|
48
|
+
});
|
|
49
|
+
return this.#pending;
|
|
50
|
+
}
|
|
51
|
+
async #fetch() {
|
|
52
|
+
const options = this.#options;
|
|
53
|
+
const response = await options.http
|
|
54
|
+
.request({ method: "GET", url: discoveryUrl(options.issuer), timeoutMs: options.timeoutMs })
|
|
55
|
+
.catch((cause) => {
|
|
56
|
+
throw new OidcError(OIDC_ERROR_CODES.Unavailable, { cause });
|
|
57
|
+
});
|
|
58
|
+
if (response.status !== 200)
|
|
59
|
+
throw new OidcError(OIDC_ERROR_CODES.Unavailable);
|
|
60
|
+
const metadata = readMetadata(response.body, options);
|
|
61
|
+
this.#held = { metadata, at: options.clock.now().getTime() };
|
|
62
|
+
return metadata;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DomainError } from "@starci/common";
|
|
2
|
+
import type { OidcErrorCode, OidcErrorKind } from "../contracts/oidc.js";
|
|
3
|
+
/** The one error class the package throws: a stable code, the kind it travels as, the key a configuration error names and the cause, never provider prose or a token. */
|
|
4
|
+
export declare class OidcError extends DomainError<OidcErrorCode> {
|
|
5
|
+
readonly kind: OidcErrorKind;
|
|
6
|
+
constructor(code: OidcErrorCode, options?: {
|
|
7
|
+
readonly cause?: unknown;
|
|
8
|
+
readonly params?: Readonly<Record<string, string>>;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DomainError } from "@starci/common";
|
|
2
|
+
import { OIDC_ERROR_KINDS } from "../contracts/oidc.js";
|
|
3
|
+
/** The one error class the package throws: a stable code, the kind it travels as, the key a configuration error names and the cause, never provider prose or a token. */
|
|
4
|
+
export class OidcError extends DomainError {
|
|
5
|
+
kind;
|
|
6
|
+
constructor(code, options = {}) {
|
|
7
|
+
super({ code, params: options.params ?? {}, ...(options.cause === undefined ? {} : { cause: options.cause }) });
|
|
8
|
+
this.kind = OIDC_ERROR_KINDS[code];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { OidcPrincipal } from "../contracts/oidc.js";
|
|
2
|
+
import type { OidcDiscovery } from "./discovery.js";
|
|
3
|
+
import type { ResolvedOidcOptions } from "./options.js";
|
|
4
|
+
import type { OidcPrincipalVerifier } from "./verifier.js";
|
|
5
|
+
/** What a grant at the token endpoint established: the verified principal and the refresh token of the provider session. The access token is verified here and dropped. */
|
|
6
|
+
export interface OidcGrant {
|
|
7
|
+
/** The principal the access token stands for (subject, mapped roles, email, session). */
|
|
8
|
+
readonly principal: OidcPrincipal;
|
|
9
|
+
/** The account: the `sub` of the access token. */
|
|
10
|
+
readonly subject: string;
|
|
11
|
+
/** The roles the issuer granted, mapped by the registration. */
|
|
12
|
+
readonly roles: readonly string[];
|
|
13
|
+
/** The verified email, or null when the token has none. */
|
|
14
|
+
readonly email: string | null;
|
|
15
|
+
/** The refresh token of the provider session. It stays on the server. */
|
|
16
|
+
readonly refreshToken: string;
|
|
17
|
+
/** When the refresh token stops being accepted. */
|
|
18
|
+
readonly refreshExpiresAt: Date;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The server-side grants of the registered client at the provider's token endpoint: the resource owner password grant, the refresh
|
|
22
|
+
* token grant and the revocation of a refresh token. Every answer is checked by the registration's `PrincipalVerifier`, so the result
|
|
23
|
+
* carries the same verified principal a bearer token would, plus the refresh token and its expiry; the access token is never returned,
|
|
24
|
+
* so it can never reach a browser. Nothing here logs a password or a token.
|
|
25
|
+
*/
|
|
26
|
+
export declare class OidcTokenClient {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(options: ResolvedOidcOptions, discovery: OidcDiscovery, verifier: OidcPrincipalVerifier);
|
|
29
|
+
/**
|
|
30
|
+
* Signs a person in with a username (or the email the realm accepts) and a password. A refusal of the credentials (`invalid_grant`: bad
|
|
31
|
+
* password, unknown or disabled account) throws `CredentialsRejected`; an outage throws `Unavailable`.
|
|
32
|
+
*/
|
|
33
|
+
passwordGrant(username: string, password: string): Promise<OidcGrant>;
|
|
34
|
+
/** Trades a refresh token for a new session grant (the provider rotates the refresh token). A dead refresh token (`invalid_grant`) throws `CredentialsRejected`. */
|
|
35
|
+
refresh(refreshToken: string): Promise<OidcGrant>;
|
|
36
|
+
/**
|
|
37
|
+
* Ends the provider session of a refresh token. It asks the discovered `revocation_endpoint` (RFC 7009); a provider that advertises none
|
|
38
|
+
* is asked at its end-session endpoint with the refresh token. A token the provider no longer knows is already ended and is no error.
|
|
39
|
+
*/
|
|
40
|
+
revoke(refreshToken: string): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { OIDC_ERROR_CODES } from "../contracts/oidc.js";
|
|
2
|
+
import { OidcError } from "./errors.js";
|
|
3
|
+
import { isRecord, revealed } from "./primitives.js";
|
|
4
|
+
import { refreshOf } from "./refresh.js";
|
|
5
|
+
import { errorCodeOf, send } from "./transport.js";
|
|
6
|
+
const rejected = (cause) => new OidcError(OIDC_ERROR_CODES.Rejected, cause === undefined ? {} : { cause });
|
|
7
|
+
/**
|
|
8
|
+
* The server-side grants of the registered client at the provider's token endpoint: the resource owner password grant, the refresh
|
|
9
|
+
* token grant and the revocation of a refresh token. Every answer is checked by the registration's `PrincipalVerifier`, so the result
|
|
10
|
+
* carries the same verified principal a bearer token would, plus the refresh token and its expiry; the access token is never returned,
|
|
11
|
+
* so it can never reach a browser. Nothing here logs a password or a token.
|
|
12
|
+
*/
|
|
13
|
+
export class OidcTokenClient {
|
|
14
|
+
#options;
|
|
15
|
+
#discovery;
|
|
16
|
+
#verifier;
|
|
17
|
+
constructor(options, discovery, verifier) {
|
|
18
|
+
this.#options = options;
|
|
19
|
+
this.#discovery = discovery;
|
|
20
|
+
this.#verifier = verifier;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Signs a person in with a username (or the email the realm accepts) and a password. A refusal of the credentials (`invalid_grant`: bad
|
|
24
|
+
* password, unknown or disabled account) throws `CredentialsRejected`; an outage throws `Unavailable`.
|
|
25
|
+
*/
|
|
26
|
+
async passwordGrant(username, password) {
|
|
27
|
+
const client = this.#registration();
|
|
28
|
+
return this.#grant({ grant_type: "password", username, password, scope: client.scope ?? "openid" });
|
|
29
|
+
}
|
|
30
|
+
/** Trades a refresh token for a new session grant (the provider rotates the refresh token). A dead refresh token (`invalid_grant`) throws `CredentialsRejected`. */
|
|
31
|
+
async refresh(refreshToken) {
|
|
32
|
+
this.#registration();
|
|
33
|
+
return this.#grant({ grant_type: "refresh_token", refresh_token: refreshToken });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Ends the provider session of a refresh token. It asks the discovered `revocation_endpoint` (RFC 7009); a provider that advertises none
|
|
37
|
+
* is asked at its end-session endpoint with the refresh token. A token the provider no longer knows is already ended and is no error.
|
|
38
|
+
*/
|
|
39
|
+
async revoke(refreshToken) {
|
|
40
|
+
const client = this.#registration();
|
|
41
|
+
const metadata = await this.#discovery.get();
|
|
42
|
+
const url = metadata.revocationEndpoint ?? metadata.endSessionEndpoint;
|
|
43
|
+
if (url === undefined)
|
|
44
|
+
throw rejected();
|
|
45
|
+
const credentials = { client_id: client.clientId, ...(client.clientSecret === undefined ? {} : { client_secret: revealed(client.clientSecret) }) };
|
|
46
|
+
const form = metadata.revocationEndpoint === undefined ? { ...credentials, refresh_token: refreshToken } : { ...credentials, token: refreshToken, token_type_hint: "refresh_token" };
|
|
47
|
+
const response = await send(this.#options, { method: "POST", url, form });
|
|
48
|
+
if (response.status >= 200 && response.status < 300)
|
|
49
|
+
return;
|
|
50
|
+
if (response.status === 400 && errorCodeOf(response.body) === "invalid_grant")
|
|
51
|
+
return;
|
|
52
|
+
throw rejected();
|
|
53
|
+
}
|
|
54
|
+
#registration() {
|
|
55
|
+
const client = this.#options.client;
|
|
56
|
+
if (client === undefined)
|
|
57
|
+
throw new OidcError(OIDC_ERROR_CODES.ConfigurationInvalid, { params: { key: "client" } });
|
|
58
|
+
return client;
|
|
59
|
+
}
|
|
60
|
+
async #grant(params) {
|
|
61
|
+
const client = this.#registration();
|
|
62
|
+
const { tokenEndpoint } = await this.#discovery.get();
|
|
63
|
+
const response = await send(this.#options, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
url: tokenEndpoint,
|
|
66
|
+
form: { client_id: client.clientId, ...(client.clientSecret === undefined ? {} : { client_secret: revealed(client.clientSecret) }), ...params },
|
|
67
|
+
});
|
|
68
|
+
if (response.status !== 200)
|
|
69
|
+
throw errorCodeOf(response.body) === "invalid_grant" ? new OidcError(OIDC_ERROR_CODES.CredentialsRejected) : rejected();
|
|
70
|
+
const body = response.body;
|
|
71
|
+
if (!isRecord(body) || typeof body.access_token !== "string")
|
|
72
|
+
throw rejected();
|
|
73
|
+
const refresh = refreshOf(body, this.#options.clock.now());
|
|
74
|
+
if (refresh === undefined)
|
|
75
|
+
throw rejected();
|
|
76
|
+
const verified = await this.#verifier.verify(body.access_token);
|
|
77
|
+
if (verified.kind === "refused")
|
|
78
|
+
throw rejected();
|
|
79
|
+
const principal = verified.value;
|
|
80
|
+
return { principal, subject: principal.id, roles: principal.roles, email: principal.email ?? null, ...refresh };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { OidcAdminClient, adminBaseOf } from "./admin.js";
|
|
2
|
+
export type { OidcCreatedUser, OidcFoundUser, OidcNewUser } from "./admin.js";
|
|
3
|
+
export { OidcBroker } from "./broker.js";
|
|
4
|
+
export type { OidcBrokerSession, OidcSocialCallback, OidcSocialRequest } from "./broker.js";
|
|
5
|
+
export { OidcClient } from "./client.js";
|
|
6
|
+
export type { OidcAuthorization, OidcExchange, OidcExchangeResult, OidcLogout } from "./client.js";
|
|
7
|
+
export { OidcDiscovery, discoveryUrl, readMetadata } from "./discovery.js";
|
|
8
|
+
export type { OidcMetadata } from "./discovery.js";
|
|
9
|
+
export { OidcError } from "./errors.js";
|
|
10
|
+
export { OidcTokenClient } from "./grants.js";
|
|
11
|
+
export type { OidcGrant } from "./grants.js";
|
|
12
|
+
export { OidcKeys, readKeySet } from "./jwks.js";
|
|
13
|
+
export type { OidcKeySet } from "./jwks.js";
|
|
14
|
+
export { OidcModule, MODULE_OPTIONS_TOKEN as OIDC_OPTIONS } from "./oidc.module.js";
|
|
15
|
+
export type { OidcModuleExtras, OidcRegisterAsyncOptions, OidcRegisterOptions } from "./oidc.module.js";
|
|
16
|
+
export { createOidc } from "./oidc.js";
|
|
17
|
+
export type { Oidc } from "./oidc.js";
|
|
18
|
+
export { OIDC_ADMIN_CLIENT, OIDC_BROKER, OIDC_CLIENT, OIDC_DISCOVERY, OIDC_KEYS, OIDC_PRINCIPAL_VERIFIER, OIDC_REGISTRATION, OIDC_TOKEN_CLIENT, OIDC_TOKEN_VERIFIER } from "./oidc.tokens.js";
|
|
19
|
+
export { fetchHttp, isAdmissibleUrl, resolveOptions } from "./options.js";
|
|
20
|
+
export type { OidcClientRegistration, OidcHttp, OidcHttpRequest, OidcHttpResponse, OidcJwksOptions, OidcOptions, OidcRoleMapping, ResolvedOidcOptions, } from "./options.js";
|
|
21
|
+
export { isRecord, pkceChallenge, randomReference, sameValue, systemClock } from "./primitives.js";
|
|
22
|
+
export type { OidcClock, Revealable, SecretInput } from "./primitives.js";
|
|
23
|
+
export { refreshOf } from "./refresh.js";
|
|
24
|
+
export type { OidcRefresh } from "./refresh.js";
|
|
25
|
+
export { rolesOf } from "./roles.js";
|
|
26
|
+
export type { RoleSources } from "./roles.js";
|
|
27
|
+
export { OidcTokenVerifier } from "./tokens.js";
|
|
28
|
+
export type { TokenExpectations } from "./tokens.js";
|
|
29
|
+
export { OidcPrincipalVerifier, principalOf, refuseSessions } from "./verifier.js";
|
|
30
|
+
export type { PrincipalRules } from "./verifier.js";
|
|
31
|
+
export type { OidcKeySource } from "./jwks.js";
|
|
32
|
+
export { GITHUB_ACTIONS_CLAIMS, GITHUB_ACTIONS_ISSUER, GITHUB_ACTIONS_JWKS_URI, GOOGLE_ISSUER, GOOGLE_ISSUER_ALIAS, GOOGLE_JWKS_URI, createIssuerVerifier, githubActionsIssuer, googleIssuer, keycloakIssuer, keycloakIssuerUrl, keycloakOptions, mapped, unmapped, } from "./issuers/index.js";
|
|
33
|
+
export type { GitHubActionsIssuerOptions, GitHubActionsRun, GoogleIdentity, GoogleIssuerOptions, IssuerEndpoints, IssuerKeys, IssuerMapping, IssuerPreset, IssuerVerifier, IssuerVerifierOptions, KeycloakIssuerOptions, } from "./issuers/index.js";
|
|
34
|
+
export { OIDC_ACCESS_TOKEN_INVALID, OIDC_ALGORITHMS, OIDC_CLAIMS, OIDC_CLIENT_CODES, OIDC_DEFAULT_ALGORITHMS, OIDC_ERROR_CODES, OIDC_ERROR_KINDS, OIDC_ROLE, OIDC_ROLE_BOUNDS, OIDC_SUBJECT_MAX_LENGTH, } from "../contracts/oidc.js";
|
|
35
|
+
export type { OidcAlgorithm, OidcClientOutcome, OidcErrorCode, OidcErrorKind, OidcFailureKind, OidcIdentity, OidcOutcome, OidcPrincipal, OidcRefusal, OidcRefusalReason, OidcSessionUser, PrincipalVerifier, } from "../contracts/oidc.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { OidcAdminClient, adminBaseOf } from "./admin.js";
|
|
2
|
+
export { OidcBroker } from "./broker.js";
|
|
3
|
+
export { OidcClient } from "./client.js";
|
|
4
|
+
export { OidcDiscovery, discoveryUrl, readMetadata } from "./discovery.js";
|
|
5
|
+
export { OidcError } from "./errors.js";
|
|
6
|
+
export { OidcTokenClient } from "./grants.js";
|
|
7
|
+
export { OidcKeys, readKeySet } from "./jwks.js";
|
|
8
|
+
export { OidcModule, MODULE_OPTIONS_TOKEN as OIDC_OPTIONS } from "./oidc.module.js";
|
|
9
|
+
export { createOidc } from "./oidc.js";
|
|
10
|
+
export { OIDC_ADMIN_CLIENT, OIDC_BROKER, OIDC_CLIENT, OIDC_DISCOVERY, OIDC_KEYS, OIDC_PRINCIPAL_VERIFIER, OIDC_REGISTRATION, OIDC_TOKEN_CLIENT, OIDC_TOKEN_VERIFIER } from "./oidc.tokens.js";
|
|
11
|
+
export { fetchHttp, isAdmissibleUrl, resolveOptions } from "./options.js";
|
|
12
|
+
export { isRecord, pkceChallenge, randomReference, sameValue, systemClock } from "./primitives.js";
|
|
13
|
+
export { refreshOf } from "./refresh.js";
|
|
14
|
+
export { rolesOf } from "./roles.js";
|
|
15
|
+
export { OidcTokenVerifier } from "./tokens.js";
|
|
16
|
+
export { OidcPrincipalVerifier, principalOf, refuseSessions } from "./verifier.js";
|
|
17
|
+
export { GITHUB_ACTIONS_CLAIMS, GITHUB_ACTIONS_ISSUER, GITHUB_ACTIONS_JWKS_URI, GOOGLE_ISSUER, GOOGLE_ISSUER_ALIAS, GOOGLE_JWKS_URI, createIssuerVerifier, githubActionsIssuer, googleIssuer, keycloakIssuer, keycloakIssuerUrl, keycloakOptions, mapped, unmapped, } from "./issuers/index.js";
|
|
18
|
+
export { OIDC_ACCESS_TOKEN_INVALID, OIDC_ALGORITHMS, OIDC_CLAIMS, OIDC_CLIENT_CODES, OIDC_DEFAULT_ALGORITHMS, OIDC_ERROR_CODES, OIDC_ERROR_KINDS, OIDC_ROLE, OIDC_ROLE_BOUNDS, OIDC_SUBJECT_MAX_LENGTH, } from "../contracts/oidc.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IssuerEndpoints, IssuerPreset } from "./issuer.js";
|
|
2
|
+
/** The issuer of GitHub Actions OIDC tokens. */
|
|
3
|
+
export declare const GITHUB_ACTIONS_ISSUER = "https://token.actions.githubusercontent.com";
|
|
4
|
+
/** Its signing keys. */
|
|
5
|
+
export declare const GITHUB_ACTIONS_JWKS_URI = "https://token.actions.githubusercontent.com/.well-known/jwks";
|
|
6
|
+
/** The claims a CI run token must carry beyond `sub` and `exp`. */
|
|
7
|
+
export declare const GITHUB_ACTIONS_CLAIMS: readonly ["repository", "repository_id", "workflow", "workflow_ref", "ref", "run_id", "sha"];
|
|
8
|
+
/** Which runs the app trusts: always one repository, optionally pinned further. */
|
|
9
|
+
export interface GitHubActionsIssuerOptions {
|
|
10
|
+
/** The audience the workflow asked for (`core.getIDToken(audience)`): required, so a token minted for another service never passes. */
|
|
11
|
+
readonly audience: string | readonly string[];
|
|
12
|
+
/** The repository the run belongs to (`owner/name`), compared without case as GitHub names are. */
|
|
13
|
+
readonly repository: string;
|
|
14
|
+
/** The numeric repository id: pins the repository across a rename or a deleted-and-recreated name. */
|
|
15
|
+
readonly repositoryId?: string | undefined;
|
|
16
|
+
/** The workflow files admitted (`.github/workflows/ci.yml`), matched against the path of `workflow_ref`; any when absent. */
|
|
17
|
+
readonly workflows?: readonly string[] | undefined;
|
|
18
|
+
/** The refs admitted (`refs/heads/main`, `refs/tags/v1.0.0`); any when absent. */
|
|
19
|
+
readonly refs?: readonly string[] | undefined;
|
|
20
|
+
/** A test double's addresses. */
|
|
21
|
+
readonly endpoints?: IssuerEndpoints | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** What a verified CI run token attests. */
|
|
24
|
+
export interface GitHubActionsRun {
|
|
25
|
+
/** The `sub` claim (`repo:owner/name:ref:refs/heads/main`). */
|
|
26
|
+
readonly subject: string;
|
|
27
|
+
readonly repository: string;
|
|
28
|
+
readonly repositoryId: string;
|
|
29
|
+
/** The owner part of `repository`. */
|
|
30
|
+
readonly repositoryOwner: string;
|
|
31
|
+
/** The workflow name. */
|
|
32
|
+
readonly workflow: string;
|
|
33
|
+
/** `owner/name/.github/workflows/<file>@<ref>`. */
|
|
34
|
+
readonly workflowRef: string;
|
|
35
|
+
/** The workflow file path of `workflowRef` (`.github/workflows/ci.yml`). */
|
|
36
|
+
readonly workflowPath: string;
|
|
37
|
+
readonly ref: string;
|
|
38
|
+
readonly sha: string;
|
|
39
|
+
readonly runId: string;
|
|
40
|
+
/** The attempt of the run (`1`), when the token names one. */
|
|
41
|
+
readonly runAttempt: string | null;
|
|
42
|
+
/** Who started the run, when the token names them. */
|
|
43
|
+
readonly actor: string | null;
|
|
44
|
+
/** What started it (`push`, `pull_request`), when the token names it. */
|
|
45
|
+
readonly eventName: string | null;
|
|
46
|
+
/** The deployment environment, when the job has one. */
|
|
47
|
+
readonly environment: string | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The GitHub Actions preset: the OIDC token a workflow run mints (`permissions: id-token: write`), RS256 against GitHub's pinned key set,
|
|
51
|
+
* for an audience the app names, from one repository, optionally pinned to its id, its workflow files and its refs. A CI attestation port
|
|
52
|
+
* (a run proving which repository, workflow, ref and commit it is) composes this inside `integrations/keycloak`.
|
|
53
|
+
*/
|
|
54
|
+
export declare const githubActionsIssuer: (options: GitHubActionsIssuerOptions) => IssuerPreset<GitHubActionsRun>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { OIDC_ERROR_CODES } from "../../contracts/oidc.js";
|
|
2
|
+
import { OidcError } from "../errors.js";
|
|
3
|
+
import { mapped, unmapped } from "./issuer.js";
|
|
4
|
+
/** The issuer of GitHub Actions OIDC tokens. */
|
|
5
|
+
export const GITHUB_ACTIONS_ISSUER = "https://token.actions.githubusercontent.com";
|
|
6
|
+
/** Its signing keys. */
|
|
7
|
+
export const GITHUB_ACTIONS_JWKS_URI = "https://token.actions.githubusercontent.com/.well-known/jwks";
|
|
8
|
+
/** The claims a CI run token must carry beyond `sub` and `exp`. */
|
|
9
|
+
export const GITHUB_ACTIONS_CLAIMS = ["repository", "repository_id", "workflow", "workflow_ref", "ref", "run_id", "sha"];
|
|
10
|
+
const text = (value) => (typeof value === "string" && value !== "" ? value : null);
|
|
11
|
+
/** `owner/name/.github/workflows/ci.yml@refs/heads/main` -> `.github/workflows/ci.yml` for the given repository, else null. */
|
|
12
|
+
const workflowPathOf = (workflowRef, repository) => {
|
|
13
|
+
const at = workflowRef.lastIndexOf("@");
|
|
14
|
+
const path = at < 0 ? workflowRef : workflowRef.slice(0, at);
|
|
15
|
+
const prefix = `${repository}/`;
|
|
16
|
+
return path.toLowerCase().startsWith(prefix.toLowerCase()) && path.length > prefix.length ? path.slice(prefix.length) : null;
|
|
17
|
+
};
|
|
18
|
+
const RUN_ID = /^\d{1,20}$/u;
|
|
19
|
+
/** The run a verified token's claims attest, or a refusal: a claim of the wrong shape is `claims`, one the policy does not admit is `policy`. */
|
|
20
|
+
const runOf = (claims, options) => {
|
|
21
|
+
const subject = text(claims.sub);
|
|
22
|
+
const repository = text(claims.repository);
|
|
23
|
+
const repositoryId = text(claims.repository_id);
|
|
24
|
+
const workflow = text(claims.workflow);
|
|
25
|
+
const workflowRef = text(claims.workflow_ref);
|
|
26
|
+
const ref = text(claims.ref);
|
|
27
|
+
const sha = text(claims.sha);
|
|
28
|
+
const runId = text(claims.run_id);
|
|
29
|
+
if (subject === null || repository === null || repositoryId === null || workflow === null || workflowRef === null || ref === null || sha === null || runId === null || !RUN_ID.test(runId))
|
|
30
|
+
return unmapped("claims");
|
|
31
|
+
const wanted = options.repository.toLowerCase();
|
|
32
|
+
if (repository.toLowerCase() !== wanted)
|
|
33
|
+
return unmapped("policy", "repository");
|
|
34
|
+
if (!subject.toLowerCase().startsWith(`repo:${wanted}:`))
|
|
35
|
+
return unmapped("policy", "sub");
|
|
36
|
+
if (options.repositoryId !== undefined && repositoryId !== options.repositoryId)
|
|
37
|
+
return unmapped("policy", "repository_id");
|
|
38
|
+
const workflowPath = workflowPathOf(workflowRef, repository);
|
|
39
|
+
if (workflowPath === null)
|
|
40
|
+
return unmapped("policy", "workflow_ref");
|
|
41
|
+
if (options.workflows !== undefined && !options.workflows.includes(workflowPath))
|
|
42
|
+
return unmapped("policy", "workflow_ref");
|
|
43
|
+
if (options.refs !== undefined && !options.refs.includes(ref))
|
|
44
|
+
return unmapped("policy", "ref");
|
|
45
|
+
return mapped({
|
|
46
|
+
subject,
|
|
47
|
+
repository,
|
|
48
|
+
repositoryId,
|
|
49
|
+
repositoryOwner: repository.slice(0, repository.indexOf("/")),
|
|
50
|
+
workflow,
|
|
51
|
+
workflowRef,
|
|
52
|
+
workflowPath,
|
|
53
|
+
ref,
|
|
54
|
+
sha,
|
|
55
|
+
runId,
|
|
56
|
+
runAttempt: text(claims.run_attempt),
|
|
57
|
+
actor: text(claims.actor),
|
|
58
|
+
eventName: text(claims.event_name),
|
|
59
|
+
environment: text(claims.environment),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const REPOSITORY = /^[A-Za-z0-9-]+\/[A-Za-z0-9._-]+$/u;
|
|
63
|
+
/**
|
|
64
|
+
* The GitHub Actions preset: the OIDC token a workflow run mints (`permissions: id-token: write`), RS256 against GitHub's pinned key set,
|
|
65
|
+
* for an audience the app names, from one repository, optionally pinned to its id, its workflow files and its refs. A CI attestation port
|
|
66
|
+
* (a run proving which repository, workflow, ref and commit it is) composes this inside `integrations/keycloak`.
|
|
67
|
+
*/
|
|
68
|
+
export const githubActionsIssuer = (options) => {
|
|
69
|
+
const audience = typeof options.audience === "string" ? [options.audience] : options.audience;
|
|
70
|
+
if (audience.length === 0 || audience.some((entry) => entry === ""))
|
|
71
|
+
throw new OidcError(OIDC_ERROR_CODES.ConfigurationInvalid, { params: { key: "audience" } });
|
|
72
|
+
if (!REPOSITORY.test(options.repository))
|
|
73
|
+
throw new OidcError(OIDC_ERROR_CODES.ConfigurationInvalid, { params: { key: "repository" } });
|
|
74
|
+
return {
|
|
75
|
+
name: "github-actions",
|
|
76
|
+
issuer: options.endpoints?.issuer ?? GITHUB_ACTIONS_ISSUER,
|
|
77
|
+
issuerAliases: [],
|
|
78
|
+
keys: { kind: "jwks", uri: options.endpoints?.jwksUri ?? GITHUB_ACTIONS_JWKS_URI },
|
|
79
|
+
algorithms: ["RS256"],
|
|
80
|
+
audience,
|
|
81
|
+
requiredClaims: [...GITHUB_ACTIONS_CLAIMS],
|
|
82
|
+
identityOf: (claims) => runOf(claims, options),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GoogleProfile } from "../../server/ports.js";
|
|
2
|
+
import type { IssuerEndpoints, IssuerPreset } from "./issuer.js";
|
|
3
|
+
/** Google's issuer, as its discovery document names it. */
|
|
4
|
+
export declare const GOOGLE_ISSUER = "https://accounts.google.com";
|
|
5
|
+
/** The other spelling Google puts in `iss`. */
|
|
6
|
+
export declare const GOOGLE_ISSUER_ALIAS = "accounts.google.com";
|
|
7
|
+
/** Google's signing keys. */
|
|
8
|
+
export declare const GOOGLE_JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
|
|
9
|
+
/** What a Google ID token must say. */
|
|
10
|
+
export interface GoogleIssuerOptions {
|
|
11
|
+
/** The app's OAuth client id(s): the `aud` of its ID tokens. */
|
|
12
|
+
readonly clientId: string | readonly string[];
|
|
13
|
+
/** When set, the Workspace domain the account must belong to (the `hd` claim). */
|
|
14
|
+
readonly hostedDomain?: string | undefined;
|
|
15
|
+
/** Refuse a token whose address Google has not verified (true). */
|
|
16
|
+
readonly requireVerifiedEmail?: boolean | undefined;
|
|
17
|
+
/** A test double's addresses. */
|
|
18
|
+
readonly endpoints?: IssuerEndpoints | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** The person a Google ID token stands for: the profile the passport flow gives, with the Workspace domain. */
|
|
21
|
+
export interface GoogleIdentity extends GoogleProfile {
|
|
22
|
+
/** The Workspace domain (`hd`), or null for a consumer account. */
|
|
23
|
+
readonly hostedDomain: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The Google preset: ID tokens of Google sign-in (One Tap, the Identity Services button, a mobile client), RS256 against Google's pinned key
|
|
27
|
+
* set, issued for the app's client id. It maps to the same profile the lite Google flow gives the authenticator, so one `signInWithGoogle`
|
|
28
|
+
* serves both.
|
|
29
|
+
*/
|
|
30
|
+
export declare const googleIssuer: (options: GoogleIssuerOptions) => IssuerPreset<GoogleIdentity>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mapped, unmapped } from "./issuer.js";
|
|
2
|
+
/** Google's issuer, as its discovery document names it. */
|
|
3
|
+
export const GOOGLE_ISSUER = "https://accounts.google.com";
|
|
4
|
+
/** The other spelling Google puts in `iss`. */
|
|
5
|
+
export const GOOGLE_ISSUER_ALIAS = "accounts.google.com";
|
|
6
|
+
/** Google's signing keys. */
|
|
7
|
+
export const GOOGLE_JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
|
|
8
|
+
const text = (value) => (typeof value === "string" && value !== "" ? value : null);
|
|
9
|
+
/**
|
|
10
|
+
* The Google preset: ID tokens of Google sign-in (One Tap, the Identity Services button, a mobile client), RS256 against Google's pinned key
|
|
11
|
+
* set, issued for the app's client id. It maps to the same profile the lite Google flow gives the authenticator, so one `signInWithGoogle`
|
|
12
|
+
* serves both.
|
|
13
|
+
*/
|
|
14
|
+
export const googleIssuer = (options) => {
|
|
15
|
+
const audience = typeof options.clientId === "string" ? [options.clientId] : options.clientId;
|
|
16
|
+
const verifiedOnly = options.requireVerifiedEmail ?? true;
|
|
17
|
+
return {
|
|
18
|
+
name: "google",
|
|
19
|
+
issuer: options.endpoints?.issuer ?? GOOGLE_ISSUER,
|
|
20
|
+
issuerAliases: options.endpoints === undefined ? [GOOGLE_ISSUER_ALIAS] : [],
|
|
21
|
+
keys: { kind: "jwks", uri: options.endpoints?.jwksUri ?? GOOGLE_JWKS_URI },
|
|
22
|
+
algorithms: ["RS256"],
|
|
23
|
+
audience,
|
|
24
|
+
requiredClaims: ["iat"],
|
|
25
|
+
identityOf: (claims) => {
|
|
26
|
+
const subject = text(claims.sub);
|
|
27
|
+
if (subject === null)
|
|
28
|
+
return unmapped("claims", "sub");
|
|
29
|
+
const verified = claims.email_verified === true || claims.email_verified === "true";
|
|
30
|
+
const email = text(claims.email);
|
|
31
|
+
if (verifiedOnly && (email === null || !verified))
|
|
32
|
+
return unmapped("policy", "email_verified");
|
|
33
|
+
const hostedDomain = text(claims.hd);
|
|
34
|
+
if (options.hostedDomain !== undefined && hostedDomain !== options.hostedDomain)
|
|
35
|
+
return unmapped("policy", "hd");
|
|
36
|
+
return mapped({ subject, email, emailVerified: verified, name: text(claims.name), picture: text(claims.picture), hostedDomain });
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createIssuerVerifier, mapped, unmapped } from "./issuer.js";
|
|
2
|
+
export type { IssuerEndpoints, IssuerKeys, IssuerMapping, IssuerPreset, IssuerVerifier, IssuerVerifierOptions } from "./issuer.js";
|
|
3
|
+
export { keycloakIssuer, keycloakIssuerUrl, keycloakOptions } from "./keycloak.js";
|
|
4
|
+
export type { KeycloakIssuerOptions } from "./keycloak.js";
|
|
5
|
+
export { GOOGLE_ISSUER, GOOGLE_ISSUER_ALIAS, GOOGLE_JWKS_URI, googleIssuer } from "./google.js";
|
|
6
|
+
export type { GoogleIdentity, GoogleIssuerOptions } from "./google.js";
|
|
7
|
+
export { GITHUB_ACTIONS_CLAIMS, GITHUB_ACTIONS_ISSUER, GITHUB_ACTIONS_JWKS_URI, githubActionsIssuer } from "./github-actions.js";
|
|
8
|
+
export type { GitHubActionsIssuerOptions, GitHubActionsRun } from "./github-actions.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createIssuerVerifier, mapped, unmapped } from "./issuer.js";
|
|
2
|
+
export { keycloakIssuer, keycloakIssuerUrl, keycloakOptions } from "./keycloak.js";
|
|
3
|
+
export { GOOGLE_ISSUER, GOOGLE_ISSUER_ALIAS, GOOGLE_JWKS_URI, googleIssuer } from "./google.js";
|
|
4
|
+
export { GITHUB_ACTIONS_CLAIMS, GITHUB_ACTIONS_ISSUER, GITHUB_ACTIONS_JWKS_URI, githubActionsIssuer } from "./github-actions.js";
|