@warlock.js/auth 5.14.0 → 5.16.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 +12 -0
- package/esm/contracts/index.d.mts +1 -1
- package/esm/contracts/providers.d.mts +59 -1
- package/esm/contracts/providers.d.mts.map +1 -1
- package/esm/index.d.mts +8 -2
- package/esm/index.mjs +7 -1
- package/esm/providers/apple-provider.d.mts +74 -0
- package/esm/providers/apple-provider.d.mts.map +1 -0
- package/esm/providers/apple-provider.mjs +157 -0
- package/esm/providers/apple-provider.mjs.map +1 -0
- package/esm/providers/discord-provider.d.mts +25 -0
- package/esm/providers/discord-provider.d.mts.map +1 -0
- package/esm/providers/discord-provider.mjs +84 -0
- package/esm/providers/discord-provider.mjs.map +1 -0
- package/esm/providers/facebook-provider.d.mts +36 -0
- package/esm/providers/facebook-provider.d.mts.map +1 -0
- package/esm/providers/facebook-provider.mjs +94 -0
- package/esm/providers/facebook-provider.mjs.map +1 -0
- package/esm/providers/github-provider.d.mts +31 -0
- package/esm/providers/github-provider.d.mts.map +1 -0
- package/esm/providers/github-provider.mjs +115 -0
- package/esm/providers/github-provider.mjs.map +1 -0
- package/esm/providers/index.d.mts +7 -1
- package/esm/providers/index.mjs +6 -0
- package/esm/providers/linkedin-provider.d.mts +35 -0
- package/esm/providers/linkedin-provider.d.mts.map +1 -0
- package/esm/providers/linkedin-provider.mjs +108 -0
- package/esm/providers/linkedin-provider.mjs.map +1 -0
- package/esm/providers/provider-login.d.mts +2 -1
- package/esm/providers/provider-login.d.mts.map +1 -1
- package/esm/providers/provider-login.mjs +47 -7
- package/esm/providers/provider-login.mjs.map +1 -1
- package/esm/providers/provider-state-cookie.mjs +35 -16
- package/esm/providers/provider-state-cookie.mjs.map +1 -1
- package/esm/providers/x-provider.d.mts +35 -0
- package/esm/providers/x-provider.d.mts.map +1 -0
- package/esm/providers/x-provider.mjs +101 -0
- package/esm/providers/x-provider.mjs.map +1 -0
- package/esm/services/auth-config.mjs +16 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/llms-full.txt +1075 -765
- package/llms.txt +5 -5
- package/package.json +8 -8
- package/skills/auth-basics/SKILL.md +16 -1
- package/skills/customize-token-storage/SKILL.md +160 -122
- package/skills/customize-user-type/SKILL.md +168 -142
- package/skills/handle-login-and-logout/SKILL.md +179 -165
- package/skills/login-with-providers/SKILL.md +131 -2
- package/skills/protect-routes/SKILL.md +308 -249
- package/skills/register-user/SKILL.md +15 -1
- package/skills/run-auth-commands/SKILL.md +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `@warlock.js/auth` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.16.0 - 2026-09-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Six new login providers: GitHub, Discord, LinkedIn, Apple, Facebook and X, configured under `auth.providers.<name>` and used through `startProviderLogin` / `completeProviderLogin`. Apple and LinkedIn verify their id_token with `jose`. The other four use plain OAuth 2.
|
|
12
|
+
- `AuthProvider.callbackMode` (`"query"` | `"form_post"`). Apple uses `form_post`, so its state cookie is written with `SameSite=None; Secure` (HTTPS required). Query-mode providers keep `SameSite=Lax`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Provider names now resolve only from `auth.providers`' own keys, so a name inherited from the object prototype, such as `toString`, is never treated as a provider.
|
|
17
|
+
- The Set-Cookie that clears the provider state cookie now repeats the attributes the cookie was written with.
|
|
18
|
+
|
|
7
19
|
## 5.13.0 - 2026-09-17
|
|
8
20
|
|
|
9
21
|
### Added
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthProvider, GoogleProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig } from "./providers.mjs";
|
|
1
|
+
import { AppleProviderConfig, AuthProvider, DiscordProviderConfig, FacebookProviderConfig, GitHubProviderConfig, GoogleProviderConfig, LinkedInProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig, XProviderConfig } from "./providers.mjs";
|
|
2
2
|
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, AuthNotification, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./types.mjs";
|
|
3
3
|
import { Authenticable } from "./auth-contract.mjs";
|
|
4
4
|
import { RequestUser } from "./request-locals.mjs";
|
|
@@ -37,6 +37,14 @@ type ProviderProfile = {
|
|
|
37
37
|
*/
|
|
38
38
|
interface AuthProvider {
|
|
39
39
|
readonly name: string;
|
|
40
|
+
/**
|
|
41
|
+
* How the provider's callback arrives. `"query"` (default) is a top-level
|
|
42
|
+
* GET redirect, safe for a `SameSite=Lax` state cookie. `"form_post"` is a
|
|
43
|
+
* cross-site POST (Apple, when `name`/`email` scopes are requested) — the
|
|
44
|
+
* state cookie needs `SameSite=None; Secure` to survive it.
|
|
45
|
+
* @default "query"
|
|
46
|
+
*/
|
|
47
|
+
readonly callbackMode?: "query" | "form_post";
|
|
40
48
|
/** The URL to send the browser to. */
|
|
41
49
|
authorizationUrl(state: ProviderAuthorizationState): string | Promise<string>;
|
|
42
50
|
/** Exchange the callback for a verified profile, or throw. */
|
|
@@ -49,11 +57,61 @@ type GoogleProviderConfig = {
|
|
|
49
57
|
redirectUri: string; /** @default ["openid", "email", "profile"] */
|
|
50
58
|
scopes?: string[];
|
|
51
59
|
};
|
|
60
|
+
/** `auth.providers.github`. */
|
|
61
|
+
type GitHubProviderConfig = {
|
|
62
|
+
clientId: string;
|
|
63
|
+
clientSecret: string; /** Must exactly match the "Authorization callback URL" registered on the GitHub OAuth App. */
|
|
64
|
+
redirectUri: string; /** @default ["read:user", "user:email"] */
|
|
65
|
+
scopes?: string[];
|
|
66
|
+
};
|
|
67
|
+
/** `auth.providers.discord`. */
|
|
68
|
+
type DiscordProviderConfig = {
|
|
69
|
+
clientId: string;
|
|
70
|
+
clientSecret: string; /** Must exactly match a redirect registered on the Discord application. */
|
|
71
|
+
redirectUri: string; /** @default ["identify", "email"] */
|
|
72
|
+
scopes?: string[];
|
|
73
|
+
};
|
|
74
|
+
/** `auth.providers.linkedin`. */
|
|
75
|
+
type LinkedInProviderConfig = {
|
|
76
|
+
clientId: string;
|
|
77
|
+
clientSecret: string; /** Must exactly match a redirect URL registered on the LinkedIn app. */
|
|
78
|
+
redirectUri: string; /** @default ["openid", "profile", "email"] */
|
|
79
|
+
scopes?: string[];
|
|
80
|
+
};
|
|
81
|
+
/** `auth.providers.apple`. */
|
|
82
|
+
type AppleProviderConfig = {
|
|
83
|
+
/** The Services ID (or app id) registered with Sign in with Apple. */clientId: string; /** The Apple Developer team id — signed into the client-secret JWT as `iss`. */
|
|
84
|
+
teamId: string; /** The id of the private key created for Sign in with Apple — signed into the client-secret JWT header as `kid`. */
|
|
85
|
+
keyId: string; /** The PKCS8 PEM contents of that private key (`.p8` file), used to sign the client-secret JWT. */
|
|
86
|
+
privateKey: string; /** Must exactly match a return URL registered on the Services ID. */
|
|
87
|
+
redirectUri: string; /** @default ["name", "email"] */
|
|
88
|
+
scopes?: string[];
|
|
89
|
+
};
|
|
90
|
+
/** `auth.providers.facebook`. */
|
|
91
|
+
type FacebookProviderConfig = {
|
|
92
|
+
clientId: string;
|
|
93
|
+
clientSecret: string; /** Must exactly match a valid OAuth redirect URI registered on the Facebook app. */
|
|
94
|
+
redirectUri: string; /** @default ["email", "public_profile"] */
|
|
95
|
+
scopes?: string[];
|
|
96
|
+
};
|
|
97
|
+
/** `auth.providers.x`. */
|
|
98
|
+
type XProviderConfig = {
|
|
99
|
+
clientId: string;
|
|
100
|
+
clientSecret: string; /** Must exactly match a callback URI registered on the X app (confidential client). */
|
|
101
|
+
redirectUri: string; /** @default ["tweet.read", "users.read"] */
|
|
102
|
+
scopes?: string[];
|
|
103
|
+
};
|
|
52
104
|
/** Creates the app user for a verified provider profile with no matching account. */
|
|
53
105
|
type ProviderUserCreator = (profile: ProviderProfile, Model: ChildModel<Auth>) => Auth | Promise<Auth>;
|
|
54
106
|
/** `auth.providers`. */
|
|
55
107
|
type ProvidersConfig = {
|
|
56
108
|
google?: GoogleProviderConfig;
|
|
109
|
+
github?: GitHubProviderConfig;
|
|
110
|
+
discord?: DiscordProviderConfig;
|
|
111
|
+
linkedin?: LinkedInProviderConfig;
|
|
112
|
+
apple?: AppleProviderConfig;
|
|
113
|
+
facebook?: FacebookProviderConfig;
|
|
114
|
+
x?: XProviderConfig;
|
|
57
115
|
/**
|
|
58
116
|
* User attribute a verified provider email is matched against.
|
|
59
117
|
* @default "email"
|
|
@@ -97,5 +155,5 @@ type OtpConfig = {
|
|
|
97
155
|
send?: OtpSender;
|
|
98
156
|
};
|
|
99
157
|
//#endregion
|
|
100
|
-
export { AuthProvider, GoogleProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig };
|
|
158
|
+
export { AppleProviderConfig, AuthProvider, DiscordProviderConfig, FacebookProviderConfig, GitHubProviderConfig, GoogleProviderConfig, LinkedInProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig, XProviderConfig };
|
|
101
159
|
//# sourceMappingURL=providers.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/providers.ts"],"mappings":";;;;;;AAOA;;KAAY,0BAAA;EACV,KAAA;EACA,KAAA;EACA,YAAA;AAAA;;AAAY;AAQd;;;KAAY,sBAAA;EACV,KAAA,EAAO,MAAA;EACP,QAAA,EAAU,0BAA0B;AAAA;;KAI1B,eAAA;EAJ0B,sCAMpC,QAAA,UAFyB;EAIzB,cAAA;EACA,KAAA,WAHA;EAKA,aAAA;EACA,IAAA;EACA,MAAA,WADA;EAGA,GAAA,EAAK,MAAM;AAAA;;;AAAA;AAQb;;UAAiB,YAAA;EAAA,SACN,IAAA;
|
|
1
|
+
{"version":3,"file":"providers.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/providers.ts"],"mappings":";;;;;;AAOA;;KAAY,0BAAA;EACV,KAAA;EACA,KAAA;EACA,YAAA;AAAA;;AAAY;AAQd;;;KAAY,sBAAA;EACV,KAAA,EAAO,MAAA;EACP,QAAA,EAAU,0BAA0B;AAAA;;KAI1B,eAAA;EAJ0B,sCAMpC,QAAA,UAFyB;EAIzB,cAAA;EACA,KAAA,WAHA;EAKA,aAAA;EACA,IAAA;EACA,MAAA,WADA;EAGA,GAAA,EAAK,MAAM;AAAA;;;AAAA;AAQb;;UAAiB,YAAA;EAAA,SACN,IAAA;EAUqD;;;;;;;EAAA,SAFrD,YAAA;EAET;EAAA,gBAAA,CAAiB,KAAA,EAAO,0BAAA,YAAsC,OAAA;EAA7C;EAEjB,cAAA,CAAe,MAAA,EAAQ,sBAAA,GAAyB,OAAA,CAAQ,eAAA;AAAA;;KAI9C,oBAAA;EACV,QAAA;EACA,YAAA,UANuE;EAQvE,WAAA,UAJU;EAMV,MAAA;AAAA;;KAIU,oBAAA;EACV,QAAA;EACA,YAAA,UANA;EAQA,WAAA,UARM;EAUN,MAAA;AAAA;;KAIU,qBAAA;EACV,QAAA;EACA,YAAA,UARA;EAUA,WAAA,UARM;EAUN,MAAA;AAAA;;KAIU,sBAAA;EACV,QAAA;EACA,YAAA,UAVA;EAYA,WAAA,UARA;EAUA,MAAA;AAAA;AANF;AAAA,KAUY,mBAAA;wEAEV,QAAA,UAXA;EAaA,MAAA,UAVA;EAYA,KAAA,UAVM;EAYN,UAAA,UARU;EAUV,WAAA;EAEA,MAAA;AAAA;;KAIU,sBAAA;EACV,QAAA;EACA,YAAA,UANA;EAQA,WAAA,UARM;EAUN,MAAA;AAAA;;KAIU,eAAA;EACV,QAAA;EACA,YAAA,UARA;EAUA,WAAA,UARM;EAUN,MAAA;AAAA;;KAIU,mBAAA,IACV,OAAA,EAAS,eAAA,EACT,KAAA,EAAO,UAAA,CAAW,IAAA,MACf,IAAA,GAAO,OAAA,CAAQ,IAAA;;KAGR,eAAA;EACV,MAAA,GAAS,oBAAA;EACT,MAAA,GAAS,oBAAA;EACT,OAAA,GAAU,qBAAA;EACV,QAAA,GAAW,sBAAA;EACX,KAAA,GAAQ,mBAAA;EACR,QAAA,GAAW,sBAAA;EACX,CAAA,GAAI,eAAA;;;;;EAKJ,UAAA,WAfkB;EAiBlB,UAAA,GAAa,mBAAA,EAjBI;EAmBjB,MAAA,GAAS,MAAA,SAAe,YAAA;AAAA;;KAId,cAAA;EAxBQ,8EA0BlB,IAAA,UAzBG;EA2BH,MAAA,UA3BkB;EA6BlB,MAAA;EA7BsB;AAGxB;;;EA+BE,kBAAA,WA7BS;EA+BT,QAAA,IAAY,IAAA,EAAM,IAAI;AAAA;;KAIZ,UAAA;EA9BN,8CAgCJ,IAAA;EACA,IAAA;EACA,SAAA,EAAW,IAAI;AAAA;;KAIL,SAAA,IAAa,KAAA,UAAe,OAAA,EAAS,UAAU,EAAE,OAAA;;KAGjD,SAAA;EA9CD;;;;;EAoDT,OAAA,WAjDQ;EAmDR,UAAA,WAlDW;EAoDX,SAAA,WAnDI;EAqDJ,WAAA,WA9CA;EAgDA,OAAA,IAAW,IAAA,qBA9CX;EAgDA,IAAA,GAAO,SAAS;AAAA"}
|
package/esm/index.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ import { registerJWTSecretGeneratorCommand } from "./commands/jwt-secret-generat
|
|
|
4
4
|
import { Auth } from "./models/auth.model.mjs";
|
|
5
5
|
import { AccessToken, accessTokenSchema } from "./models/access-token/access-token.model.mjs";
|
|
6
6
|
import { OneTimeToken, OneTimeTokenPurpose, oneTimeTokenSchema } from "./models/one-time-token/one-time-token.model.mjs";
|
|
7
|
-
import { AuthProvider, GoogleProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig } from "./contracts/providers.mjs";
|
|
7
|
+
import { AppleProviderConfig, AuthProvider, DiscordProviderConfig, FacebookProviderConfig, GitHubProviderConfig, GoogleProviderConfig, LinkedInProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig, XProviderConfig } from "./contracts/providers.mjs";
|
|
8
8
|
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, AuthNotification, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./contracts/types.mjs";
|
|
9
9
|
import { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema } from "./models/refresh-token/refresh-token.model.mjs";
|
|
10
10
|
import { Authenticable } from "./contracts/auth-contract.mjs";
|
|
@@ -31,7 +31,13 @@ import { PasskeyOptionsJSON, PasskeyResponseJSON } from "./passkeys/simplewebaut
|
|
|
31
31
|
import { generatePasskeyAuthenticationOptions, verifyPasskeyAuthentication } from "./passkeys/passkey-authentication.mjs";
|
|
32
32
|
import { generatePasskeyRegistrationOptions, verifyPasskeyRegistration } from "./passkeys/passkey-registration.mjs";
|
|
33
33
|
import { GOOGLE_AUTH_FEATURE, GoogleProvider, JoseModule, pkceChallenge } from "./providers/google-provider.mjs";
|
|
34
|
+
import { APPLE_AUTH_FEATURE, AppleJoseModule, AppleProvider } from "./providers/apple-provider.mjs";
|
|
35
|
+
import { DiscordProvider } from "./providers/discord-provider.mjs";
|
|
36
|
+
import { FacebookProvider } from "./providers/facebook-provider.mjs";
|
|
37
|
+
import { GitHubProvider } from "./providers/github-provider.mjs";
|
|
38
|
+
import { LINKEDIN_AUTH_FEATURE, LinkedInProvider } from "./providers/linkedin-provider.mjs";
|
|
34
39
|
import { completeProviderLogin, resolveAuthProvider, startProviderLogin } from "./providers/provider-login.mjs";
|
|
40
|
+
import { XProvider } from "./providers/x-provider.mjs";
|
|
35
41
|
import { AuthEventCallback, AuthEventName, AuthEventPayloads, authEvents } from "./services/auth-events.mjs";
|
|
36
42
|
import { authService } from "./services/auth.service.mjs";
|
|
37
43
|
import { currentUser } from "./services/current-user.mjs";
|
|
@@ -42,5 +48,5 @@ import { IssuedOneTimeToken, consumeOneTimeToken, hashOneTimeToken, issueOneTime
|
|
|
42
48
|
import { requestPasswordReset, resetPassword } from "./services/password-reset.mjs";
|
|
43
49
|
import { safeEqual } from "./utils/safe-equal.mjs";
|
|
44
50
|
import { isNeverExpiring, isUsableExpiry, tokenHasExpClaim } from "./utils/token-expiry.mjs";
|
|
45
|
-
export { AccessToken, AccessTokenConfig, AccessTokenOutput, Auth, AuthConfigurations, AuthCredentials, AuthErrorCodes, AuthEventCallback, AuthEventName, AuthEventPayloads, AuthNotification, AuthProvider, AuthProviderSdkMissingError, Authenticable, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, CsrfOriginMismatchError, CsrfOriginMismatchReason, DeviceInfo, EmailNotVerifiedError, EmailVerificationConfig, GOOGLE_AUTH_FEATURE, GoogleProvider, GoogleProviderConfig, InvalidOneTimeTokenError, InvalidPasskeyError, InvalidProviderCallbackError, IssuedOneTimeToken, JoseModule, LegacyJwtConfig, LoginResult, LoginThrottleIdentifier, LoginThrottleOptions, LogoutWithoutTokenBehavior, NO_EXPIRATION, NotificationsUnavailableError, OneTimeToken, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenPurpose, OneTimeTokenUrlBuilder, OtpConfig, OtpMessage, OtpSender, PageAuthConfig, PasskeyCredential, type PasskeyOptionsJSON, type PasskeyResponseJSON, PasskeysConfig, PasswordResetConfig, PasswordSetter, ProviderAccount, ProviderAuthorizationState, ProviderCallbackParams, ProviderEmailNotVerifiedError, ProviderProfile, ProviderUserCreator, ProvidersConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, RequestUser, SetAuthCookieOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, accessTokenSchema, assertCsrfOriginAllowed, authEvents, authMiddleware, authMigrations, authService, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, safeEqual, sendEmailVerification, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
|
|
51
|
+
export { APPLE_AUTH_FEATURE, AccessToken, AccessTokenConfig, AccessTokenOutput, AppleJoseModule, AppleProvider, AppleProviderConfig, Auth, AuthConfigurations, AuthCredentials, AuthErrorCodes, AuthEventCallback, AuthEventName, AuthEventPayloads, AuthNotification, AuthProvider, AuthProviderSdkMissingError, Authenticable, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, CsrfOriginMismatchError, CsrfOriginMismatchReason, DeviceInfo, DiscordProvider, DiscordProviderConfig, EmailNotVerifiedError, EmailVerificationConfig, FacebookProvider, FacebookProviderConfig, GOOGLE_AUTH_FEATURE, GitHubProvider, GitHubProviderConfig, GoogleProvider, GoogleProviderConfig, InvalidOneTimeTokenError, InvalidPasskeyError, InvalidProviderCallbackError, IssuedOneTimeToken, JoseModule, LINKEDIN_AUTH_FEATURE, LegacyJwtConfig, LinkedInProvider, LinkedInProviderConfig, LoginResult, LoginThrottleIdentifier, LoginThrottleOptions, LogoutWithoutTokenBehavior, NO_EXPIRATION, NotificationsUnavailableError, OneTimeToken, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenPurpose, OneTimeTokenUrlBuilder, OtpConfig, OtpMessage, OtpSender, PageAuthConfig, PasskeyCredential, type PasskeyOptionsJSON, type PasskeyResponseJSON, PasskeysConfig, PasswordResetConfig, PasswordSetter, ProviderAccount, ProviderAuthorizationState, ProviderCallbackParams, ProviderEmailNotVerifiedError, ProviderProfile, ProviderUserCreator, ProvidersConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, RequestUser, SetAuthCookieOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, XProvider, XProviderConfig, accessTokenSchema, assertCsrfOriginAllowed, authEvents, authMiddleware, authMigrations, authService, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, safeEqual, sendEmailVerification, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
|
|
46
52
|
import "./contracts/request-locals.mjs";
|
package/esm/index.mjs
CHANGED
|
@@ -42,8 +42,14 @@ import { generatePasskeyAuthenticationOptions, verifyPasskeyAuthentication } fro
|
|
|
42
42
|
import { generatePasskeyRegistrationOptions, verifyPasskeyRegistration } from "./passkeys/passkey-registration.mjs";
|
|
43
43
|
import "./passkeys/index.mjs";
|
|
44
44
|
import { GOOGLE_AUTH_FEATURE, GoogleProvider, pkceChallenge } from "./providers/google-provider.mjs";
|
|
45
|
+
import { APPLE_AUTH_FEATURE, AppleProvider } from "./providers/apple-provider.mjs";
|
|
46
|
+
import { DiscordProvider } from "./providers/discord-provider.mjs";
|
|
47
|
+
import { FacebookProvider } from "./providers/facebook-provider.mjs";
|
|
48
|
+
import { GitHubProvider } from "./providers/github-provider.mjs";
|
|
49
|
+
import { LINKEDIN_AUTH_FEATURE, LinkedInProvider } from "./providers/linkedin-provider.mjs";
|
|
50
|
+
import { XProvider } from "./providers/x-provider.mjs";
|
|
45
51
|
import { completeProviderLogin, resolveAuthProvider, startProviderLogin } from "./providers/provider-login.mjs";
|
|
46
52
|
import "./providers/index.mjs";
|
|
47
53
|
import "./utils/index.mjs";
|
|
48
54
|
|
|
49
|
-
export { AccessToken, Auth, AuthErrorCodes, AuthProviderSdkMissingError, CsrfOriginMismatchError, EmailNotVerifiedError, GOOGLE_AUTH_FEATURE, GoogleProvider, InvalidOneTimeTokenError, InvalidPasskeyError, InvalidProviderCallbackError, NO_EXPIRATION, NotificationsUnavailableError, OneTimeToken, PasskeyCredential, ProviderAccount, ProviderEmailNotVerifiedError, RefreshToken, TokenTypeError, accessTokenSchema, assertCsrfOriginAllowed, authEvents, authMiddleware, authMigrations, authService, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, safeEqual, sendEmailVerification, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
|
|
55
|
+
export { APPLE_AUTH_FEATURE, AccessToken, AppleProvider, Auth, AuthErrorCodes, AuthProviderSdkMissingError, CsrfOriginMismatchError, DiscordProvider, EmailNotVerifiedError, FacebookProvider, GOOGLE_AUTH_FEATURE, GitHubProvider, GoogleProvider, InvalidOneTimeTokenError, InvalidPasskeyError, InvalidProviderCallbackError, LINKEDIN_AUTH_FEATURE, LinkedInProvider, NO_EXPIRATION, NotificationsUnavailableError, OneTimeToken, PasskeyCredential, ProviderAccount, ProviderEmailNotVerifiedError, RefreshToken, TokenTypeError, XProvider, accessTokenSchema, assertCsrfOriginAllowed, authEvents, authMiddleware, authMigrations, authService, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, safeEqual, sendEmailVerification, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AppleProviderConfig, AuthProvider, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
|
|
2
|
+
import { JoseModule } from "./google-provider.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../auth/src/providers/apple-provider.d.ts
|
|
5
|
+
/** The `warlock add` feature that installs `jose`. */
|
|
6
|
+
declare const APPLE_AUTH_FEATURE = "auth-apple";
|
|
7
|
+
type AppleSignJwtBuilder = {
|
|
8
|
+
setIssuer: (issuer: string) => AppleSignJwtBuilder;
|
|
9
|
+
setIssuedAt: () => AppleSignJwtBuilder;
|
|
10
|
+
setExpirationTime: (time: number) => AppleSignJwtBuilder;
|
|
11
|
+
setAudience: (audience: string) => AppleSignJwtBuilder;
|
|
12
|
+
setSubject: (subject: string) => AppleSignJwtBuilder;
|
|
13
|
+
sign: (key: unknown) => Promise<string>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The slice of `jose` the Apple provider uses on top of {@link JoseModule} —
|
|
17
|
+
* `importPKCS8` and `SignJWT` sign the ES256 client-secret JWT Apple requires
|
|
18
|
+
* instead of a static client secret.
|
|
19
|
+
*/
|
|
20
|
+
type AppleJoseModule = JoseModule & {
|
|
21
|
+
importPKCS8: (pkcs8: string, alg: string) => Promise<unknown>;
|
|
22
|
+
SignJWT: new (payload: Record<string, unknown>) => {
|
|
23
|
+
setProtectedHeader: (header: Record<string, unknown>) => AppleSignJwtBuilder;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Apple ("Sign in with Apple") is OpenID Connect over the authorization code
|
|
28
|
+
* flow with PKCE (S256), `state` and `nonce`, the same shape as
|
|
29
|
+
* {@link GoogleProvider} — with two differences that come straight from
|
|
30
|
+
* Apple's own docs (this package has no Apple developer account, so nothing
|
|
31
|
+
* here is proven against a live exchange):
|
|
32
|
+
*
|
|
33
|
+
* 1. Apple has no static client secret. The "secret" the token endpoint wants
|
|
34
|
+
* is a fresh ES256 JWT (`iss` = team id, `sub` = client id, `aud` =
|
|
35
|
+
* `https://appleid.apple.com`, signed with the app's private key and the
|
|
36
|
+
* key id as `kid`) — signed once per callback here rather than cached, to
|
|
37
|
+
* avoid ever presenting a stale one.
|
|
38
|
+
* 2. Requesting `name`/`email` (the default scopes) forces
|
|
39
|
+
* `response_mode=form_post`: Apple POSTs `code`, `state` and, on the
|
|
40
|
+
* user's FIRST authorization only, a `user` field — a JSON string with
|
|
41
|
+
* `{ name: { firstName, lastName }, email } ` that never comes back again
|
|
42
|
+
* on a later login. The callback route must therefore accept a POST body,
|
|
43
|
+
* not just a query string, and `request.input` must read that body the
|
|
44
|
+
* same way it already reads a GET query for the other providers.
|
|
45
|
+
*
|
|
46
|
+
* The id_token is verified with `jose` against Apple's JWKS (signature,
|
|
47
|
+
* `iss`, `aud` = the client id, `exp`), then its `nonce` is compared. Apple is
|
|
48
|
+
* also documented to send `email_verified` (and `is_private_email`) as the
|
|
49
|
+
* STRING `"true"`/`"false"` rather than a JSON boolean on some responses, so
|
|
50
|
+
* both forms are accepted here — treating only the unambiguous `"false"` (or
|
|
51
|
+
* `false`) as unverified. A private-relay address (`@privaterelay.appleid.com`)
|
|
52
|
+
* is still a real, working forwarding address and is accepted as any other.
|
|
53
|
+
*/
|
|
54
|
+
declare class AppleProvider implements AuthProvider {
|
|
55
|
+
private readonly settings;
|
|
56
|
+
readonly name = "apple";
|
|
57
|
+
/** Apple's default scopes (`name`, `email`) force `response_mode=form_post`. */
|
|
58
|
+
readonly callbackMode = "form_post";
|
|
59
|
+
constructor(settings: AppleProviderConfig);
|
|
60
|
+
authorizationUrl(state: ProviderAuthorizationState): string;
|
|
61
|
+
handleCallback({
|
|
62
|
+
query,
|
|
63
|
+
expected
|
|
64
|
+
}: ProviderCallbackParams): Promise<ProviderProfile>;
|
|
65
|
+
/** Sign a fresh ES256 client-secret JWT — Apple has no static one. */
|
|
66
|
+
private clientSecret;
|
|
67
|
+
/** Redeem the authorization code (with the PKCE verifier) for an id_token. */
|
|
68
|
+
private exchangeCode;
|
|
69
|
+
/** Verify signature, issuer, audience and expiry; resolve the claims. */
|
|
70
|
+
private verifyIdToken;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { APPLE_AUTH_FEATURE, AppleJoseModule, AppleProvider };
|
|
74
|
+
//# sourceMappingURL=apple-provider.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/apple-provider.ts"],"mappings":";;;;;cAqBa,kBAAA;AAAA,KAER,mBAAA;EACH,SAAA,GAAY,MAAA,aAAmB,mBAAA;EAC/B,WAAA,QAAmB,mBAAA;EACnB,iBAAA,GAAoB,IAAA,aAAiB,mBAAA;EACrC,WAAA,GAAc,QAAA,aAAqB,mBAAA;EACnC,UAAA,GAAa,OAAA,aAAoB,mBAAA;EACjC,IAAA,GAAO,GAAA,cAAiB,OAAA;AAAA;;;;;;KAQd,eAAA,GAAkB,UAAA;EAC5B,WAAA,GAAc,KAAA,UAAe,GAAA,aAAgB,OAAA;EAC7C,OAAA,OAAc,OAAA,EAAS,MAAA;IACrB,kBAAA,GAAqB,MAAA,EAAQ,MAAA,sBAA4B,mBAAA;EAAA;AAAA;;;;;;;;;;;;;;;;AAX5B;AAQjC;;;;;;;;;;;;cAsCa,aAAA,YAAyB,YAAA;EAAA,iBAKA,QAAA;EAAA,SAJpB,IAAA;EArChB;EAAA,SAuCgB,YAAA;cAEoB,QAAA,EAAU,mBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAqBlB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAlEb;EAAA,QAkGT,YAAA;EAlGgE;EAAA,QAqHhE,YAAA;EAlFH;EAAA,QAyHG,aAAA;AAAA"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
|
|
2
|
+
import { loadOptionalPeer } from "../services/optional-peer.mjs";
|
|
3
|
+
import { pkceChallenge } from "./google-provider.mjs";
|
|
4
|
+
|
|
5
|
+
//#region ../auth/src/providers/apple-provider.ts
|
|
6
|
+
const AUTHORIZATION_ENDPOINT = "https://appleid.apple.com/auth/authorize";
|
|
7
|
+
const TOKEN_ENDPOINT = "https://appleid.apple.com/auth/token";
|
|
8
|
+
const JWKS_URI = "https://appleid.apple.com/auth/keys";
|
|
9
|
+
const ISSUER = "https://appleid.apple.com";
|
|
10
|
+
const DEFAULT_SCOPES = ["name", "email"];
|
|
11
|
+
/** Apple's own recommendation for a per-request client secret: minutes, not the 6-month maximum it allows. */
|
|
12
|
+
const CLIENT_SECRET_TTL_SECONDS = 300;
|
|
13
|
+
/** The `warlock add` feature that installs `jose`. */
|
|
14
|
+
const APPLE_AUTH_FEATURE = "auth-apple";
|
|
15
|
+
/** Apple's key set, created once per process; `jose` caches the fetched JWKS inside it. */
|
|
16
|
+
let appleKeySet;
|
|
17
|
+
/**
|
|
18
|
+
* Apple ("Sign in with Apple") is OpenID Connect over the authorization code
|
|
19
|
+
* flow with PKCE (S256), `state` and `nonce`, the same shape as
|
|
20
|
+
* {@link GoogleProvider} — with two differences that come straight from
|
|
21
|
+
* Apple's own docs (this package has no Apple developer account, so nothing
|
|
22
|
+
* here is proven against a live exchange):
|
|
23
|
+
*
|
|
24
|
+
* 1. Apple has no static client secret. The "secret" the token endpoint wants
|
|
25
|
+
* is a fresh ES256 JWT (`iss` = team id, `sub` = client id, `aud` =
|
|
26
|
+
* `https://appleid.apple.com`, signed with the app's private key and the
|
|
27
|
+
* key id as `kid`) — signed once per callback here rather than cached, to
|
|
28
|
+
* avoid ever presenting a stale one.
|
|
29
|
+
* 2. Requesting `name`/`email` (the default scopes) forces
|
|
30
|
+
* `response_mode=form_post`: Apple POSTs `code`, `state` and, on the
|
|
31
|
+
* user's FIRST authorization only, a `user` field — a JSON string with
|
|
32
|
+
* `{ name: { firstName, lastName }, email } ` that never comes back again
|
|
33
|
+
* on a later login. The callback route must therefore accept a POST body,
|
|
34
|
+
* not just a query string, and `request.input` must read that body the
|
|
35
|
+
* same way it already reads a GET query for the other providers.
|
|
36
|
+
*
|
|
37
|
+
* The id_token is verified with `jose` against Apple's JWKS (signature,
|
|
38
|
+
* `iss`, `aud` = the client id, `exp`), then its `nonce` is compared. Apple is
|
|
39
|
+
* also documented to send `email_verified` (and `is_private_email`) as the
|
|
40
|
+
* STRING `"true"`/`"false"` rather than a JSON boolean on some responses, so
|
|
41
|
+
* both forms are accepted here — treating only the unambiguous `"false"` (or
|
|
42
|
+
* `false`) as unverified. A private-relay address (`@privaterelay.appleid.com`)
|
|
43
|
+
* is still a real, working forwarding address and is accepted as any other.
|
|
44
|
+
*/
|
|
45
|
+
var AppleProvider = class {
|
|
46
|
+
constructor(settings) {
|
|
47
|
+
this.settings = settings;
|
|
48
|
+
this.name = "apple";
|
|
49
|
+
this.callbackMode = "form_post";
|
|
50
|
+
}
|
|
51
|
+
authorizationUrl(state) {
|
|
52
|
+
const scopes = this.settings.scopes ?? DEFAULT_SCOPES;
|
|
53
|
+
const url = new URL(AUTHORIZATION_ENDPOINT);
|
|
54
|
+
url.searchParams.set("client_id", this.settings.clientId);
|
|
55
|
+
url.searchParams.set("redirect_uri", this.settings.redirectUri);
|
|
56
|
+
url.searchParams.set("response_type", "code");
|
|
57
|
+
url.searchParams.set("scope", scopes.join(" "));
|
|
58
|
+
url.searchParams.set("state", state.state);
|
|
59
|
+
url.searchParams.set("nonce", state.nonce);
|
|
60
|
+
url.searchParams.set("code_challenge", pkceChallenge(state.codeVerifier));
|
|
61
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
62
|
+
if (scopes.includes("name") || scopes.includes("email")) url.searchParams.set("response_mode", "form_post");
|
|
63
|
+
return url.toString();
|
|
64
|
+
}
|
|
65
|
+
async handleCallback({ query, expected }) {
|
|
66
|
+
const jose = await loadOptionalPeer("jose", APPLE_AUTH_FEATURE);
|
|
67
|
+
const code = query.code;
|
|
68
|
+
if (typeof code !== "string" || code.length === 0) throw new InvalidProviderCallbackError("missing-code");
|
|
69
|
+
const idToken = await this.exchangeCode(jose, code, expected.codeVerifier);
|
|
70
|
+
const claims = await this.verifyIdToken(jose, idToken);
|
|
71
|
+
if (typeof claims.nonce !== "string" || claims.nonce !== expected.nonce) throw new InvalidProviderCallbackError("nonce-mismatch");
|
|
72
|
+
if (typeof claims.sub !== "string" || claims.sub.length === 0) throw new InvalidProviderCallbackError("missing-subject");
|
|
73
|
+
return {
|
|
74
|
+
provider: this.name,
|
|
75
|
+
providerUserId: claims.sub,
|
|
76
|
+
email: typeof claims.email === "string" ? claims.email : void 0,
|
|
77
|
+
emailVerified: isVerifiedFlag(claims.email_verified),
|
|
78
|
+
name: nameFromUserField(query.user),
|
|
79
|
+
raw: claims
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Sign a fresh ES256 client-secret JWT — Apple has no static one. */
|
|
83
|
+
async clientSecret(jose) {
|
|
84
|
+
const key = await jose.importPKCS8(this.settings.privateKey, "ES256");
|
|
85
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
86
|
+
try {
|
|
87
|
+
return await new jose.SignJWT({}).setProtectedHeader({
|
|
88
|
+
alg: "ES256",
|
|
89
|
+
kid: this.settings.keyId
|
|
90
|
+
}).setIssuer(this.settings.teamId).setIssuedAt().setExpirationTime(now + CLIENT_SECRET_TTL_SECONDS).setAudience(ISSUER).setSubject(this.settings.clientId).sign(key);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new InvalidProviderCallbackError("client-secret-signing-failed", error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Redeem the authorization code (with the PKCE verifier) for an id_token. */
|
|
96
|
+
async exchangeCode(jose, code, codeVerifier) {
|
|
97
|
+
const clientSecret = await this.clientSecret(jose);
|
|
98
|
+
let body;
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(TOKEN_ENDPOINT, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
103
|
+
body: new URLSearchParams({
|
|
104
|
+
code,
|
|
105
|
+
client_id: this.settings.clientId,
|
|
106
|
+
client_secret: clientSecret,
|
|
107
|
+
redirect_uri: this.settings.redirectUri,
|
|
108
|
+
grant_type: "authorization_code",
|
|
109
|
+
code_verifier: codeVerifier
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) throw new Error(`Apple token endpoint answered ${response.status}`);
|
|
113
|
+
body = await response.json();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new InvalidProviderCallbackError("code-exchange-failed", error);
|
|
116
|
+
}
|
|
117
|
+
if (typeof body.id_token !== "string") throw new InvalidProviderCallbackError("missing-id-token");
|
|
118
|
+
return body.id_token;
|
|
119
|
+
}
|
|
120
|
+
/** Verify signature, issuer, audience and expiry; resolve the claims. */
|
|
121
|
+
async verifyIdToken(jose, idToken) {
|
|
122
|
+
appleKeySet ??= jose.createRemoteJWKSet(new URL(JWKS_URI));
|
|
123
|
+
try {
|
|
124
|
+
const { payload } = await jose.jwtVerify(idToken, appleKeySet, {
|
|
125
|
+
issuer: [ISSUER],
|
|
126
|
+
audience: this.settings.clientId
|
|
127
|
+
});
|
|
128
|
+
return payload;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new InvalidProviderCallbackError("invalid-id-token", error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
/** Apple's `email_verified` arrives as a JSON boolean on some responses, a `"true"`/`"false"` string on others. */
|
|
135
|
+
function isVerifiedFlag(value) {
|
|
136
|
+
return value === true || value === "true";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Apple sends the account name only once, on the FIRST authorization, as a
|
|
140
|
+
* `user` form field holding a JSON string: `{"name":{"firstName","lastName"}}`.
|
|
141
|
+
* Every later login omits it entirely — callers that need the name must save
|
|
142
|
+
* it themselves the first time.
|
|
143
|
+
*/
|
|
144
|
+
function nameFromUserField(user) {
|
|
145
|
+
if (typeof user !== "string" || user.length === 0) return void 0;
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(user);
|
|
148
|
+
const name = [parsed.name?.firstName, parsed.name?.lastName].filter((part) => Boolean(part)).join(" ");
|
|
149
|
+
return name.length > 0 ? name : void 0;
|
|
150
|
+
} catch {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
//#endregion
|
|
156
|
+
export { APPLE_AUTH_FEATURE, AppleProvider };
|
|
157
|
+
//# sourceMappingURL=apple-provider.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-provider.mjs","names":[],"sources":["../../../../../../../auth/src/providers/apple-provider.ts"],"sourcesContent":["import type {\n AppleProviderConfig,\n AuthProvider,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { loadOptionalPeer } from \"../services/optional-peer\";\nimport type { JoseModule } from \"./google-provider\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://appleid.apple.com/auth/authorize\";\nconst TOKEN_ENDPOINT = \"https://appleid.apple.com/auth/token\";\nconst JWKS_URI = \"https://appleid.apple.com/auth/keys\";\nconst ISSUER = \"https://appleid.apple.com\";\nconst DEFAULT_SCOPES = [\"name\", \"email\"];\n/** Apple's own recommendation for a per-request client secret: minutes, not the 6-month maximum it allows. */\nconst CLIENT_SECRET_TTL_SECONDS = 5 * 60;\n\n/** The `warlock add` feature that installs `jose`. */\nexport const APPLE_AUTH_FEATURE = \"auth-apple\";\n\ntype AppleSignJwtBuilder = {\n setIssuer: (issuer: string) => AppleSignJwtBuilder;\n setIssuedAt: () => AppleSignJwtBuilder;\n setExpirationTime: (time: number) => AppleSignJwtBuilder;\n setAudience: (audience: string) => AppleSignJwtBuilder;\n setSubject: (subject: string) => AppleSignJwtBuilder;\n sign: (key: unknown) => Promise<string>;\n};\n\n/**\n * The slice of `jose` the Apple provider uses on top of {@link JoseModule} —\n * `importPKCS8` and `SignJWT` sign the ES256 client-secret JWT Apple requires\n * instead of a static client secret.\n */\nexport type AppleJoseModule = JoseModule & {\n importPKCS8: (pkcs8: string, alg: string) => Promise<unknown>;\n SignJWT: new (payload: Record<string, unknown>) => {\n setProtectedHeader: (header: Record<string, unknown>) => AppleSignJwtBuilder;\n };\n};\n\n/** Apple's key set, created once per process; `jose` caches the fetched JWKS inside it. */\nlet appleKeySet: unknown;\n\n/**\n * Apple (\"Sign in with Apple\") is OpenID Connect over the authorization code\n * flow with PKCE (S256), `state` and `nonce`, the same shape as\n * {@link GoogleProvider} — with two differences that come straight from\n * Apple's own docs (this package has no Apple developer account, so nothing\n * here is proven against a live exchange):\n *\n * 1. Apple has no static client secret. The \"secret\" the token endpoint wants\n * is a fresh ES256 JWT (`iss` = team id, `sub` = client id, `aud` =\n * `https://appleid.apple.com`, signed with the app's private key and the\n * key id as `kid`) — signed once per callback here rather than cached, to\n * avoid ever presenting a stale one.\n * 2. Requesting `name`/`email` (the default scopes) forces\n * `response_mode=form_post`: Apple POSTs `code`, `state` and, on the\n * user's FIRST authorization only, a `user` field — a JSON string with\n * `{ name: { firstName, lastName }, email } ` that never comes back again\n * on a later login. The callback route must therefore accept a POST body,\n * not just a query string, and `request.input` must read that body the\n * same way it already reads a GET query for the other providers.\n *\n * The id_token is verified with `jose` against Apple's JWKS (signature,\n * `iss`, `aud` = the client id, `exp`), then its `nonce` is compared. Apple is\n * also documented to send `email_verified` (and `is_private_email`) as the\n * STRING `\"true\"`/`\"false\"` rather than a JSON boolean on some responses, so\n * both forms are accepted here — treating only the unambiguous `\"false\"` (or\n * `false`) as unverified. A private-relay address (`@privaterelay.appleid.com`)\n * is still a real, working forwarding address and is accepted as any other.\n */\nexport class AppleProvider implements AuthProvider {\n public readonly name = \"apple\";\n /** Apple's default scopes (`name`, `email`) force `response_mode=form_post`. */\n public readonly callbackMode = \"form_post\";\n\n public constructor(private readonly settings: AppleProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const scopes = this.settings.scopes ?? DEFAULT_SCOPES;\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", scopes.join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"nonce\", state.nonce);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n // Apple requires form_post whenever name/email scopes are requested.\n if (scopes.includes(\"name\") || scopes.includes(\"email\")) {\n url.searchParams.set(\"response_mode\", \"form_post\");\n }\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n // Load the peer first: a missing SDK fails loudly before any network call.\n const jose = await loadOptionalPeer<AppleJoseModule>(\"jose\", APPLE_AUTH_FEATURE);\n\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const idToken = await this.exchangeCode(jose, code, expected.codeVerifier);\n const claims = await this.verifyIdToken(jose, idToken);\n\n if (typeof claims.nonce !== \"string\" || claims.nonce !== expected.nonce) {\n throw new InvalidProviderCallbackError(\"nonce-mismatch\");\n }\n\n if (typeof claims.sub !== \"string\" || claims.sub.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: claims.sub,\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n emailVerified: isVerifiedFlag(claims.email_verified),\n name: nameFromUserField(query.user),\n raw: claims,\n };\n }\n\n /** Sign a fresh ES256 client-secret JWT — Apple has no static one. */\n private async clientSecret(jose: AppleJoseModule): Promise<string> {\n const key = await jose.importPKCS8(this.settings.privateKey, \"ES256\");\n const now = Math.floor(Date.now() / 1000);\n\n try {\n return await new jose.SignJWT({})\n .setProtectedHeader({ alg: \"ES256\", kid: this.settings.keyId })\n .setIssuer(this.settings.teamId)\n .setIssuedAt()\n .setExpirationTime(now + CLIENT_SECRET_TTL_SECONDS)\n .setAudience(ISSUER)\n .setSubject(this.settings.clientId)\n .sign(key);\n } catch (error) {\n throw new InvalidProviderCallbackError(\"client-secret-signing-failed\", error);\n }\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an id_token. */\n private async exchangeCode(\n jose: AppleJoseModule,\n code: string,\n codeVerifier: string,\n ): Promise<string> {\n const clientSecret = await this.clientSecret(jose);\n let body: { id_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Apple token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { id_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.id_token !== \"string\") {\n throw new InvalidProviderCallbackError(\"missing-id-token\");\n }\n\n return body.id_token;\n }\n\n /** Verify signature, issuer, audience and expiry; resolve the claims. */\n private async verifyIdToken(\n jose: AppleJoseModule,\n idToken: string,\n ): Promise<Record<string, unknown>> {\n appleKeySet ??= jose.createRemoteJWKSet(new URL(JWKS_URI));\n\n try {\n const { payload } = await jose.jwtVerify(idToken, appleKeySet, {\n issuer: [ISSUER],\n audience: this.settings.clientId,\n });\n\n return payload;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"invalid-id-token\", error);\n }\n }\n}\n\n/** Apple's `email_verified` arrives as a JSON boolean on some responses, a `\"true\"`/`\"false\"` string on others. */\nfunction isVerifiedFlag(value: unknown): boolean {\n return value === true || value === \"true\";\n}\n\n/**\n * Apple sends the account name only once, on the FIRST authorization, as a\n * `user` form field holding a JSON string: `{\"name\":{\"firstName\",\"lastName\"}}`.\n * Every later login omits it entirely — callers that need the name must save\n * it themselves the first time.\n */\nfunction nameFromUserField(user: unknown): string | undefined {\n if (typeof user !== \"string\" || user.length === 0) return undefined;\n\n try {\n const parsed = JSON.parse(user) as { name?: { firstName?: string; lastName?: string } };\n const first = parsed.name?.firstName;\n const last = parsed.name?.lastName;\n const name = [first, last].filter((part): part is string => Boolean(part)).join(\" \");\n\n return name.length > 0 ? name : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AuthProvider, DiscordProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../auth/src/providers/discord-provider.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
|
|
6
|
+
* Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the
|
|
7
|
+
* access token is redeemed once for `/users/@me` over a direct HTTPS call.
|
|
8
|
+
* No vendor SDK is involved.
|
|
9
|
+
*/
|
|
10
|
+
declare class DiscordProvider implements AuthProvider {
|
|
11
|
+
private readonly settings;
|
|
12
|
+
readonly name = "discord";
|
|
13
|
+
constructor(settings: DiscordProviderConfig);
|
|
14
|
+
authorizationUrl(state: ProviderAuthorizationState): string;
|
|
15
|
+
handleCallback({
|
|
16
|
+
query,
|
|
17
|
+
expected
|
|
18
|
+
}: ProviderCallbackParams): Promise<ProviderProfile>;
|
|
19
|
+
/** Redeem the authorization code (with the PKCE verifier) for an access token. */
|
|
20
|
+
private exchangeCode;
|
|
21
|
+
private fetchUser;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { DiscordProvider };
|
|
25
|
+
//# sourceMappingURL=discord-provider.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/discord-provider.ts"],"mappings":";;;;;AA8BA;;;;cAAa,eAAA,YAA2B,YAAA;EAAA,iBAGF,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,qBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAtBE;EAAA,QAkDxB,YAAA;EAAA,QAiCA,SAAA;AAAA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
|
|
2
|
+
import { pkceChallenge } from "./google-provider.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../auth/src/providers/discord-provider.ts
|
|
5
|
+
const AUTHORIZATION_ENDPOINT = "https://discord.com/oauth2/authorize";
|
|
6
|
+
const TOKEN_ENDPOINT = "https://discord.com/api/oauth2/token";
|
|
7
|
+
const USER_ENDPOINT = "https://discord.com/api/users/@me";
|
|
8
|
+
const DEFAULT_SCOPES = ["identify", "email"];
|
|
9
|
+
/**
|
|
10
|
+
* Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
|
|
11
|
+
* Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the
|
|
12
|
+
* access token is redeemed once for `/users/@me` over a direct HTTPS call.
|
|
13
|
+
* No vendor SDK is involved.
|
|
14
|
+
*/
|
|
15
|
+
var DiscordProvider = class {
|
|
16
|
+
constructor(settings) {
|
|
17
|
+
this.settings = settings;
|
|
18
|
+
this.name = "discord";
|
|
19
|
+
}
|
|
20
|
+
authorizationUrl(state) {
|
|
21
|
+
const url = new URL(AUTHORIZATION_ENDPOINT);
|
|
22
|
+
url.searchParams.set("client_id", this.settings.clientId);
|
|
23
|
+
url.searchParams.set("redirect_uri", this.settings.redirectUri);
|
|
24
|
+
url.searchParams.set("response_type", "code");
|
|
25
|
+
url.searchParams.set("scope", (this.settings.scopes ?? DEFAULT_SCOPES).join(" "));
|
|
26
|
+
url.searchParams.set("state", state.state);
|
|
27
|
+
url.searchParams.set("code_challenge", pkceChallenge(state.codeVerifier));
|
|
28
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
29
|
+
return url.toString();
|
|
30
|
+
}
|
|
31
|
+
async handleCallback({ query, expected }) {
|
|
32
|
+
const code = query.code;
|
|
33
|
+
if (typeof code !== "string" || code.length === 0) throw new InvalidProviderCallbackError("missing-code");
|
|
34
|
+
const accessToken = await this.exchangeCode(code, expected.codeVerifier);
|
|
35
|
+
const user = await this.fetchUser(accessToken);
|
|
36
|
+
if (typeof user.id !== "string" || user.id.length === 0) throw new InvalidProviderCallbackError("missing-subject");
|
|
37
|
+
return {
|
|
38
|
+
provider: this.name,
|
|
39
|
+
providerUserId: user.id,
|
|
40
|
+
email: user.email ?? void 0,
|
|
41
|
+
emailVerified: user.verified === true,
|
|
42
|
+
name: user.global_name ?? user.username,
|
|
43
|
+
avatar: user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png` : void 0,
|
|
44
|
+
raw: user
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Redeem the authorization code (with the PKCE verifier) for an access token. */
|
|
48
|
+
async exchangeCode(code, codeVerifier) {
|
|
49
|
+
let body;
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(TOKEN_ENDPOINT, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
54
|
+
body: new URLSearchParams({
|
|
55
|
+
code,
|
|
56
|
+
client_id: this.settings.clientId,
|
|
57
|
+
client_secret: this.settings.clientSecret,
|
|
58
|
+
redirect_uri: this.settings.redirectUri,
|
|
59
|
+
grant_type: "authorization_code",
|
|
60
|
+
code_verifier: codeVerifier
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) throw new Error(`Discord token endpoint answered ${response.status}`);
|
|
64
|
+
body = await response.json();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new InvalidProviderCallbackError("code-exchange-failed", error);
|
|
67
|
+
}
|
|
68
|
+
if (typeof body.access_token !== "string" || body.access_token.length === 0) throw new InvalidProviderCallbackError("missing-access-token");
|
|
69
|
+
return body.access_token;
|
|
70
|
+
}
|
|
71
|
+
async fetchUser(accessToken) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(USER_ENDPOINT, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
74
|
+
if (!response.ok) throw new Error(`Discord user endpoint answered ${response.status}`);
|
|
75
|
+
return await response.json();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new InvalidProviderCallbackError("profile-fetch-failed", error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
export { DiscordProvider };
|
|
84
|
+
//# sourceMappingURL=discord-provider.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-provider.mjs","names":[],"sources":["../../../../../../../auth/src/providers/discord-provider.ts"],"sourcesContent":["import type {\n AuthProvider,\n DiscordProviderConfig,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://discord.com/oauth2/authorize\";\nconst TOKEN_ENDPOINT = \"https://discord.com/api/oauth2/token\";\nconst USER_ENDPOINT = \"https://discord.com/api/users/@me\";\nconst DEFAULT_SCOPES = [\"identify\", \"email\"];\n\ntype DiscordUser = {\n id: string;\n username: string;\n global_name?: string | null;\n avatar?: string | null;\n email?: string | null;\n verified?: boolean;\n};\n\n/**\n * Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).\n * Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the\n * access token is redeemed once for `/users/@me` over a direct HTTPS call.\n * No vendor SDK is involved.\n */\nexport class DiscordProvider implements AuthProvider {\n public readonly name = \"discord\";\n\n public constructor(private readonly settings: DiscordProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (this.settings.scopes ?? DEFAULT_SCOPES).join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const accessToken = await this.exchangeCode(code, expected.codeVerifier);\n const user = await this.fetchUser(accessToken);\n\n if (typeof user.id !== \"string\" || user.id.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: user.id,\n email: user.email ?? undefined,\n emailVerified: user.verified === true,\n name: user.global_name ?? user.username,\n avatar: user.avatar\n ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`\n : undefined,\n raw: user,\n };\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an access token. */\n private async exchangeCode(code: string, codeVerifier: string): Promise<string> {\n let body: { access_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: this.settings.clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Discord token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { access_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.access_token !== \"string\" || body.access_token.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-access-token\");\n }\n\n return body.access_token;\n }\n\n private async fetchUser(accessToken: string): Promise<DiscordUser> {\n try {\n const response = await fetch(USER_ENDPOINT, {\n headers: { authorization: `Bearer ${accessToken}` },\n });\n\n if (!response.ok) {\n throw new Error(`Discord user endpoint answered ${response.status}`);\n }\n\n return (await response.json()) as DiscordUser;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"profile-fetch-failed\", error);\n }\n }\n}\n"],"mappings":";;;;AAUA,MAAM,yBAAyB;AAC/B,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB,CAAC,YAAY,OAAO;;;;;;;AAiB3C,IAAa,kBAAb,MAAqD;CAGnD,AAAO,YAAY,AAAiB,UAAiC;EAAjC;cAFb;CAE+C;CAEtE,AAAO,iBAAiB,OAA2C;EACjE,MAAM,MAAM,IAAI,IAAI,sBAAsB;EAE1C,IAAI,aAAa,IAAI,aAAa,KAAK,SAAS,QAAQ;EACxD,IAAI,aAAa,IAAI,gBAAgB,KAAK,SAAS,WAAW;EAC9D,IAAI,aAAa,IAAI,iBAAiB,MAAM;EAC5C,IAAI,aAAa,IAAI,UAAU,KAAK,SAAS,UAAU,eAAc,CAAE,KAAK,GAAG,CAAC;EAChF,IAAI,aAAa,IAAI,SAAS,MAAM,KAAK;EACzC,IAAI,aAAa,IAAI,kBAAkB,cAAc,MAAM,YAAY,CAAC;EACxE,IAAI,aAAa,IAAI,yBAAyB,MAAM;EAEpD,OAAO,IAAI,SAAS;CACtB;CAEA,MAAa,eAAe,EAC1B,OACA,YACmD;EACnD,MAAM,OAAO,MAAM;EAEnB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,6BAA6B,cAAc;EAGvD,MAAM,cAAc,MAAM,KAAK,aAAa,MAAM,SAAS,YAAY;EACvE,MAAM,OAAO,MAAM,KAAK,UAAU,WAAW;EAE7C,IAAI,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,WAAW,GACpD,MAAM,IAAI,6BAA6B,iBAAiB;EAG1D,OAAO;GACL,UAAU,KAAK;GACf,gBAAgB,KAAK;GACrB,OAAO,KAAK,SAAS;GACrB,eAAe,KAAK,aAAa;GACjC,MAAM,KAAK,eAAe,KAAK;GAC/B,QAAQ,KAAK,SACT,sCAAsC,KAAK,GAAG,GAAG,KAAK,OAAO,QAC7D;GACJ,KAAK;EACP;CACF;;CAGA,MAAc,aAAa,MAAc,cAAuC;EAC9E,IAAI;EAEJ,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,gBAAgB;IAC3C,QAAQ;IACR,SAAS,EAAE,gBAAgB,oCAAoC;IAC/D,MAAM,IAAI,gBAAgB;KACxB;KACA,WAAW,KAAK,SAAS;KACzB,eAAe,KAAK,SAAS;KAC7B,cAAc,KAAK,SAAS;KAC5B,YAAY;KACZ,eAAe;IACjB,CAAC;GACH,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,mCAAmC,SAAS,QAAQ;GAGtE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;EAEA,IAAI,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,WAAW,GACxE,MAAM,IAAI,6BAA6B,sBAAsB;EAG/D,OAAO,KAAK;CACd;CAEA,MAAc,UAAU,aAA2C;EACjE,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,eAAe,EAC1C,SAAS,EAAE,eAAe,UAAU,cAAc,EACpD,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,kCAAkC,SAAS,QAAQ;GAGrE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;CACF;AACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AuthProvider, FacebookProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../auth/src/providers/facebook-provider.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Facebook sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
|
|
6
|
+
* Facebook is not OpenID Connect — there is no `id_token`. Unlike every other
|
|
7
|
+
* built-in provider, Facebook's token endpoint is redeemed with a `GET`
|
|
8
|
+
* request (query-string parameters, not a POST body) — that shape comes
|
|
9
|
+
* straight from Facebook's Graph API docs, not from a live exchange (this
|
|
10
|
+
* package has no Facebook test app). The profile is then read once from
|
|
11
|
+
* `Graph /me?fields=id,name,email,picture`, over a direct HTTPS call; no
|
|
12
|
+
* Facebook SDK is involved.
|
|
13
|
+
*
|
|
14
|
+
* Facebook only ever returns a confirmed address for `email` — there is no
|
|
15
|
+
* separate verified flag — so an email present on the profile is treated as
|
|
16
|
+
* verified. An app without `email` permission granted, or one that simply has
|
|
17
|
+
* no email on file, gets `email: undefined`, which `resolveProviderUser`
|
|
18
|
+
* already rejects as an unverified/missing email, the same path GitHub's
|
|
19
|
+
* unverified addresses take.
|
|
20
|
+
*/
|
|
21
|
+
declare class FacebookProvider implements AuthProvider {
|
|
22
|
+
private readonly settings;
|
|
23
|
+
readonly name = "facebook";
|
|
24
|
+
constructor(settings: FacebookProviderConfig);
|
|
25
|
+
authorizationUrl(state: ProviderAuthorizationState): string;
|
|
26
|
+
handleCallback({
|
|
27
|
+
query,
|
|
28
|
+
expected
|
|
29
|
+
}: ProviderCallbackParams): Promise<ProviderProfile>;
|
|
30
|
+
/** Redeem the authorization code (with the PKCE verifier) for an access token. */
|
|
31
|
+
private exchangeCode;
|
|
32
|
+
private fetchUser;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { FacebookProvider };
|
|
36
|
+
//# sourceMappingURL=facebook-provider.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facebook-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/facebook-provider.ts"],"mappings":";;;;;AAuCA;;;;;;;;;;;;;;;cAAa,gBAAA,YAA4B,YAAA;EAAA,iBAGH,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,sBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAjBZ;EAAA,QA4CV,YAAA;EAAA,QAkCA,SAAA;AAAA"}
|