@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +103 -14
  3. package/dist/client/oidc/index.d.ts +5 -0
  4. package/dist/client/oidc/index.js +3 -0
  5. package/dist/client/oidc/oidc-client.d.ts +85 -0
  6. package/dist/client/oidc/oidc-client.js +304 -0
  7. package/dist/client/oidc/pkce.d.ts +15 -0
  8. package/dist/client/oidc/pkce.js +29 -0
  9. package/dist/contracts/index.d.ts +12 -0
  10. package/dist/contracts/index.js +14 -0
  11. package/dist/contracts/oidc.d.ts +148 -0
  12. package/dist/contracts/oidc.js +68 -0
  13. package/dist/oidc/admin.d.ts +42 -0
  14. package/dist/oidc/admin.js +139 -0
  15. package/dist/oidc/broker.d.ts +57 -0
  16. package/dist/oidc/broker.js +77 -0
  17. package/dist/oidc/client.d.ts +58 -0
  18. package/dist/oidc/client.js +119 -0
  19. package/dist/oidc/discovery.d.ts +23 -0
  20. package/dist/oidc/discovery.js +64 -0
  21. package/dist/oidc/errors.d.ts +10 -0
  22. package/dist/oidc/errors.js +10 -0
  23. package/dist/oidc/grants.d.ts +41 -0
  24. package/dist/oidc/grants.js +82 -0
  25. package/dist/oidc/index.d.ts +35 -0
  26. package/dist/oidc/index.js +18 -0
  27. package/dist/oidc/issuers/github-actions.d.ts +54 -0
  28. package/dist/oidc/issuers/github-actions.js +84 -0
  29. package/dist/oidc/issuers/google.d.ts +30 -0
  30. package/dist/oidc/issuers/google.js +39 -0
  31. package/dist/oidc/issuers/index.d.ts +8 -0
  32. package/dist/oidc/issuers/index.js +4 -0
  33. package/dist/oidc/issuers/issuer.d.ts +79 -0
  34. package/dist/oidc/issuers/issuer.js +39 -0
  35. package/dist/oidc/issuers/keycloak.d.ts +26 -0
  36. package/dist/oidc/issuers/keycloak.js +41 -0
  37. package/dist/oidc/jwks.d.ts +26 -0
  38. package/dist/oidc/jwks.js +80 -0
  39. package/dist/oidc/oidc.d.ts +32 -0
  40. package/dist/oidc/oidc.js +21 -0
  41. package/dist/oidc/oidc.module.d.ts +34 -0
  42. package/dist/oidc/oidc.module.js +51 -0
  43. package/dist/oidc/oidc.tokens.d.ts +18 -0
  44. package/dist/oidc/oidc.tokens.js +18 -0
  45. package/dist/oidc/options.d.ts +119 -0
  46. package/dist/oidc/options.js +98 -0
  47. package/dist/oidc/primitives.d.ts +15 -0
  48. package/dist/oidc/primitives.js +11 -0
  49. package/dist/oidc/refresh.d.ts +10 -0
  50. package/dist/oidc/refresh.js +24 -0
  51. package/dist/oidc/roles.d.ts +12 -0
  52. package/dist/oidc/roles.js +51 -0
  53. package/dist/oidc/tokens.d.ts +26 -0
  54. package/dist/oidc/tokens.js +79 -0
  55. package/dist/oidc/transport.d.ts +8 -0
  56. package/dist/oidc/transport.js +19 -0
  57. package/dist/oidc/verifier.d.ts +24 -0
  58. package/dist/oidc/verifier.js +64 -0
  59. package/dist/server/auth.module.d.ts +1 -1
  60. package/dist/server/auth.module.js +20 -6
  61. package/dist/server/auth.tokens.d.ts +2 -0
  62. package/dist/server/auth.tokens.js +2 -0
  63. package/dist/server/doors.d.ts +14 -8
  64. package/dist/server/doors.js +22 -11
  65. package/dist/server/flows.d.ts +13 -5
  66. package/dist/server/flows.js +37 -18
  67. package/dist/server/index.d.ts +7 -7
  68. package/dist/server/index.js +3 -4
  69. package/dist/server/options.d.ts +11 -13
  70. package/dist/server/ports.d.ts +8 -1
  71. package/dist/server/providers/github.d.ts +59 -0
  72. package/dist/server/providers/github.js +111 -0
  73. package/dist/server/providers/google.d.ts +56 -0
  74. package/dist/server/providers/google.js +87 -0
  75. package/dist/server/providers/index.d.ts +14 -0
  76. package/dist/server/providers/index.js +9 -0
  77. package/dist/server/providers/oauth-state.d.ts +47 -0
  78. package/dist/server/providers/oauth-state.js +74 -0
  79. package/dist/server/providers/provider.d.ts +58 -0
  80. package/dist/server/providers/provider.js +32 -0
  81. package/dist/server/providers/registry.d.ts +30 -0
  82. package/dist/server/providers/registry.js +37 -0
  83. package/dist/server/strategies.d.ts +3 -18
  84. package/dist/server/strategies.js +2 -41
  85. package/dist/server/verifier.d.ts +1 -1
  86. package/dist/testing/index.d.ts +3 -0
  87. package/dist/testing/index.js +2 -0
  88. package/dist/testing/oidc.d.ts +175 -0
  89. package/dist/testing/oidc.js +493 -0
  90. package/package.json +22 -7
  91. package/dist/server/google-state.d.ts +0 -33
  92. package/dist/server/google-state.js +0 -73
@@ -1,21 +1,29 @@
1
1
  import type { HttpExchange } from "./http.js";
2
2
  import type { AuthOptions } from "./options.js";
3
3
  import type { AuthSubject } from "./ports.js";
4
- /** The result of the Google callback leg. */
5
- export interface GoogleCompletion {
4
+ /** The result of a provider's callback leg. */
5
+ export interface ProviderCompletion {
6
6
  /** The signed-in person. */
7
7
  readonly subject: AuthSubject;
8
8
  /** The same-site path the browser lands on. */
9
9
  readonly next: string;
10
10
  }
11
+ /** The result of the Google callback leg (the name 0.5.0 gave it). */
12
+ export type GoogleCompletion = ProviderCompletion;
11
13
  /** Runs the passport strategies for one request without sessions; the only caller of `passport.authenticate`. */
12
14
  export declare class AuthFlows {
13
15
  #private;
14
- constructor(options: Pick<AuthOptions, "google">);
16
+ constructor(options: Pick<AuthOptions, "google" | "providers">);
17
+ /** The ids of the registered providers, in registration order. */
18
+ get providers(): readonly string[];
15
19
  /** Runs the local strategy over the posted body; answers the person, or throws the app's refusal. */
16
20
  authenticateLocal(exchange: HttpExchange): Promise<AuthSubject>;
17
- /** Sends the browser to the Google consent screen (the response is the redirect); the optional `next` query value is kept in the state cookie. */
21
+ /** Sends the browser to the provider's consent screen (the response is the redirect); the optional `next` query value is kept in the state cookie. */
22
+ begin(id: string, exchange: HttpExchange): Promise<void>;
23
+ /** Finishes the provider's round trip: the state cookie, the PKCE verifier and the profile must all check out. */
24
+ complete(id: string, exchange: HttpExchange): Promise<ProviderCompletion>;
25
+ /** Sends the browser to the Google consent screen. */
18
26
  beginGoogle(exchange: HttpExchange): Promise<void>;
19
- /** Finishes the Google round trip: the state cookie, the PKCE verifier and the profile must all check out. */
27
+ /** Finishes the Google round trip. */
20
28
  completeGoogle(exchange: HttpExchange): Promise<GoogleCompletion>;
21
29
  }
@@ -1,22 +1,31 @@
1
1
  import passport from "passport";
2
+ import { isPlainRecord } from "@starci/common";
2
3
  import { AUTH_ERROR_CODES } from "../contracts/index.js";
3
4
  import { AuthError } from "./errors.js";
4
- import { isPlainRecord } from "@starci/common";
5
5
  import { sameSitePath } from "./primitives.js";
6
+ import { GOOGLE_CODES, GOOGLE_PROVIDER_ID } from "./providers/google.js";
7
+ import { PROVIDER_CODES } from "./providers/provider.js";
8
+ import { providersOf } from "./providers/registry.js";
6
9
  const isSubject = (value) => isPlainRecord(value) && typeof value.userId === "string" && typeof value.email === "string";
7
10
  const isHandler = (value) => typeof value === "function";
8
11
  /** The middleware passport built; passport types it as any, so it is checked once here. */
9
- const handlerOf = (built) => {
12
+ const handlerOf = (built, code) => {
10
13
  if (!isHandler(built))
11
- throw new AuthError(AUTH_ERROR_CODES.GoogleUnavailable);
14
+ throw new AuthError(code);
12
15
  return built;
13
16
  };
14
17
  const errorOf = (error) => (error instanceof Error ? error : new Error("passport"));
18
+ /** The code a door refuses an unregistered provider with: Google's own (as in 0.5.0), else the provider-neutral one. */
19
+ const unavailableOf = (id) => new AuthError(id === GOOGLE_PROVIDER_ID ? GOOGLE_CODES.unavailable : PROVIDER_CODES.unavailable, { context: { provider: id } });
15
20
  /** Runs the passport strategies for one request without sessions; the only caller of `passport.authenticate`. */
16
21
  export class AuthFlows {
17
- #google;
22
+ #providers;
18
23
  constructor(options) {
19
- this.#google = options.google;
24
+ this.#providers = new Map(providersOf(options).map((provider) => [provider.id, provider]));
25
+ }
26
+ /** The ids of the registered providers, in registration order. */
27
+ get providers() {
28
+ return [...this.#providers.keys()];
20
29
  }
21
30
  /** Runs the local strategy over the posted body; answers the person, or throws the app's refusal. */
22
31
  authenticateLocal(exchange) {
@@ -29,25 +38,27 @@ export class AuthFlows {
29
38
  else
30
39
  reject(new AuthError(AUTH_ERROR_CODES.MissingCredentials));
31
40
  };
32
- handlerOf(passport.authenticate("local", { session: false }, settled))(exchange.req, exchange.res, reject);
41
+ handlerOf(passport.authenticate("local", { session: false }, settled), AUTH_ERROR_CODES.MissingCredentials)(exchange.req, exchange.res, reject);
33
42
  });
34
43
  }
35
- /** Sends the browser to the Google consent screen (the response is the redirect); the optional `next` query value is kept in the state cookie. */
36
- beginGoogle(exchange) {
37
- if (this.#google === null)
38
- return Promise.reject(new AuthError(AUTH_ERROR_CODES.GoogleUnavailable));
44
+ /** Sends the browser to the provider's consent screen (the response is the redirect); the optional `next` query value is kept in the state cookie. */
45
+ begin(id, exchange) {
46
+ const provider = this.#providers.get(id);
47
+ if (provider === undefined)
48
+ return Promise.reject(unavailableOf(id));
39
49
  return new Promise((resolve, reject) => {
40
50
  exchange.res.once("finish", resolve);
41
- const built = passport.authenticate("google", { session: false, prompt: "select_account" }, (error) => {
51
+ const built = passport.authenticate(provider.id, { session: false, ...provider.authorizeParams }, (error) => {
42
52
  reject(errorOf(error));
43
53
  });
44
- handlerOf(built)(exchange.req, exchange.res, reject);
54
+ handlerOf(built, provider.codes.unavailable)(exchange.req, exchange.res, reject);
45
55
  });
46
56
  }
47
- /** Finishes the Google round trip: the state cookie, the PKCE verifier and the profile must all check out. */
48
- completeGoogle(exchange) {
49
- if (this.#google === null)
50
- return Promise.reject(new AuthError(AUTH_ERROR_CODES.GoogleUnavailable));
57
+ /** Finishes the provider's round trip: the state cookie, the PKCE verifier and the profile must all check out. */
58
+ complete(id, exchange) {
59
+ const provider = this.#providers.get(id);
60
+ if (provider === undefined)
61
+ return Promise.reject(unavailableOf(id));
51
62
  return new Promise((resolve, reject) => {
52
63
  const settled = (error, user, info) => {
53
64
  if (error !== null && error !== undefined) {
@@ -55,13 +66,21 @@ export class AuthFlows {
55
66
  return;
56
67
  }
57
68
  if (!isSubject(user)) {
58
- reject(new AuthError(AUTH_ERROR_CODES.GoogleStateInvalid));
69
+ reject(new AuthError(provider.codes.stateInvalid, { context: { provider: provider.id } }));
59
70
  return;
60
71
  }
61
72
  const state = isPlainRecord(info) && isPlainRecord(info.state) ? info.state.next : undefined;
62
73
  resolve({ subject: user, next: sameSitePath(state, "/") });
63
74
  };
64
- handlerOf(passport.authenticate("google", { session: false }, settled))(exchange.req, exchange.res, reject);
75
+ handlerOf(passport.authenticate(provider.id, { session: false }, settled), provider.codes.unavailable)(exchange.req, exchange.res, reject);
65
76
  });
66
77
  }
78
+ /** Sends the browser to the Google consent screen. */
79
+ beginGoogle(exchange) {
80
+ return this.begin(GOOGLE_PROVIDER_ID, exchange);
81
+ }
82
+ /** Finishes the Google round trip. */
83
+ completeGoogle(exchange) {
84
+ return this.complete(GOOGLE_PROVIDER_ID, exchange);
85
+ }
67
86
  }
@@ -1,24 +1,24 @@
1
- export { AUTH_AUTHENTICATOR, AUTH_DOORS_SERVICE, AUTH_FLOWS, AUTH_GOOGLE_STATE, AUTH_GOOGLE_STRATEGY, AUTH_LOCAL_STRATEGY, AUTH_PASSWORDS, AUTH_PRINCIPAL_VERIFIER, AUTH_REFRESH_COOKIE, AUTH_SESSIONS, AUTH_SIGN_IN_BUDGET, AUTH_TOKENS } from "./auth.tokens.js";
1
+ export { AUTH_AUTHENTICATOR, AUTH_DOORS_SERVICE, AUTH_FLOWS, AUTH_GOOGLE_STATE, AUTH_GOOGLE_STRATEGY, AUTH_LOCAL_STRATEGY, AUTH_PASSWORDS, AUTH_PRINCIPAL_VERIFIER, AUTH_PROVIDERS, AUTH_REFRESH_COOKIE, AUTH_SESSIONS, AUTH_SIGN_IN_BUDGET, AUTH_TOKENS } from "./auth.tokens.js";
2
2
  export { AuthModule, MODULE_OPTIONS_TOKEN as AUTH_OPTIONS } from "./auth.module.js";
3
3
  export type { AuthModuleExtras, AuthRegisterAsyncOptions, AuthRegisterOptions } from "./auth.module.js";
4
4
  export { SignInBudget } from "./budget.js";
5
5
  export { RefreshCookie } from "./cookie.js";
6
- export { AuthDoors, GOOGLE_SIGN_IN_FAILED_EVENT, SEE_OTHER, accessOf } from "./doors.js";
6
+ export { AuthDoors, GOOGLE_SIGN_IN_FAILED_EVENT, SEE_OTHER, accessOf, providerSignInFailedEvent } from "./doors.js";
7
7
  export type { DoorsDeps } from "./doors.js";
8
8
  export { AuthError, ok, refused } from "./errors.js";
9
9
  export type { Outcome } from "./errors.js";
10
10
  export { AuthFlows } from "./flows.js";
11
- export type { GoogleCompletion } from "./flows.js";
12
- export { DEFAULT_GOOGLE_STATE_COOKIE, GOOGLE_STATE_PATH, GOOGLE_STATE_TTL_MS, GoogleStateClient } from "./google-state.js";
11
+ export type { GoogleCompletion, ProviderCompletion } from "./flows.js";
13
12
  export type { CookieWrite, HttpExchange, HttpRequest, HttpResponse } from "./http.js";
14
- export type { AuthOptions, BudgetOptions, GoogleOptions, RefreshCookieOptions, SigningKeyOptions } from "./options.js";
13
+ export type { AuthOptions, BudgetOptions, RefreshCookieOptions, SigningKeyOptions } from "./options.js";
15
14
  export { AuthPasswords } from "./passwords.js";
16
- export type { AccessClaims, AccountDirectory, AccountStanding, AuthAuthenticator, AuthClock, AuthIds, AuthLogger, AuthPrincipal, AuthSubject, DeviceMeta, GoogleProfile, IssuedAccessToken, NewSession, RateCounter, RefreshTokenRecord, SessionStore, SessionTokens, SessionTransaction, } from "./ports.js";
15
+ export type { AccessClaims, AccountDirectory, AccountStanding, AuthAuthenticator, AuthClock, AuthIds, AuthLogger, AuthPrincipal, AuthSubject, DeviceMeta, GoogleProfile, IssuedAccessToken, NewSession, ProviderProfile, RateCounter, RefreshTokenRecord, SessionStore, SessionTokens, SessionTransaction, } from "./ports.js";
17
16
  export { cookieValue, digestOf, isAcceptablePassword, isPresentablePassword, normalizeEmail, sameSitePath } from "./primitives.js";
18
17
  export type { Revealable, SecretInput } from "./primitives.js";
18
+ export { DEFAULT_GOOGLE_STATE_COOKIE, GITHUB_ENDPOINTS, GITHUB_PROVIDER_ID, GITHUB_SCOPES, GOOGLE_CODES, GOOGLE_PROVIDER_ID, GOOGLE_STATE_PATH, GOOGLE_STATE_TTL_MS, GoogleStateClient, OAUTH_STATE_TTL_MS, OAuthStateClient, PROVIDER_CODES, PassportGitHubStrategy, PassportGoogleStrategy, buildProviders, defineProvider, github, githubProfileOf, google, googleProfileOf, oauthStateCookie, providersOf, signInWithProvider, } from "./providers/index.js";
19
+ export type { AnySignInProvider, GitHubOptions, GitHubRawProfile, GoogleOptions, OAuthStateDeps, OAuthStateOptions, ProviderCodes, ProviderContext, ProviderDefinition, ProviderEntry, ProviderRuntimeDeps, SignInProvider, } from "./providers/index.js";
19
20
  export { randomIds, systemClock } from "./runtime.js";
20
21
  export { AuthSessions, DEFAULT_REUSE_GRACE_MS, REFRESH_REUSE_EVENT } from "./sessions.js";
21
- export { googleProfileOf } from "./strategies.js";
22
22
  export { AuthTokens } from "./tokens.js";
23
23
  export { AuthPrincipalVerifier, refuseSessions } from "./verifier.js";
24
24
  export type { PrincipalVerifier } from "./verifier.js";
@@ -1,15 +1,14 @@
1
- export { AUTH_AUTHENTICATOR, AUTH_DOORS_SERVICE, AUTH_FLOWS, AUTH_GOOGLE_STATE, AUTH_GOOGLE_STRATEGY, AUTH_LOCAL_STRATEGY, AUTH_PASSWORDS, AUTH_PRINCIPAL_VERIFIER, AUTH_REFRESH_COOKIE, AUTH_SESSIONS, AUTH_SIGN_IN_BUDGET, AUTH_TOKENS } from "./auth.tokens.js";
1
+ export { AUTH_AUTHENTICATOR, AUTH_DOORS_SERVICE, AUTH_FLOWS, AUTH_GOOGLE_STATE, AUTH_GOOGLE_STRATEGY, AUTH_LOCAL_STRATEGY, AUTH_PASSWORDS, AUTH_PRINCIPAL_VERIFIER, AUTH_PROVIDERS, AUTH_REFRESH_COOKIE, AUTH_SESSIONS, AUTH_SIGN_IN_BUDGET, AUTH_TOKENS } from "./auth.tokens.js";
2
2
  export { AuthModule, MODULE_OPTIONS_TOKEN as AUTH_OPTIONS } from "./auth.module.js";
3
3
  export { SignInBudget } from "./budget.js";
4
4
  export { RefreshCookie } from "./cookie.js";
5
- export { AuthDoors, GOOGLE_SIGN_IN_FAILED_EVENT, SEE_OTHER, accessOf } from "./doors.js";
5
+ export { AuthDoors, GOOGLE_SIGN_IN_FAILED_EVENT, SEE_OTHER, accessOf, providerSignInFailedEvent } from "./doors.js";
6
6
  export { AuthError, ok, refused } from "./errors.js";
7
7
  export { AuthFlows } from "./flows.js";
8
- export { DEFAULT_GOOGLE_STATE_COOKIE, GOOGLE_STATE_PATH, GOOGLE_STATE_TTL_MS, GoogleStateClient } from "./google-state.js";
9
8
  export { AuthPasswords } from "./passwords.js";
10
9
  export { cookieValue, digestOf, isAcceptablePassword, isPresentablePassword, normalizeEmail, sameSitePath } from "./primitives.js";
10
+ export { DEFAULT_GOOGLE_STATE_COOKIE, GITHUB_ENDPOINTS, GITHUB_PROVIDER_ID, GITHUB_SCOPES, GOOGLE_CODES, GOOGLE_PROVIDER_ID, GOOGLE_STATE_PATH, GOOGLE_STATE_TTL_MS, GoogleStateClient, OAUTH_STATE_TTL_MS, OAuthStateClient, PROVIDER_CODES, PassportGitHubStrategy, PassportGoogleStrategy, buildProviders, defineProvider, github, githubProfileOf, google, googleProfileOf, oauthStateCookie, providersOf, signInWithProvider, } from "./providers/index.js";
11
11
  export { randomIds, systemClock } from "./runtime.js";
12
12
  export { AuthSessions, DEFAULT_REUSE_GRACE_MS, REFRESH_REUSE_EVENT } from "./sessions.js";
13
- export { googleProfileOf } from "./strategies.js";
14
13
  export { AuthTokens } from "./tokens.js";
15
14
  export { AuthPrincipalVerifier, refuseSessions } from "./verifier.js";
@@ -1,5 +1,7 @@
1
1
  import type { SecretInput } from "./primitives.js";
2
2
  import type { AccountDirectory, AuthClock, AuthIds, AuthLogger, RateCounter, SessionStore } from "./ports.js";
3
+ import type { GoogleOptions } from "./providers/google.js";
4
+ import type { AnySignInProvider } from "./providers/provider.js";
3
5
  /** One signing key of the key set. */
4
6
  export interface SigningKeyOptions {
5
7
  /** The public key (SPKI PEM) published at the JWKS door. Derived from the private key when absent; a verify-only key needs it. */
@@ -9,17 +11,8 @@ export interface SigningKeyOptions {
9
11
  /** The `kid` of the key; the RFC 7638 thumbprint of the public key when absent. */
10
12
  readonly keyId?: string | undefined;
11
13
  }
12
- /** The Google OAuth client and where Google sends the browser back. */
13
- export interface GoogleOptions {
14
- /** The public client id. */
15
- readonly clientId: string;
16
- /** The client secret. */
17
- readonly clientSecret: SecretInput;
18
- /** The registered callback address (`<api>/auth/google/callback`). */
19
- readonly callbackUrl: string;
20
- /** The name of the cookie that carries one Google round trip. */
21
- readonly stateCookie?: string | undefined;
22
- }
14
+ /** The Google OAuth client: declared beside the Google provider, named here as 0.5.0 did. */
15
+ export type { GoogleOptions } from "./providers/google.js";
23
16
  /** The refresh cookie. */
24
17
  export interface RefreshCookieOptions {
25
18
  /** The cookie name. */
@@ -65,8 +58,13 @@ export interface AuthOptions {
65
58
  readonly bcryptCost: number;
66
59
  /** The origin of the web app: a return address is accepted only on this origin, and the Google callback lands there. */
67
60
  readonly returnOrigin: string;
68
- /** The Google client; null when the deployment has none (the Google door is then unavailable). */
69
- readonly google: GoogleOptions | null;
61
+ /**
62
+ * The Google client of 0.5.0; null or absent when the deployment has none (the Google door is then unavailable). The same as listing
63
+ * `google({...})` in `providers`; naming Google both ways is a configuration error.
64
+ */
65
+ readonly google?: GoogleOptions | null | undefined;
66
+ /** The sign-in providers beyond email and password (`[google({...}), github({...})]`), each served at `/auth/<id>` and `/auth/<id>/callback`. */
67
+ readonly providers?: readonly AnySignInProvider[] | undefined;
70
68
  /** The refresh cookie. */
71
69
  readonly cookie: RefreshCookieOptions;
72
70
  /** The sign-in budget. */
@@ -21,12 +21,19 @@ export interface GoogleProfile {
21
21
  /** The picture address, when released. */
22
22
  readonly picture: string | null;
23
23
  }
24
- /** The sign-in decisions the app makes for the two strategies; each throws the app's error when it refuses. */
24
+ /** What any sign-in provider told us about the person who consented: Google's facts, with the provider that told them. */
25
+ export interface ProviderProfile extends GoogleProfile {
26
+ /** The provider id (`github`); with `subject` it names the external account. */
27
+ readonly provider: string;
28
+ }
29
+ /** The sign-in decisions the app makes for the strategies; each throws the app's error when it refuses. */
25
30
  export interface AuthAuthenticator {
26
31
  /** Verifies an email and password; answers the person or throws. */
27
32
  verifyPassword(email: string, password: string): Promise<AuthSubject>;
28
33
  /** Finds, links or creates the person a verified Google profile stands for; answers the person or throws. */
29
34
  signInWithGoogle(profile: GoogleProfile): Promise<AuthSubject>;
35
+ /** Finds, links or creates the person another provider's profile stands for (GitHub); needed only when the app registers such a provider. */
36
+ signInWithProvider?(profile: ProviderProfile): Promise<AuthSubject>;
30
37
  }
31
38
  /** The account facts a refresh needs to decide that a session may go on. */
32
39
  export interface AccountStanding {
@@ -0,0 +1,59 @@
1
+ import OAuth2Strategy from "passport-oauth2";
2
+ import type { AuthAuthenticator, AuthSubject, ProviderProfile } from "../ports.js";
3
+ import type { SecretInput } from "../primitives.js";
4
+ import type { OAuthStateClient } from "./oauth-state.js";
5
+ import type { SignInProvider } from "./provider.js";
6
+ /** The id of the GitHub provider: its passport strategy, its state purpose and its doors (`/auth/github`, `/auth/github/callback`). */
7
+ export declare const GITHUB_PROVIDER_ID = "github";
8
+ /** The GitHub endpoints the provider speaks to (github.com OAuth apps and the REST API). */
9
+ export declare const GITHUB_ENDPOINTS: {
10
+ readonly authorization: "https://github.com/login/oauth/authorize";
11
+ readonly token: "https://github.com/login/oauth/access_token";
12
+ readonly user: "https://api.github.com/user";
13
+ readonly emails: "https://api.github.com/user/emails";
14
+ };
15
+ /** The scopes the provider asks for: the profile and the addresses with their verification. */
16
+ export declare const GITHUB_SCOPES: readonly string[];
17
+ /** The GitHub OAuth app and where GitHub sends the browser back. */
18
+ export interface GitHubOptions {
19
+ /** The OAuth app's client id. */
20
+ readonly clientId: string;
21
+ /** The OAuth app's client secret. */
22
+ readonly clientSecret: SecretInput;
23
+ /** The registered callback address (`<api>/auth/github/callback`). */
24
+ readonly callbackUrl: string;
25
+ /** The name of the cookie that carries one GitHub round trip (`auth_github_state`). */
26
+ readonly stateCookie?: string | undefined;
27
+ /** The `User-Agent` the REST API requires (`starci-auth`). */
28
+ readonly userAgent?: string | undefined;
29
+ }
30
+ /** What the GitHub REST API answered for one person: `GET /user` and `GET /user/emails`. */
31
+ export interface GitHubRawProfile {
32
+ readonly user: unknown;
33
+ readonly emails: unknown;
34
+ }
35
+ /** The facts the app needs from GitHub: the numeric account id is the subject (a login can be renamed and reused), the primary verified address, the name and the avatar. */
36
+ export declare const githubProfileOf: (raw: GitHubRawProfile) => ProviderProfile;
37
+ type ProfileDone = (error?: unknown, profile?: unknown) => void;
38
+ /** passport-oauth2 against GitHub, with the profile read from the REST API (`/user`, `/user/emails`) under the access token. */
39
+ declare class GitHubOAuth2Strategy extends OAuth2Strategy {
40
+ constructor(options: GitHubOptions, verify: OAuth2Strategy.VerifyFunction);
41
+ /**
42
+ * Called by passport-oauth2 once the code is exchanged: the person and their addresses, raw, for `githubProfileOf`. passport runs it on an
43
+ * `Object.create` of the registered strategy, so it reaches the client through the prototype (`this._oauth2`), never a private member.
44
+ */
45
+ userProfile(accessToken: string, done: ProfileDone): void;
46
+ }
47
+ declare const PassportGitHubStrategy_base: new (options: GitHubOptions) => GitHubOAuth2Strategy & {
48
+ validate(...args: any[]): unknown;
49
+ };
50
+ /** The GitHub OAuth strategy: authorization code with `state` and PKCE (S256), the round trip held in a signed cookie. */
51
+ export declare class PassportGitHubStrategy extends PassportGitHubStrategy_base {
52
+ #private;
53
+ constructor(options: GitHubOptions, authenticator: AuthAuthenticator, state: OAuthStateClient);
54
+ /** Called by passport with what GitHub answered; answers the person the app stands it for. */
55
+ validate(_accessToken: string, _refreshToken: string, raw: GitHubRawProfile): Promise<AuthSubject>;
56
+ }
57
+ /** The GitHub provider: `AuthModule.register({ providers: [github({ clientId, clientSecret, callbackUrl })] })`; the app implements `signInWithProvider`. */
58
+ export declare const github: (options: GitHubOptions) => SignInProvider<GitHubOptions, GitHubRawProfile>;
59
+ export {};
@@ -0,0 +1,111 @@
1
+ import { PassportStrategy } from "@nestjs/passport";
2
+ import { isPlainRecord } from "@starci/common";
3
+ import OAuth2Strategy from "passport-oauth2";
4
+ import { AUTH_ERROR_CODES } from "../../contracts/index.js";
5
+ import { AuthError } from "../errors.js";
6
+ import { revealed } from "../primitives.js";
7
+ import { defineProvider, signInWithProvider } from "./provider.js";
8
+ /** The id of the GitHub provider: its passport strategy, its state purpose and its doors (`/auth/github`, `/auth/github/callback`). */
9
+ export const GITHUB_PROVIDER_ID = "github";
10
+ /** The GitHub endpoints the provider speaks to (github.com OAuth apps and the REST API). */
11
+ export const GITHUB_ENDPOINTS = {
12
+ authorization: "https://github.com/login/oauth/authorize",
13
+ token: "https://github.com/login/oauth/access_token",
14
+ user: "https://api.github.com/user",
15
+ emails: "https://api.github.com/user/emails",
16
+ };
17
+ /** The scopes the provider asks for: the profile and the addresses with their verification. */
18
+ export const GITHUB_SCOPES = ["read:user", "user:email"];
19
+ const text = (value) => (typeof value === "string" && value !== "" ? value : null);
20
+ /** The address to trust: the primary verified one, else any verified one, else the public profile address (unverified). */
21
+ const addressOf = (user, emails) => {
22
+ const listed = Array.isArray(emails) ? emails.filter(isPlainRecord) : [];
23
+ const verified = listed.filter((entry) => entry.verified === true && text(entry.email) !== null);
24
+ const chosen = verified.find((entry) => entry.primary === true) ?? verified[0];
25
+ if (chosen !== undefined)
26
+ return { email: text(chosen.email), verified: true };
27
+ return { email: text(user.email), verified: false };
28
+ };
29
+ /** The facts the app needs from GitHub: the numeric account id is the subject (a login can be renamed and reused), the primary verified address, the name and the avatar. */
30
+ export const githubProfileOf = (raw) => {
31
+ const user = raw.user;
32
+ if (!isPlainRecord(user) || typeof user.id !== "number" || !Number.isSafeInteger(user.id) || user.id <= 0)
33
+ throw new AuthError(AUTH_ERROR_CODES.ProviderRejected, { context: { provider: GITHUB_PROVIDER_ID } });
34
+ const address = addressOf(user, raw.emails);
35
+ return {
36
+ provider: GITHUB_PROVIDER_ID,
37
+ subject: String(user.id),
38
+ email: address.email,
39
+ emailVerified: address.verified,
40
+ name: text(user.name) ?? text(user.login),
41
+ picture: text(user.avatar_url),
42
+ };
43
+ };
44
+ const parsed = (body) => {
45
+ try {
46
+ return JSON.parse(typeof body === "string" ? body : (body?.toString("utf8") ?? ""));
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ };
52
+ /** passport-oauth2 against GitHub, with the profile read from the REST API (`/user`, `/user/emails`) under the access token. */
53
+ class GitHubOAuth2Strategy extends OAuth2Strategy {
54
+ constructor(options, verify) {
55
+ super({
56
+ authorizationURL: GITHUB_ENDPOINTS.authorization,
57
+ tokenURL: GITHUB_ENDPOINTS.token,
58
+ clientID: options.clientId,
59
+ clientSecret: revealed(options.clientSecret),
60
+ callbackURL: options.callbackUrl,
61
+ scope: [...GITHUB_SCOPES],
62
+ customHeaders: { "User-Agent": options.userAgent ?? "starci-auth", Accept: "application/json" },
63
+ state: true,
64
+ pkce: true,
65
+ }, verify);
66
+ this.name = GITHUB_PROVIDER_ID;
67
+ this._oauth2.useAuthorizationHeaderforGET(true);
68
+ }
69
+ /**
70
+ * Called by passport-oauth2 once the code is exchanged: the person and their addresses, raw, for `githubProfileOf`. passport runs it on an
71
+ * `Object.create` of the registered strategy, so it reaches the client through the prototype (`this._oauth2`), never a private member.
72
+ */
73
+ userProfile(accessToken, done) {
74
+ const read = (url) => new Promise((resolve, reject) => {
75
+ // node-oauth types the error as always present; it is null on success.
76
+ this._oauth2.get(url, accessToken, (error, body) => {
77
+ if (error === null || error === undefined)
78
+ resolve(parsed(body));
79
+ else
80
+ reject(new AuthError(AUTH_ERROR_CODES.ProviderRejected, { context: { provider: GITHUB_PROVIDER_ID, status: isPlainRecord(error) ? error.statusCode : undefined } }));
81
+ });
82
+ });
83
+ Promise.all([read(GITHUB_ENDPOINTS.user), read(GITHUB_ENDPOINTS.emails)]).then(([user, emails]) => {
84
+ done(null, { user, emails });
85
+ }, (error) => {
86
+ done(error);
87
+ });
88
+ }
89
+ }
90
+ /** The GitHub OAuth strategy: authorization code with `state` and PKCE (S256), the round trip held in a signed cookie. */
91
+ export class PassportGitHubStrategy extends PassportStrategy(GitHubOAuth2Strategy, GITHUB_PROVIDER_ID) {
92
+ #authenticator;
93
+ constructor(options, authenticator, state) {
94
+ super(options);
95
+ this.#authenticator = authenticator;
96
+ // passport-oauth2 types describe only its session stores; the stateless PKCE store replaces the one it built.
97
+ Object.assign(this, { _stateStore: state });
98
+ }
99
+ /** Called by passport with what GitHub answered; answers the person the app stands it for. */
100
+ validate(_accessToken, _refreshToken, raw) {
101
+ return signInWithProvider(this.#authenticator, githubProfileOf(raw));
102
+ }
103
+ }
104
+ /** The GitHub provider: `AuthModule.register({ providers: [github({ clientId, clientSecret, callbackUrl })] })`; the app implements `signInWithProvider`. */
105
+ export const github = (options) => defineProvider({
106
+ id: GITHUB_PROVIDER_ID,
107
+ options,
108
+ stateCookie: options.stateCookie,
109
+ profileOf: githubProfileOf,
110
+ strategy: ({ authenticator, state }) => new PassportGitHubStrategy(options, authenticator, state),
111
+ });
@@ -0,0 +1,56 @@
1
+ import { Strategy as GoogleStrategy } from "passport-google-oauth20";
2
+ import type { Profile } from "passport-google-oauth20";
3
+ import type { AuthOptions } from "../options.js";
4
+ import type { AuthAuthenticator, AuthClock, AuthIds, AuthSubject, GoogleProfile } from "../ports.js";
5
+ import type { SecretInput } from "../primitives.js";
6
+ import type { AuthTokens } from "../tokens.js";
7
+ import { OAuthStateClient } from "./oauth-state.js";
8
+ import type { ProviderCodes, SignInProvider } from "./provider.js";
9
+ /** The id of the Google provider: its passport strategy, its state purpose and its doors (`/auth/google`). */
10
+ export declare const GOOGLE_PROVIDER_ID = "google";
11
+ /** The cookie that carries one Google round trip, when the options name none. */
12
+ export declare const DEFAULT_GOOGLE_STATE_COOKIE = "auth_google_state";
13
+ /** The only path the state cookie is sent to. */
14
+ export declare const GOOGLE_STATE_PATH: "/auth/google";
15
+ /** How long a Google round trip may take, in milliseconds. */
16
+ export declare const GOOGLE_STATE_TTL_MS = 600000;
17
+ /** The codes the Google doors refuse with: the ones 0.5.0 answered, so a front end maps them as before. */
18
+ export declare const GOOGLE_CODES: ProviderCodes;
19
+ /** The Google OAuth client and where Google sends the browser back. */
20
+ export interface GoogleOptions {
21
+ /** The public client id. */
22
+ readonly clientId: string;
23
+ /** The client secret. */
24
+ readonly clientSecret: SecretInput;
25
+ /** The registered callback address (`<api>/auth/google/callback`). */
26
+ readonly callbackUrl: string;
27
+ /** The name of the cookie that carries one Google round trip. */
28
+ readonly stateCookie?: string | undefined;
29
+ }
30
+ /**
31
+ * The state store of the Google round trip: the provider-neutral store with Google's cookie (`auth_google_state` on `/auth/google`) and
32
+ * purpose (`p: "google"`), built from the options as in 0.5.0.
33
+ */
34
+ export declare class GoogleStateClient extends OAuthStateClient {
35
+ constructor(deps: {
36
+ readonly tokens: AuthTokens;
37
+ readonly clock: AuthClock;
38
+ readonly ids: AuthIds;
39
+ readonly options: Pick<AuthOptions, "returnOrigin" | "google">;
40
+ });
41
+ }
42
+ /** The facts the app needs from a Google profile; an account without a subject is refused. */
43
+ export declare const googleProfileOf: (profile: Profile) => GoogleProfile;
44
+ declare const PassportGoogleStrategy_base: new (...args: [options: import("passport-google-oauth20").StrategyOptionsWithRequest] | [options: import("passport-google-oauth20").StrategyOptions] | [options: import("passport-google-oauth20").StrategyOptions] | [options: import("passport-google-oauth20").StrategyOptionsWithRequest]) => GoogleStrategy & {
45
+ validate(...args: any[]): unknown;
46
+ };
47
+ /** The Google OAuth 2.0 strategy: authorization code with `state` and PKCE (S256), the round trip held in a signed cookie. */
48
+ export declare class PassportGoogleStrategy extends PassportGoogleStrategy_base {
49
+ #private;
50
+ constructor(google: GoogleOptions, authenticator: AuthAuthenticator, state: OAuthStateClient);
51
+ /** Called by passport with Google's profile; answers the person the app stands it for. */
52
+ validate(_accessToken: string, _refreshToken: string, profile: Profile): Promise<AuthSubject>;
53
+ }
54
+ /** The Google provider: `AuthModule.register({ providers: [google({ clientId, clientSecret, callbackUrl })] })`, or the `google` option of 0.5.0. */
55
+ export declare const google: (options: GoogleOptions) => SignInProvider<GoogleOptions, Profile>;
56
+ export {};
@@ -0,0 +1,87 @@
1
+ import { PassportStrategy } from "@nestjs/passport";
2
+ import { Strategy as GoogleStrategy } from "passport-google-oauth20";
3
+ import { AUTH_DOORS, AUTH_ERROR_CODES } from "../../contracts/index.js";
4
+ import { AuthError } from "../errors.js";
5
+ import { revealed } from "../primitives.js";
6
+ import { OAUTH_STATE_TTL_MS, OAuthStateClient } from "./oauth-state.js";
7
+ import { defineProvider } from "./provider.js";
8
+ /** The id of the Google provider: its passport strategy, its state purpose and its doors (`/auth/google`). */
9
+ export const GOOGLE_PROVIDER_ID = "google";
10
+ /** The cookie that carries one Google round trip, when the options name none. */
11
+ export const DEFAULT_GOOGLE_STATE_COOKIE = "auth_google_state";
12
+ /** The only path the state cookie is sent to. */
13
+ export const GOOGLE_STATE_PATH = AUTH_DOORS.google;
14
+ /** How long a Google round trip may take, in milliseconds. */
15
+ export const GOOGLE_STATE_TTL_MS = OAUTH_STATE_TTL_MS;
16
+ /** The codes the Google doors refuse with: the ones 0.5.0 answered, so a front end maps them as before. */
17
+ export const GOOGLE_CODES = {
18
+ unavailable: AUTH_ERROR_CODES.GoogleUnavailable,
19
+ stateInvalid: AUTH_ERROR_CODES.GoogleStateInvalid,
20
+ rejected: AUTH_ERROR_CODES.GoogleRejected,
21
+ };
22
+ /**
23
+ * The state store of the Google round trip: the provider-neutral store with Google's cookie (`auth_google_state` on `/auth/google`) and
24
+ * purpose (`p: "google"`), built from the options as in 0.5.0.
25
+ */
26
+ export class GoogleStateClient extends OAuthStateClient {
27
+ constructor(deps) {
28
+ super({
29
+ tokens: deps.tokens,
30
+ clock: deps.clock,
31
+ ids: deps.ids,
32
+ state: {
33
+ provider: GOOGLE_PROVIDER_ID,
34
+ cookie: deps.options.google?.stateCookie ?? DEFAULT_GOOGLE_STATE_COOKIE,
35
+ path: GOOGLE_STATE_PATH,
36
+ returnOrigin: deps.options.returnOrigin,
37
+ },
38
+ });
39
+ }
40
+ }
41
+ const verifiedOf = (profile) => {
42
+ const claim = profile._json.email_verified;
43
+ return claim === true || claim === "true";
44
+ };
45
+ /** The facts the app needs from a Google profile; an account without a subject is refused. */
46
+ export const googleProfileOf = (profile) => {
47
+ if (profile.id === "")
48
+ throw new AuthError(AUTH_ERROR_CODES.GoogleRejected);
49
+ return {
50
+ subject: profile.id,
51
+ email: profile.emails?.[0]?.value ?? null,
52
+ emailVerified: verifiedOf(profile),
53
+ name: profile.displayName === "" ? null : profile.displayName,
54
+ picture: profile.photos?.[0]?.value ?? null,
55
+ };
56
+ };
57
+ /** The Google OAuth 2.0 strategy: authorization code with `state` and PKCE (S256), the round trip held in a signed cookie. */
58
+ export class PassportGoogleStrategy extends PassportStrategy(GoogleStrategy, GOOGLE_PROVIDER_ID) {
59
+ #authenticator;
60
+ constructor(google, authenticator, state) {
61
+ super({
62
+ clientID: google.clientId,
63
+ clientSecret: revealed(google.clientSecret),
64
+ callbackURL: google.callbackUrl,
65
+ scope: ["openid", "email", "profile"],
66
+ state: true,
67
+ pkce: true,
68
+ });
69
+ this.#authenticator = authenticator;
70
+ // passport-oauth2 types describe only its session stores; the stateless PKCE store replaces the one it built.
71
+ Object.assign(this, { _stateStore: state });
72
+ }
73
+ /** Called by passport with Google's profile; answers the person the app stands it for. */
74
+ validate(_accessToken, _refreshToken, profile) {
75
+ return this.#authenticator.signInWithGoogle(googleProfileOf(profile));
76
+ }
77
+ }
78
+ /** The Google provider: `AuthModule.register({ providers: [google({ clientId, clientSecret, callbackUrl })] })`, or the `google` option of 0.5.0. */
79
+ export const google = (options) => defineProvider({
80
+ id: GOOGLE_PROVIDER_ID,
81
+ options,
82
+ stateCookie: options.stateCookie ?? DEFAULT_GOOGLE_STATE_COOKIE,
83
+ authorizeParams: { prompt: "select_account" },
84
+ codes: GOOGLE_CODES,
85
+ profileOf: (raw) => ({ provider: GOOGLE_PROVIDER_ID, ...googleProfileOf(raw) }),
86
+ strategy: ({ authenticator, state }) => new PassportGoogleStrategy(options, authenticator, state),
87
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The sign-in providers of the lite edition: `AuthModule.register({ ...options, providers: [google({...}), github({...})] })`. A next
3
+ * provider (Facebook) is one file beside these that calls `defineProvider`, and one more entry in that list.
4
+ */
5
+ export { DEFAULT_GOOGLE_STATE_COOKIE, GOOGLE_CODES, GOOGLE_PROVIDER_ID, GOOGLE_STATE_PATH, GOOGLE_STATE_TTL_MS, GoogleStateClient, PassportGoogleStrategy, google, googleProfileOf } from "./google.js";
6
+ export type { GoogleOptions } from "./google.js";
7
+ export { GITHUB_ENDPOINTS, GITHUB_PROVIDER_ID, GITHUB_SCOPES, PassportGitHubStrategy, github, githubProfileOf } from "./github.js";
8
+ export type { GitHubOptions, GitHubRawProfile } from "./github.js";
9
+ export { OAUTH_STATE_TTL_MS, OAuthStateClient, oauthStateCookie } from "./oauth-state.js";
10
+ export type { OAuthStateDeps, OAuthStateOptions } from "./oauth-state.js";
11
+ export { PROVIDER_CODES, defineProvider, signInWithProvider } from "./provider.js";
12
+ export type { AnySignInProvider, ProviderCodes, ProviderContext, ProviderDefinition, SignInProvider } from "./provider.js";
13
+ export { buildProviders, providersOf } from "./registry.js";
14
+ export type { ProviderEntry, ProviderRuntimeDeps } from "./registry.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The sign-in providers of the lite edition: `AuthModule.register({ ...options, providers: [google({...}), github({...})] })`. A next
3
+ * provider (Facebook) is one file beside these that calls `defineProvider`, and one more entry in that list.
4
+ */
5
+ export { DEFAULT_GOOGLE_STATE_COOKIE, GOOGLE_CODES, GOOGLE_PROVIDER_ID, GOOGLE_STATE_PATH, GOOGLE_STATE_TTL_MS, GoogleStateClient, PassportGoogleStrategy, google, googleProfileOf } from "./google.js";
6
+ export { GITHUB_ENDPOINTS, GITHUB_PROVIDER_ID, GITHUB_SCOPES, PassportGitHubStrategy, github, githubProfileOf } from "./github.js";
7
+ export { OAUTH_STATE_TTL_MS, OAuthStateClient, oauthStateCookie } from "./oauth-state.js";
8
+ export { PROVIDER_CODES, defineProvider, signInWithProvider } from "./provider.js";
9
+ export { buildProviders, providersOf } from "./registry.js";