@checodo/better-auth-authentication 0.0.0-stage → 0.1.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 +7 -0
- package/LICENSE.md +21 -0
- package/README.md +44 -2
- package/dist/client.d.ts +25 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +27 -0
- package/dist/client.js.map +1 -0
- package/dist/error-codes-CVA2nmg9.js +16 -0
- package/dist/error-codes-CVA2nmg9.js.map +1 -0
- package/dist/index-B-TXlJI5.d.ts +327 -0
- package/dist/index-B-TXlJI5.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +578 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -3
package/CHANGELOG.md
ADDED
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clas Heinemeyer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @checodo/better-auth-authentication
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sign in with Steam, Epic Games, and Xbox in [better-auth](https://better-auth.com), through `signIn.social` like any built-in social provider.
|
|
4
|
+
|
|
5
|
+
A community plugin, not made by the better-auth team. It looks and works
|
|
6
|
+
like better-auth's own plugins: a server plugin, a typed client plugin,
|
|
7
|
+
error codes.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @checodo/better-auth-authentication
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires `better-auth` and `@better-auth/core` 1.7.20 or a later 1.x
|
|
16
|
+
release (peer dependencies). ESM only.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// auth.ts
|
|
22
|
+
import { betterAuth } from "better-auth";
|
|
23
|
+
import { authentication } from "@checodo/better-auth-authentication";
|
|
24
|
+
|
|
25
|
+
export const auth = betterAuth({
|
|
26
|
+
plugins: [
|
|
27
|
+
authentication({
|
|
28
|
+
steam: { apiKey: process.env.STEAM_API_KEY },
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// auth-client.ts
|
|
36
|
+
import { createAuthClient } from "better-auth/client";
|
|
37
|
+
import { authenticationClient } from "@checodo/better-auth-authentication/client";
|
|
38
|
+
|
|
39
|
+
export const authClient = createAuthClient({ plugins: [authenticationClient()] });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Documentation
|
|
43
|
+
|
|
44
|
+
Every option, endpoint, error code, and table:
|
|
45
|
+
[docs](https://github.com/checodo/better-auth-plugins/blob/main/apps/docs/content/docs/authentication/index.mdx).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { i as AUTHENTICATION_ERROR_CODES, r as AUTHENTICATION_CALLBACK_ERRORS, t as AuthenticationPlugin } from "./index-B-TXlJI5.js";
|
|
2
|
+
/**
|
|
3
|
+
* Client companion to `authentication()`. The providers need no client
|
|
4
|
+
* methods of their own: sign in with better-auth's `signIn.social` and link
|
|
5
|
+
* with `linkSocial`. This adds the plugin's error codes to the client.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createAuthClient } from "better-auth/client";
|
|
10
|
+
* import { authenticationClient } from "@checodo/better-auth-authentication/client";
|
|
11
|
+
*
|
|
12
|
+
* export const authClient = createAuthClient({ plugins: [authenticationClient()] });
|
|
13
|
+
*
|
|
14
|
+
* await authClient.signIn.social({ provider: "epic", callbackURL: "/dashboard" });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const authenticationClient: () => {
|
|
18
|
+
id: "authentication";
|
|
19
|
+
$InferServerPlugin: AuthenticationPlugin;
|
|
20
|
+
$ERROR_CODES: {
|
|
21
|
+
EMAIL_DOMAIN_RESERVED: import("better-auth").RawError<"EMAIL_DOMAIN_RESERVED">;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES };
|
|
25
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;;;;;;;qBAmBa;;EAGiB,oBAAA"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { n as AUTHENTICATION_ERROR_CODES, t as AUTHENTICATION_CALLBACK_ERRORS } from "./error-codes-CVA2nmg9.js";
|
|
2
|
+
// src/client.ts
|
|
3
|
+
/**
|
|
4
|
+
* Client companion to `authentication()`. The providers need no client
|
|
5
|
+
* methods of their own: sign in with better-auth's `signIn.social` and link
|
|
6
|
+
* with `linkSocial`. This adds the plugin's error codes to the client.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createAuthClient } from "better-auth/client";
|
|
11
|
+
* import { authenticationClient } from "@checodo/better-auth-authentication/client";
|
|
12
|
+
*
|
|
13
|
+
* export const authClient = createAuthClient({ plugins: [authenticationClient()] });
|
|
14
|
+
*
|
|
15
|
+
* await authClient.signIn.social({ provider: "epic", callbackURL: "/dashboard" });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
const authenticationClient = () => {
|
|
19
|
+
return {
|
|
20
|
+
id: "authentication",
|
|
21
|
+
$InferServerPlugin: {},
|
|
22
|
+
$ERROR_CODES: AUTHENTICATION_ERROR_CODES
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES, authenticationClient };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from \"better-auth/client\";\nimport { AUTHENTICATION_ERROR_CODES } from \"./error-codes\";\nimport type { AuthenticationPlugin } from \"./index\";\n\n/**\n * Client companion to `authentication()`. The providers need no client\n * methods of their own: sign in with better-auth's `signIn.social` and link\n * with `linkSocial`. This adds the plugin's error codes to the client.\n *\n * @example\n * ```ts\n * import { createAuthClient } from \"better-auth/client\";\n * import { authenticationClient } from \"@checodo/better-auth-authentication/client\";\n *\n * export const authClient = createAuthClient({ plugins: [authenticationClient()] });\n *\n * await authClient.signIn.social({ provider: \"epic\", callbackURL: \"/dashboard\" });\n * ```\n */\nexport const authenticationClient = () => {\n return {\n id: \"authentication\",\n $InferServerPlugin: {} as AuthenticationPlugin,\n $ERROR_CODES: AUTHENTICATION_ERROR_CODES,\n } satisfies BetterAuthClientPlugin;\n};\n\nexport { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES } from \"./error-codes\";\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,MAAa,6BAA6B;CACxC,OAAO;EACL,IAAI;EACJ,oBAAoB,CAAC;EACrB,cAAc;CAChB;AACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
|
|
2
|
+
// src/error-codes.ts
|
|
3
|
+
const AUTHENTICATION_ERROR_CODES = defineErrorCodes({ EMAIL_DOMAIN_RESERVED: "This email domain is reserved for accounts from a sign-in provider and can't be used" });
|
|
4
|
+
/**
|
|
5
|
+
* `error` values in the query of the error page a failed sign-in redirects
|
|
6
|
+
* to (better-auth's `/error`, or `errorCallbackURL`), besides better-auth's own.
|
|
7
|
+
*/
|
|
8
|
+
const AUTHENTICATION_CALLBACK_ERRORS = {
|
|
9
|
+
/** The player cancelled on Steam's sign-in page. */
|
|
10
|
+
STEAM_SIGN_IN_CANCELLED: "steam_sign_in_cancelled",
|
|
11
|
+
/** Steam's answer didn't check out, or Steam couldn't confirm it. */
|
|
12
|
+
STEAM_VERIFICATION_FAILED: "steam_verification_failed"
|
|
13
|
+
};
|
|
14
|
+
export { AUTHENTICATION_ERROR_CODES as n, AUTHENTICATION_CALLBACK_ERRORS as t };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=error-codes-CVA2nmg9.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-codes-CVA2nmg9.js","names":[],"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from \"@better-auth/core/utils/error-codes\";\n\nexport const AUTHENTICATION_ERROR_CODES = defineErrorCodes({\n EMAIL_DOMAIN_RESERVED:\n \"This email domain is reserved for accounts from a sign-in provider and can't be used\",\n});\n\nexport type AuthenticationErrorCode = keyof typeof AUTHENTICATION_ERROR_CODES;\n\n/**\n * `error` values in the query of the error page a failed sign-in redirects\n * to (better-auth's `/error`, or `errorCallbackURL`), besides better-auth's own.\n */\nexport const AUTHENTICATION_CALLBACK_ERRORS = {\n /** The player cancelled on Steam's sign-in page. */\n STEAM_SIGN_IN_CANCELLED: \"steam_sign_in_cancelled\",\n /** Steam's answer didn't check out, or Steam couldn't confirm it. */\n STEAM_VERIFICATION_FAILED: \"steam_verification_failed\",\n} as const;\n"],"mappings":";;AAEA,MAAa,6BAA6B,iBAAiB,EACzD,uBACE,uFACJ,CAAC;;;;;AAQD,MAAa,iCAAiC;;CAE5C,yBAAyB;;CAEzB,2BAA2B;AAC7B"}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { OAuthMappedUser, ProviderOptions } from "@better-auth/core/oauth2";
|
|
2
|
+
/** Like better-auth's `mapProfileToUser`: changes the user created from a profile. */
|
|
3
|
+
type ProfileMapper<Profile extends object> = (profile: Profile) => OAuthMappedUser | Promise<OAuthMappedUser>;
|
|
4
|
+
/**
|
|
5
|
+
* What every provider accepts, like better-auth's built-in providers:
|
|
6
|
+
* sign-up control and profile mapping.
|
|
7
|
+
*/
|
|
8
|
+
interface CommonProviderOptions<Profile extends object> {
|
|
9
|
+
/** Only existing users can sign in; new users get an error. */
|
|
10
|
+
disableSignUp?: boolean | undefined;
|
|
11
|
+
/** New users must sign in with `requestSignUp: true`. */
|
|
12
|
+
disableImplicitSignUp?: boolean | undefined;
|
|
13
|
+
/** Update the user's name and image from the provider on every sign-in. */
|
|
14
|
+
overrideUserInfoOnSignIn?: boolean | undefined;
|
|
15
|
+
/** Change the user created from the provider's profile. */
|
|
16
|
+
mapProfileToUser?: ProfileMapper<Profile> | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Domain of the placeholder email for users the provider gives no email
|
|
19
|
+
* (`<account id>@<domain>`). Defaults to `<provider id>.invalid`, a domain
|
|
20
|
+
* that can never receive email. Sign-ups with this domain are refused
|
|
21
|
+
* everywhere else, so nobody can claim a player's placeholder.
|
|
22
|
+
*/
|
|
23
|
+
emailDomain?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
/** OAuth options shared by Epic and Xbox, like `github({ ... })`. */
|
|
26
|
+
type OAuthClientOptions = Pick<ProviderOptions, "clientSecret" | "scope" | "disableDefaultScope" | "redirectURI" | "prompt"> & {
|
|
27
|
+
clientId: string;
|
|
28
|
+
clientSecret: string;
|
|
29
|
+
};
|
|
30
|
+
/** The Steam player, from the Steam Web API's `GetPlayerSummaries`. */
|
|
31
|
+
interface SteamProfile {
|
|
32
|
+
/** 64-bit SteamID, as a string. */
|
|
33
|
+
steamid: string;
|
|
34
|
+
personaname?: string | undefined;
|
|
35
|
+
profileurl?: string | undefined;
|
|
36
|
+
avatar?: string | undefined;
|
|
37
|
+
avatarmedium?: string | undefined;
|
|
38
|
+
avatarfull?: string | undefined;
|
|
39
|
+
/** Only for public profiles. */
|
|
40
|
+
realname?: string | undefined;
|
|
41
|
+
/** Only for public profiles. */
|
|
42
|
+
loccountrycode?: string | undefined;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
interface SteamOptions extends CommonProviderOptions<SteamProfile> {
|
|
46
|
+
/**
|
|
47
|
+
* A Steam Web API key (https://steamcommunity.com/dev/apikey), to load
|
|
48
|
+
* the player's name and avatar. Without it, users are named after their
|
|
49
|
+
* SteamID. Never logged.
|
|
50
|
+
*/
|
|
51
|
+
apiKey?: string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Web API host. Defaults to `https://api.steampowered.com`; publisher keys
|
|
54
|
+
* use `https://partner.steam-api.com`.
|
|
55
|
+
*/
|
|
56
|
+
apiBaseURL?: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
/** The Epic Games account, from the ID token. */
|
|
59
|
+
interface EpicProfile {
|
|
60
|
+
/** The Epic account id. */
|
|
61
|
+
sub: string;
|
|
62
|
+
/** The display name. */
|
|
63
|
+
dn?: string | undefined;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
type EpicOptions = OAuthClientOptions & CommonProviderOptions<EpicProfile> & {
|
|
67
|
+
/**
|
|
68
|
+
* Send a PKCE challenge. Epic doesn't document PKCE for web sign-in, so
|
|
69
|
+
* it is off by default.
|
|
70
|
+
*/
|
|
71
|
+
pkce?: boolean | undefined;
|
|
72
|
+
};
|
|
73
|
+
/** The Xbox player, from the XSTS token's display claims. */
|
|
74
|
+
interface XboxProfile {
|
|
75
|
+
/** The Xbox user id (XUID). */
|
|
76
|
+
xid: string;
|
|
77
|
+
/** The gamertag. */
|
|
78
|
+
gtg?: string | undefined;
|
|
79
|
+
/** User hash. */
|
|
80
|
+
uhs?: string | undefined;
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
type XboxOptions = Omit<OAuthClientOptions, "scope" | "disableDefaultScope"> & CommonProviderOptions<XboxProfile> & {
|
|
84
|
+
/**
|
|
85
|
+
* The XSTS relying party. Defaults to `http://xboxlive.com`, which
|
|
86
|
+
* returns the XUID and gamertag; Xbox partners can use their own.
|
|
87
|
+
*/
|
|
88
|
+
relyingParty?: string | undefined;
|
|
89
|
+
/** XSTS sandbox. Defaults to `RETAIL`. */
|
|
90
|
+
sandboxId?: string | undefined;
|
|
91
|
+
/** Request `xboxlive.offline_access` for a refresh token. Defaults to `false`. */
|
|
92
|
+
offlineAccess?: boolean | undefined;
|
|
93
|
+
};
|
|
94
|
+
interface AuthenticationOptions {
|
|
95
|
+
/** Sign in with Steam (OpenID 2.0). */
|
|
96
|
+
steam?: SteamOptions | undefined;
|
|
97
|
+
/** Sign in with Epic Games (Epic Account Services, OpenID Connect). */
|
|
98
|
+
epic?: EpicOptions | undefined;
|
|
99
|
+
/** Sign in with Xbox (Microsoft account, then Xbox Live). Needs Xbox developer access. */
|
|
100
|
+
xbox?: XboxOptions | undefined;
|
|
101
|
+
}
|
|
102
|
+
/** The provider ids this plugin adds. */
|
|
103
|
+
type AuthenticationProviderId = "steam" | "epic" | "xbox";
|
|
104
|
+
declare const AUTHENTICATION_ERROR_CODES: {
|
|
105
|
+
EMAIL_DOMAIN_RESERVED: import("better-auth").RawError<"EMAIL_DOMAIN_RESERVED">;
|
|
106
|
+
};
|
|
107
|
+
type AuthenticationErrorCode = keyof typeof AUTHENTICATION_ERROR_CODES;
|
|
108
|
+
/**
|
|
109
|
+
* `error` values in the query of the error page a failed sign-in redirects
|
|
110
|
+
* to (better-auth's `/error`, or `errorCallbackURL`), besides better-auth's own.
|
|
111
|
+
*/
|
|
112
|
+
declare const AUTHENTICATION_CALLBACK_ERRORS: {
|
|
113
|
+
/** The player cancelled on Steam's sign-in page. */
|
|
114
|
+
readonly STEAM_SIGN_IN_CANCELLED: "steam_sign_in_cancelled";
|
|
115
|
+
/** Steam's answer didn't check out, or Steam couldn't confirm it. */
|
|
116
|
+
readonly STEAM_VERIFICATION_FAILED: "steam_verification_failed";
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Adds Steam, Epic Games, and Xbox as social providers. They are used like
|
|
120
|
+
* better-auth's built-in ones: `signIn.social({ provider: "steam" })`,
|
|
121
|
+
* `linkSocial`, callbacks at `/callback/<provider>`, account linking,
|
|
122
|
+
* `callbackURL` checks, and sign-up options all work the same way.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* import { betterAuth } from "better-auth";
|
|
127
|
+
* import { authentication } from "@checodo/better-auth-authentication";
|
|
128
|
+
*
|
|
129
|
+
* export const auth = betterAuth({
|
|
130
|
+
* plugins: [
|
|
131
|
+
* authentication({
|
|
132
|
+
* steam: { apiKey: process.env.STEAM_API_KEY },
|
|
133
|
+
* epic: { clientId: "...", clientSecret: "..." },
|
|
134
|
+
* }),
|
|
135
|
+
* ],
|
|
136
|
+
* });
|
|
137
|
+
*
|
|
138
|
+
* // client
|
|
139
|
+
* await authClient.signIn.social({ provider: "steam", callbackURL: "/dashboard" });
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
declare const authentication: (options: AuthenticationOptions) => {
|
|
143
|
+
id: "authentication";
|
|
144
|
+
version: string;
|
|
145
|
+
init(ctx: import("better-auth").AuthContext): {
|
|
146
|
+
context: {
|
|
147
|
+
socialProviders: import("better-auth").OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
|
|
148
|
+
};
|
|
149
|
+
options: {
|
|
150
|
+
databaseHooks: {
|
|
151
|
+
user?: {
|
|
152
|
+
create?: {
|
|
153
|
+
before?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
154
|
+
data: {
|
|
155
|
+
id?: string | undefined;
|
|
156
|
+
createdAt?: Date | undefined;
|
|
157
|
+
updatedAt?: Date | undefined;
|
|
158
|
+
email?: string | undefined;
|
|
159
|
+
emailVerified?: boolean | undefined;
|
|
160
|
+
name?: string | undefined;
|
|
161
|
+
image?: string | null | undefined;
|
|
162
|
+
} & Record<string, any>;
|
|
163
|
+
}>;
|
|
164
|
+
after?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
165
|
+
};
|
|
166
|
+
update?: {
|
|
167
|
+
before?: (user: Partial<import("better-auth").User> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
168
|
+
data: {
|
|
169
|
+
[x: string]: any;
|
|
170
|
+
id?: string | undefined;
|
|
171
|
+
createdAt?: Date | undefined;
|
|
172
|
+
updatedAt?: Date | undefined;
|
|
173
|
+
email?: string | undefined;
|
|
174
|
+
emailVerified?: boolean | undefined;
|
|
175
|
+
name?: string | undefined;
|
|
176
|
+
image?: string | null | undefined;
|
|
177
|
+
};
|
|
178
|
+
}>;
|
|
179
|
+
after?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
180
|
+
};
|
|
181
|
+
delete?: {
|
|
182
|
+
before?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
|
|
183
|
+
after?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
session?: {
|
|
187
|
+
create?: {
|
|
188
|
+
before?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
189
|
+
data: {
|
|
190
|
+
id?: string | undefined;
|
|
191
|
+
createdAt?: Date | undefined;
|
|
192
|
+
updatedAt?: Date | undefined;
|
|
193
|
+
userId?: string | undefined;
|
|
194
|
+
expiresAt?: Date | undefined;
|
|
195
|
+
token?: string | undefined;
|
|
196
|
+
ipAddress?: string | null | undefined;
|
|
197
|
+
userAgent?: string | null | undefined;
|
|
198
|
+
} & Record<string, any>;
|
|
199
|
+
}>;
|
|
200
|
+
after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
201
|
+
};
|
|
202
|
+
update?: {
|
|
203
|
+
before?: (session: Partial<import("better-auth").Session> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
204
|
+
data: {
|
|
205
|
+
[x: string]: any;
|
|
206
|
+
id?: string | undefined;
|
|
207
|
+
createdAt?: Date | undefined;
|
|
208
|
+
updatedAt?: Date | undefined;
|
|
209
|
+
userId?: string | undefined;
|
|
210
|
+
expiresAt?: Date | undefined;
|
|
211
|
+
token?: string | undefined;
|
|
212
|
+
ipAddress?: string | null | undefined;
|
|
213
|
+
userAgent?: string | null | undefined;
|
|
214
|
+
};
|
|
215
|
+
}>;
|
|
216
|
+
after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
217
|
+
};
|
|
218
|
+
delete?: {
|
|
219
|
+
before?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
|
|
220
|
+
after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
account?: {
|
|
224
|
+
create?: {
|
|
225
|
+
before?: (account: import("better-auth").Account, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
226
|
+
data: {
|
|
227
|
+
id?: string | undefined;
|
|
228
|
+
createdAt?: Date | undefined;
|
|
229
|
+
updatedAt?: Date | undefined;
|
|
230
|
+
providerId?: string | undefined;
|
|
231
|
+
accountId?: string | undefined;
|
|
232
|
+
userId?: string | undefined;
|
|
233
|
+
accessToken?: string | null | undefined;
|
|
234
|
+
refreshToken?: string | null | undefined;
|
|
235
|
+
idToken?: string | null | undefined;
|
|
236
|
+
accessTokenExpiresAt?: Date | null | undefined;
|
|
237
|
+
refreshTokenExpiresAt?: Date | null | undefined;
|
|
238
|
+
scope?: string | null | undefined;
|
|
239
|
+
password?: string | null | undefined;
|
|
240
|
+
} & Record<string, any>;
|
|
241
|
+
}>;
|
|
242
|
+
after?: (account: import("better-auth").Account, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
243
|
+
};
|
|
244
|
+
update?: {
|
|
245
|
+
before?: (account: Partial<import("better-auth").Account> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
246
|
+
data: {
|
|
247
|
+
[x: string]: any;
|
|
248
|
+
id?: string | undefined;
|
|
249
|
+
createdAt?: Date | undefined;
|
|
250
|
+
updatedAt?: Date | undefined;
|
|
251
|
+
providerId?: string | undefined;
|
|
252
|
+
accountId?: string | undefined;
|
|
253
|
+
userId?: string | undefined;
|
|
254
|
+
accessToken?: string | null | undefined;
|
|
255
|
+
refreshToken?: string | null | undefined;
|
|
256
|
+
idToken?: string | null | undefined;
|
|
257
|
+
accessTokenExpiresAt?: Date | null | undefined;
|
|
258
|
+
refreshTokenExpiresAt?: Date | null | undefined;
|
|
259
|
+
scope?: string | null | undefined;
|
|
260
|
+
password?: string | null | undefined;
|
|
261
|
+
};
|
|
262
|
+
}>;
|
|
263
|
+
after?: (account: import("better-auth").Account & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
264
|
+
};
|
|
265
|
+
delete?: {
|
|
266
|
+
before?: (account: import("better-auth").Account & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
|
|
267
|
+
after?: (account: import("better-auth").Account & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
verification?: {
|
|
271
|
+
create?: {
|
|
272
|
+
before?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
273
|
+
data: {
|
|
274
|
+
id?: string | undefined;
|
|
275
|
+
createdAt?: Date | undefined;
|
|
276
|
+
updatedAt?: Date | undefined;
|
|
277
|
+
value?: string | undefined;
|
|
278
|
+
expiresAt?: Date | undefined;
|
|
279
|
+
identifier?: string | undefined;
|
|
280
|
+
} & Record<string, any>;
|
|
281
|
+
}>;
|
|
282
|
+
after?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
283
|
+
};
|
|
284
|
+
update?: {
|
|
285
|
+
before?: (verification: Partial<import("better-auth").Verification> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
286
|
+
data: {
|
|
287
|
+
[x: string]: any;
|
|
288
|
+
id?: string | undefined;
|
|
289
|
+
createdAt?: Date | undefined;
|
|
290
|
+
updatedAt?: Date | undefined;
|
|
291
|
+
value?: string | undefined;
|
|
292
|
+
expiresAt?: Date | undefined;
|
|
293
|
+
identifier?: string | undefined;
|
|
294
|
+
};
|
|
295
|
+
}>;
|
|
296
|
+
after?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
297
|
+
};
|
|
298
|
+
delete?: {
|
|
299
|
+
before?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
|
|
300
|
+
after?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
} | undefined;
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
hooks: {
|
|
307
|
+
before: {
|
|
308
|
+
matcher: (context: import("better-auth").HookEndpointContext) => boolean;
|
|
309
|
+
handler: import("better-auth/api").AuthMiddleware;
|
|
310
|
+
}[];
|
|
311
|
+
};
|
|
312
|
+
$ERROR_CODES: {
|
|
313
|
+
EMAIL_DOMAIN_RESERVED: import("better-auth").RawError<"EMAIL_DOMAIN_RESERVED">;
|
|
314
|
+
};
|
|
315
|
+
options: AuthenticationOptions;
|
|
316
|
+
};
|
|
317
|
+
type AuthenticationPlugin = ReturnType<typeof authentication>;
|
|
318
|
+
/** Lets `auth.$context.getPlugin("authentication")` infer this plugin's exact type. */
|
|
319
|
+
declare module "@better-auth/core" {
|
|
320
|
+
interface BetterAuthPluginRegistry<AuthOptions, Options> {
|
|
321
|
+
authentication: {
|
|
322
|
+
creator: typeof authentication;
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export { AuthenticationErrorCode as a, CommonProviderOptions as c, SteamOptions as d, SteamProfile as f, AUTHENTICATION_ERROR_CODES as i, EpicOptions as l, XboxProfile as m, authentication as n, AuthenticationOptions as o, XboxOptions as p, AUTHENTICATION_CALLBACK_ERRORS as r, AuthenticationProviderId as s, AuthenticationPlugin as t, EpicProfile as u };
|
|
327
|
+
//# sourceMappingURL=index-B-TXlJI5.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-B-TXlJI5.d.ts","names":[],"sources":["../src/types.ts","../src/error-codes.ts","../src/index.ts"],"mappings":";;KAGY,cAAc,2BACxB,SAAS,YACN,kBAAkB,QAAQ;;;;;UAMd,sBAAsB;;EAErC;;EAEA;;EAEA;;EAEA,mBAAmB,cAAc;;;;;;;EAOjC;;;KAIU,qBAAqB,KAC/B;EAGA;EACA;;;UAIe;;EAEf;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;GACC;;UAGc,qBAAqB,sBAAsB;;;;;;EAM1D;;;;;EAKA;;;UAIe;;EAEf;;EAEA;GACC;;KAGS,cAAc,qBACxB,sBAAsB;;;;;EAKpB;;;UAIa;;EAEf;;EAEA;;EAEA;GACC;;KAGS,cAAc,KAAK,uDAC7B,sBAAsB;;;;;EAKpB;;EAEA;;EAEA;;UAGa;;EAEf,QAAQ;;EAER,OAAO;;EAEP,OAAO;;;KAIG;cCtHC;;;KAKD,uCAAuC;;;;;cAMtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCmCA,iBAAkB,SAAS;;;;;;;;;;UA0Cs7uC;YAA6P,UAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;YAA8Q,SAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;;UAAwH;YAA6P,UAAC,MAAA,8BAAA,QAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;YAAuR,SAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;;UAAwH;YAAwK,UAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;YAA0M,SAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;;;;UAAmL;YAAmQ,UAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;YAA0R,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;UAAuK;YAAgQ,UAAC,SAAA,8BAAA,WAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;;YAAmS,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;UAA8H;YAA8K,UAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;YAAmN,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;;;UAAyL;YAAmQ,UAAC,+BAAA,SAAA,+BAAA,kCAAA;;;;;;;;;;;;;;;;;YAAgQ,SAAC,+BAAA,SAAA,+BAAA,kCAAA;;UAA6I;YAA+P,UAAC,SAAA,8BAAA,WAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;;;;;;;YAAmS,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;UAA8H;YAA+K,UAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;YAAoN,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;;;UAAmM;YAA6Q,UAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;YAA8S,SAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;;UAAwI;YAA6Q,UAAC,cAAA,8BAAA,gBAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;YAAuT,SAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;;UAAwI;YAAwL,UAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;YAAkO,SAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;;;;;;KAV519C,uBAAuB,kBAAkB;;;YAIzC,yBAAyB,aAAa;IAC9C;MACE,gBAAgB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as AuthenticationErrorCode, c as CommonProviderOptions, d as SteamOptions, f as SteamProfile, i as AUTHENTICATION_ERROR_CODES, l as EpicOptions, m as XboxProfile, n as authentication, o as AuthenticationOptions, p as XboxOptions, r as AUTHENTICATION_CALLBACK_ERRORS, s as AuthenticationProviderId, t as AuthenticationPlugin, u as EpicProfile } from "./index-B-TXlJI5.js";
|
|
2
|
+
export { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES, type AuthenticationErrorCode, type AuthenticationOptions, AuthenticationPlugin, type AuthenticationProviderId, type CommonProviderOptions, type EpicOptions, type EpicProfile, type SteamOptions, type SteamProfile, type XboxOptions, type XboxProfile, authentication };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import { n as AUTHENTICATION_ERROR_CODES, t as AUTHENTICATION_CALLBACK_ERRORS } from "./error-codes-CVA2nmg9.js";
|
|
2
|
+
import { getCurrentAdapter, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
3
|
+
import { APIError, createAuthMiddleware } from "better-auth/api";
|
|
4
|
+
import { createAuthorizationURL, refreshAccessToken, validateAuthorizationCode, validateToken } from "@better-auth/core/oauth2";
|
|
5
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
6
|
+
// package.json
|
|
7
|
+
var version = "0.1.0";
|
|
8
|
+
// src/utils/email.ts
|
|
9
|
+
/** Hostname labels: letters, digits, hyphens; at least two labels. */
|
|
10
|
+
const DOMAIN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,63}$/;
|
|
11
|
+
function isValidEmailDomain(domain) {
|
|
12
|
+
return DOMAIN.test(domain);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A placeholder email for a user the provider gives no email:
|
|
16
|
+
* `<account id>@<domain>`. Unique per account, and on a domain that can't
|
|
17
|
+
* receive email (`.invalid` by default).
|
|
18
|
+
*/
|
|
19
|
+
function placeholderEmail(accountId, domain) {
|
|
20
|
+
return `${accountId.toLowerCase()}@${domain}`;
|
|
21
|
+
}
|
|
22
|
+
function hasDomain(email, domain) {
|
|
23
|
+
return email.toLowerCase().endsWith(`@${domain}`);
|
|
24
|
+
}
|
|
25
|
+
// src/hooks/reserved-email.ts
|
|
26
|
+
/**
|
|
27
|
+
* Placeholder emails (`<account id>@steam.invalid`) belong to the provider
|
|
28
|
+
* they came from:
|
|
29
|
+
*
|
|
30
|
+
* - If anyone could sign up with one, they could take a player's
|
|
31
|
+
* placeholder first and block (or, with careless account linking, take
|
|
32
|
+
* over) that player's sign-in. So users with these domains can only be
|
|
33
|
+
* created or changed by that provider's own callback, or by server code
|
|
34
|
+
* outside a request.
|
|
35
|
+
* - With `overrideUserInfoOnSignIn`, better-auth writes the provider's
|
|
36
|
+
* email back on every sign-in. For a user with a real email (who linked
|
|
37
|
+
* the provider account, or changed their email later) that would replace
|
|
38
|
+
* it with the placeholder; the email change is dropped instead.
|
|
39
|
+
*/
|
|
40
|
+
function createReservedEmailHooks(definitions) {
|
|
41
|
+
/** The provider whose placeholder `email` is, if the request is that provider's callback; throws otherwise. */
|
|
42
|
+
const ownerOf = (email, context) => {
|
|
43
|
+
if (typeof email !== "string" || !context) return null;
|
|
44
|
+
const owner = definitions.find((definition) => hasDomain(email, definition.emailDomain));
|
|
45
|
+
if (!owner) return null;
|
|
46
|
+
const params = context.params;
|
|
47
|
+
if (context.path !== "/callback/:id" || params?.id !== owner.provider.id) throw APIError.from("BAD_REQUEST", AUTHENTICATION_ERROR_CODES.EMAIL_DOMAIN_RESERVED);
|
|
48
|
+
return owner;
|
|
49
|
+
};
|
|
50
|
+
return { user: {
|
|
51
|
+
create: { before: async (user, context) => {
|
|
52
|
+
ownerOf(user.email, context);
|
|
53
|
+
} },
|
|
54
|
+
update: { before: async (data, context) => {
|
|
55
|
+
if (!ownerOf(data.email, context) || !context) return;
|
|
56
|
+
if (!await (await getCurrentAdapter(context.context.adapter)).findOne({
|
|
57
|
+
model: "user",
|
|
58
|
+
where: [{
|
|
59
|
+
field: "email",
|
|
60
|
+
value: data.email.toLowerCase()
|
|
61
|
+
}]
|
|
62
|
+
})) return { data: {
|
|
63
|
+
email: void 0,
|
|
64
|
+
emailVerified: void 0
|
|
65
|
+
} };
|
|
66
|
+
} }
|
|
67
|
+
} };
|
|
68
|
+
}
|
|
69
|
+
// ../plugin-utils/src/options.ts
|
|
70
|
+
/**
|
|
71
|
+
* Returns a function that throws an option error prefixed with the plugin
|
|
72
|
+
* id, `[referral] "codeLength" must be ...`, for validating options at
|
|
73
|
+
* plugin creation.
|
|
74
|
+
*/
|
|
75
|
+
function createFail(pluginId) {
|
|
76
|
+
return (message) => {
|
|
77
|
+
throw new Error(`[${pluginId}] ${message}`);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// src/providers/epic.ts
|
|
81
|
+
/**
|
|
82
|
+
* From Epic's discovery document,
|
|
83
|
+
* https://api.epicgames.dev/epic/oauth/v1/.well-known/openid-configuration
|
|
84
|
+
*/
|
|
85
|
+
const EPIC_ISSUER = "https://api.epicgames.dev/epic/oauth/v1";
|
|
86
|
+
const EPIC_AUTHORIZATION_ENDPOINT = "https://www.epicgames.com/id/authorize";
|
|
87
|
+
const EPIC_TOKEN_ENDPOINT = "https://api.epicgames.dev/epic/oauth/v1/token";
|
|
88
|
+
const EPIC_JWKS_ENDPOINT = "https://api.epicgames.dev/epic/oauth/v1/.well-known/jwks.json";
|
|
89
|
+
/** `basic_profile` is required by Epic Account Services; `openid` returns an ID token. */
|
|
90
|
+
const DEFAULT_SCOPES = ["basic_profile", "openid"];
|
|
91
|
+
/**
|
|
92
|
+
* Epic Games (Epic Account Services) over OpenID Connect. The player is
|
|
93
|
+
* identified by the ID token, verified against Epic's keys, issuer, and
|
|
94
|
+
* this client id. Epic shares no email address.
|
|
95
|
+
*/
|
|
96
|
+
function epic(options, emailDomain) {
|
|
97
|
+
return {
|
|
98
|
+
provider: {
|
|
99
|
+
id: "epic",
|
|
100
|
+
name: "Epic Games",
|
|
101
|
+
issuer: EPIC_ISSUER,
|
|
102
|
+
accountSubject: ({ profile }) => profile.sub,
|
|
103
|
+
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI, additionalParams }) {
|
|
104
|
+
const allScopes = options.disableDefaultScope ? [] : [...DEFAULT_SCOPES];
|
|
105
|
+
if (options.scope) allScopes.push(...options.scope);
|
|
106
|
+
if (scopes) allScopes.push(...scopes);
|
|
107
|
+
return createAuthorizationURL({
|
|
108
|
+
id: "epic",
|
|
109
|
+
options,
|
|
110
|
+
authorizationEndpoint: EPIC_AUTHORIZATION_ENDPOINT,
|
|
111
|
+
scopes: [...new Set(allScopes)],
|
|
112
|
+
state,
|
|
113
|
+
codeVerifier: options.pkce ? codeVerifier : void 0,
|
|
114
|
+
redirectURI,
|
|
115
|
+
prompt: options.prompt,
|
|
116
|
+
additionalParams
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
validateAuthorizationCode({ code, codeVerifier, redirectURI }) {
|
|
120
|
+
return validateAuthorizationCode({
|
|
121
|
+
code,
|
|
122
|
+
codeVerifier: options.pkce ? codeVerifier : void 0,
|
|
123
|
+
redirectURI,
|
|
124
|
+
options,
|
|
125
|
+
tokenEndpoint: EPIC_TOKEN_ENDPOINT,
|
|
126
|
+
authentication: "basic"
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
refreshAccessToken: (refreshToken) => refreshAccessToken({
|
|
130
|
+
refreshToken,
|
|
131
|
+
options: {
|
|
132
|
+
clientId: options.clientId,
|
|
133
|
+
clientSecret: options.clientSecret
|
|
134
|
+
},
|
|
135
|
+
tokenEndpoint: EPIC_TOKEN_ENDPOINT,
|
|
136
|
+
authentication: "basic"
|
|
137
|
+
}),
|
|
138
|
+
async getUserInfo(tokens) {
|
|
139
|
+
if (!tokens.idToken) {
|
|
140
|
+
tryGetCurrentAuthEndpointContext()?.context.logger.error("[authentication] Epic returned no ID token; keep the \"openid\" scope");
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
let payload;
|
|
144
|
+
try {
|
|
145
|
+
({payload} = await validateToken(tokens.idToken, EPIC_JWKS_ENDPOINT, {
|
|
146
|
+
issuer: EPIC_ISSUER,
|
|
147
|
+
audience: options.clientId,
|
|
148
|
+
requiredClaims: ["sub"]
|
|
149
|
+
}));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
tryGetCurrentAuthEndpointContext()?.context.logger.error("[authentication] Epic's ID token didn't verify", error);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const mapped = await options.mapProfileToUser?.(payload);
|
|
155
|
+
return {
|
|
156
|
+
user: {
|
|
157
|
+
name: payload.dn || `Epic ${payload.sub}`,
|
|
158
|
+
email: placeholderEmail(payload.sub, emailDomain),
|
|
159
|
+
emailVerified: false,
|
|
160
|
+
...mapped
|
|
161
|
+
},
|
|
162
|
+
data: payload
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
disableSignUp: options.disableSignUp,
|
|
166
|
+
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
167
|
+
options: {
|
|
168
|
+
clientId: options.clientId,
|
|
169
|
+
clientSecret: options.clientSecret,
|
|
170
|
+
redirectURI: options.redirectURI,
|
|
171
|
+
disableSignUp: options.disableSignUp,
|
|
172
|
+
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
173
|
+
overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
emailDomain
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// src/providers/steam.ts
|
|
180
|
+
const STEAM_OPENID_ENDPOINT = "https://steamcommunity.com/openid/login";
|
|
181
|
+
const OPENID_NS = "http://specs.openid.net/auth/2.0";
|
|
182
|
+
const IDENTIFIER_SELECT = "http://specs.openid.net/auth/2.0/identifier_select";
|
|
183
|
+
const CLAIMED_ID = /^https:\/\/steamcommunity\.com\/openid\/id\/(\d{17})$/;
|
|
184
|
+
/** OpenID 2.0 requires these to be signed in a positive assertion. */
|
|
185
|
+
const REQUIRED_SIGNED = [
|
|
186
|
+
"op_endpoint",
|
|
187
|
+
"return_to",
|
|
188
|
+
"response_nonce",
|
|
189
|
+
"assoc_handle",
|
|
190
|
+
"claimed_id",
|
|
191
|
+
"identity"
|
|
192
|
+
];
|
|
193
|
+
/** How long the one-time code from the callback hook is valid. */
|
|
194
|
+
const CODE_TTL_MS = 6e4;
|
|
195
|
+
const STEAM_TIMEOUT_MS = 1e4;
|
|
196
|
+
const codeIdentifier = (code) => `steam-openid:${code}`;
|
|
197
|
+
/** Where Steam sends the player back: better-auth's own callback, with the OAuth state. */
|
|
198
|
+
function steamReturnTo(redirectURI, state) {
|
|
199
|
+
const url = new URL(redirectURI);
|
|
200
|
+
url.searchParams.set("state", state);
|
|
201
|
+
return url.toString();
|
|
202
|
+
}
|
|
203
|
+
/** Steam's sign-in page for an OpenID 2.0 `checkid_setup` request. */
|
|
204
|
+
function steamAuthorizationURL(returnTo) {
|
|
205
|
+
const url = new URL(STEAM_OPENID_ENDPOINT);
|
|
206
|
+
url.searchParams.set("openid.ns", OPENID_NS);
|
|
207
|
+
url.searchParams.set("openid.mode", "checkid_setup");
|
|
208
|
+
url.searchParams.set("openid.return_to", returnTo);
|
|
209
|
+
url.searchParams.set("openid.realm", new URL(returnTo).origin);
|
|
210
|
+
url.searchParams.set("openid.identity", IDENTIFIER_SELECT);
|
|
211
|
+
url.searchParams.set("openid.claimed_id", IDENTIFIER_SELECT);
|
|
212
|
+
return url;
|
|
213
|
+
}
|
|
214
|
+
/** Parses OpenID's key-value form (`key:value` lines). */
|
|
215
|
+
function parseKeyValue(text) {
|
|
216
|
+
const result = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const line of text.split("\n")) {
|
|
218
|
+
const separator = line.indexOf(":");
|
|
219
|
+
if (separator > 0) result.set(line.slice(0, separator), line.slice(separator + 1).trim());
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Checks Steam's positive assertion and returns the SteamID64, or `null`.
|
|
225
|
+
* Everything in the query is untrusted until Steam confirms it: the
|
|
226
|
+
* fields are checked against what this request expects, then sent back to
|
|
227
|
+
* Steam (`check_authentication`), which verifies the signature and that
|
|
228
|
+
* the nonce wasn't used before.
|
|
229
|
+
*/
|
|
230
|
+
async function verifySteamAssertion(query, expectedReturnTo) {
|
|
231
|
+
const get = (key) => {
|
|
232
|
+
const value = query[`openid.${key}`];
|
|
233
|
+
return typeof value === "string" ? value : void 0;
|
|
234
|
+
};
|
|
235
|
+
if (get("ns") !== OPENID_NS || get("mode") !== "id_res") return null;
|
|
236
|
+
if (get("op_endpoint") !== "https://steamcommunity.com/openid/login") return null;
|
|
237
|
+
if (get("return_to") !== expectedReturnTo) return null;
|
|
238
|
+
const claimedId = get("claimed_id");
|
|
239
|
+
const steamId = claimedId ? CLAIMED_ID.exec(claimedId)?.[1] : void 0;
|
|
240
|
+
if (!steamId || get("identity") !== claimedId) return null;
|
|
241
|
+
const signed = new Set((get("signed") ?? "").split(","));
|
|
242
|
+
if (!REQUIRED_SIGNED.every((field) => signed.has(field)) || !get("sig")) return null;
|
|
243
|
+
const body = new URLSearchParams();
|
|
244
|
+
for (const [key, value] of Object.entries(query)) if (key.startsWith("openid.") && typeof value === "string") body.set(key, value);
|
|
245
|
+
body.set("openid.mode", "check_authentication");
|
|
246
|
+
const response = await fetch(STEAM_OPENID_ENDPOINT, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
body,
|
|
249
|
+
redirect: "error",
|
|
250
|
+
signal: AbortSignal.timeout(STEAM_TIMEOUT_MS)
|
|
251
|
+
});
|
|
252
|
+
if (!response.ok) return null;
|
|
253
|
+
const answer = parseKeyValue(await response.text());
|
|
254
|
+
return answer.get("ns") === OPENID_NS && answer.get("is_valid") === "true" ? steamId : null;
|
|
255
|
+
}
|
|
256
|
+
/** Loads the player's public profile; `null` if it can't be loaded. */
|
|
257
|
+
async function fetchSteamProfile(steamId, apiKey, apiBaseURL) {
|
|
258
|
+
const url = new URL("/ISteamUser/GetPlayerSummaries/v2/", apiBaseURL);
|
|
259
|
+
url.searchParams.set("key", apiKey);
|
|
260
|
+
url.searchParams.set("steamids", steamId);
|
|
261
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(STEAM_TIMEOUT_MS) });
|
|
262
|
+
if (!response.ok) return null;
|
|
263
|
+
return (await response.json()).response?.players?.find((p) => p.steamid === steamId) ?? null;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Steam uses OpenID 2.0, not OAuth: it sends the player back with a signed
|
|
267
|
+
* assertion instead of a code. Steam's `return_to` is better-auth's own
|
|
268
|
+
* `/callback/steam`; a before-hook there verifies the assertion and swaps
|
|
269
|
+
* it for a one-time `code`, which `validateAuthorizationCode` redeems. The
|
|
270
|
+
* rest is better-auth's callback, like for any provider: state and CSRF
|
|
271
|
+
* check, user and account, session, redirect.
|
|
272
|
+
*/
|
|
273
|
+
function steam(options, emailDomain) {
|
|
274
|
+
const apiBaseURL = options.apiBaseURL ?? "https://api.steampowered.com";
|
|
275
|
+
return {
|
|
276
|
+
provider: {
|
|
277
|
+
id: "steam",
|
|
278
|
+
name: "Steam",
|
|
279
|
+
accountSubject: ({ profile }) => profile.steamid,
|
|
280
|
+
createAuthorizationURL({ state, redirectURI }) {
|
|
281
|
+
return steamAuthorizationURL(steamReturnTo(redirectURI, state));
|
|
282
|
+
},
|
|
283
|
+
async validateAuthorizationCode({ code }) {
|
|
284
|
+
const ctx = tryGetCurrentAuthEndpointContext();
|
|
285
|
+
if (!ctx) return null;
|
|
286
|
+
const verified = await ctx.context.internalAdapter.consumeVerificationValue(codeIdentifier(code));
|
|
287
|
+
if (!verified || new Date(verified.expiresAt).getTime() < Date.now()) return null;
|
|
288
|
+
return {
|
|
289
|
+
tokenType: "steam-openid",
|
|
290
|
+
raw: { steamid: verified.value }
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
async getUserInfo(tokens) {
|
|
294
|
+
const steamid = tokens.raw?.steamid;
|
|
295
|
+
if (typeof steamid !== "string") return null;
|
|
296
|
+
let profile = { steamid };
|
|
297
|
+
if (options.apiKey) try {
|
|
298
|
+
profile = await fetchSteamProfile(steamid, options.apiKey, apiBaseURL) ?? profile;
|
|
299
|
+
} catch {
|
|
300
|
+
tryGetCurrentAuthEndpointContext()?.context.logger.warn("[authentication] could not load the Steam profile");
|
|
301
|
+
}
|
|
302
|
+
const mapped = await options.mapProfileToUser?.(profile);
|
|
303
|
+
return {
|
|
304
|
+
user: {
|
|
305
|
+
name: profile.personaname || `Steam ${steamid}`,
|
|
306
|
+
email: placeholderEmail(steamid, emailDomain),
|
|
307
|
+
emailVerified: false,
|
|
308
|
+
image: profile.avatarfull,
|
|
309
|
+
...mapped
|
|
310
|
+
},
|
|
311
|
+
data: profile
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
disableSignUp: options.disableSignUp,
|
|
315
|
+
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
316
|
+
options: {
|
|
317
|
+
disableSignUp: options.disableSignUp,
|
|
318
|
+
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
319
|
+
overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
emailDomain,
|
|
323
|
+
hooks: { before: [{
|
|
324
|
+
matcher: (context) => context.path === "/callback/:id" && context.params?.id === "steam",
|
|
325
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
326
|
+
const query = ctx.query ?? {};
|
|
327
|
+
const state = typeof query.state === "string" ? query.state : void 0;
|
|
328
|
+
const fail = (error) => ({ context: { query: {
|
|
329
|
+
state,
|
|
330
|
+
code: "",
|
|
331
|
+
error
|
|
332
|
+
} } });
|
|
333
|
+
if (!state || ctx.method !== "GET") return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_VERIFICATION_FAILED);
|
|
334
|
+
if (query["openid.mode"] === "cancel") return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_SIGN_IN_CANCELLED);
|
|
335
|
+
let steamId = null;
|
|
336
|
+
try {
|
|
337
|
+
steamId = await verifySteamAssertion(query, steamReturnTo(`${ctx.context.baseURL}/callback/steam`, state));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
ctx.context.logger.error("[authentication] Steam verification failed", error);
|
|
340
|
+
}
|
|
341
|
+
if (!steamId) return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_VERIFICATION_FAILED);
|
|
342
|
+
const code = generateRandomString(32);
|
|
343
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
344
|
+
identifier: codeIdentifier(code),
|
|
345
|
+
value: steamId,
|
|
346
|
+
expiresAt: new Date(Date.now() + CODE_TTL_MS)
|
|
347
|
+
});
|
|
348
|
+
return { context: { query: {
|
|
349
|
+
state,
|
|
350
|
+
code
|
|
351
|
+
} } };
|
|
352
|
+
})
|
|
353
|
+
}] }
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// src/providers/xbox.ts
|
|
357
|
+
/**
|
|
358
|
+
* Microsoft's documented flow for Xbox sign-in on websites ("Xbox services
|
|
359
|
+
* sign-in for title websites", Microsoft Game Development Kit): Microsoft
|
|
360
|
+
* account OAuth on the `consumers` tenant with `xboxlive.signin`, then an
|
|
361
|
+
* Xbox user token, then an XSTS token whose claims name the player.
|
|
362
|
+
*/
|
|
363
|
+
const XBOX_AUTHORIZATION_ENDPOINT = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize";
|
|
364
|
+
const XBOX_TOKEN_ENDPOINT = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
|
365
|
+
const XBOX_USER_AUTH_ENDPOINT = "https://user.auth.xboxlive.com/user/authenticate";
|
|
366
|
+
const XBOX_XSTS_ENDPOINT = "https://xsts.auth.xboxlive.com/xsts/authorize";
|
|
367
|
+
const XBOX_TIMEOUT_MS = 1e4;
|
|
368
|
+
async function postXbox(url, body) {
|
|
369
|
+
const response = await fetch(url, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: {
|
|
372
|
+
"content-type": "application/json",
|
|
373
|
+
accept: "application/json",
|
|
374
|
+
"x-xbl-contract-version": "1"
|
|
375
|
+
},
|
|
376
|
+
body: JSON.stringify(body),
|
|
377
|
+
redirect: "error",
|
|
378
|
+
signal: AbortSignal.timeout(XBOX_TIMEOUT_MS)
|
|
379
|
+
});
|
|
380
|
+
if (!response.ok) {
|
|
381
|
+
const xErr = response.headers.get("x-err") ?? "none";
|
|
382
|
+
tryGetCurrentAuthEndpointContext()?.context.logger.error(`[authentication] Xbox token request to ${new URL(url).host} failed (${response.status}, XErr ${xErr})`);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
return await response.json();
|
|
387
|
+
} catch {
|
|
388
|
+
tryGetCurrentAuthEndpointContext()?.context.logger.error(`[authentication] Xbox token response from ${new URL(url).host} isn't JSON`);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Exchanges a Microsoft access token for the player's XSTS claims (XUID, gamertag). */
|
|
393
|
+
async function fetchXboxProfile(accessToken, { relyingParty, sandboxId }) {
|
|
394
|
+
const user = await postXbox(XBOX_USER_AUTH_ENDPOINT, {
|
|
395
|
+
RelyingParty: "http://auth.xboxlive.com",
|
|
396
|
+
TokenType: "JWT",
|
|
397
|
+
Properties: {
|
|
398
|
+
AuthMethod: "RPS",
|
|
399
|
+
SiteName: "user.auth.xboxlive.com",
|
|
400
|
+
RpsTicket: `d=${accessToken}`
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
if (!user?.Token) return null;
|
|
404
|
+
const xsts = await postXbox(XBOX_XSTS_ENDPOINT, {
|
|
405
|
+
RelyingParty: relyingParty,
|
|
406
|
+
TokenType: "JWT",
|
|
407
|
+
Properties: {
|
|
408
|
+
SandboxId: sandboxId,
|
|
409
|
+
UserTokens: [user.Token]
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
const claims = xsts?.DisplayClaims?.xui?.[0];
|
|
413
|
+
if (!claims?.xid) {
|
|
414
|
+
if (xsts) tryGetCurrentAuthEndpointContext()?.context.logger.error("[authentication] the XSTS token has no XUID (xid claim); check the relying party");
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
return claims;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Xbox, for apps enrolled in the Xbox developer program (Microsoft only
|
|
421
|
+
* grants `xboxlive.signin` to them). The player is identified by the XUID
|
|
422
|
+
* from XSTS; Xbox shares no email address.
|
|
423
|
+
*/
|
|
424
|
+
function xbox(options, emailDomain) {
|
|
425
|
+
const relyingParty = options.relyingParty ?? "http://xboxlive.com";
|
|
426
|
+
const sandboxId = options.sandboxId ?? "RETAIL";
|
|
427
|
+
const scopes = ["xboxlive.signin", ...options.offlineAccess ? ["xboxlive.offline_access"] : []];
|
|
428
|
+
return {
|
|
429
|
+
provider: {
|
|
430
|
+
id: "xbox",
|
|
431
|
+
name: "Xbox",
|
|
432
|
+
accountSubject: ({ profile }) => profile.xid,
|
|
433
|
+
createAuthorizationURL({ state, codeVerifier, redirectURI, additionalParams }) {
|
|
434
|
+
return createAuthorizationURL({
|
|
435
|
+
id: "xbox",
|
|
436
|
+
options,
|
|
437
|
+
authorizationEndpoint: XBOX_AUTHORIZATION_ENDPOINT,
|
|
438
|
+
scopes,
|
|
439
|
+
state,
|
|
440
|
+
codeVerifier,
|
|
441
|
+
redirectURI,
|
|
442
|
+
prompt: options.prompt,
|
|
443
|
+
additionalParams
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
validateAuthorizationCode({ code, codeVerifier, redirectURI }) {
|
|
447
|
+
return validateAuthorizationCode({
|
|
448
|
+
code,
|
|
449
|
+
codeVerifier,
|
|
450
|
+
redirectURI,
|
|
451
|
+
options,
|
|
452
|
+
tokenEndpoint: XBOX_TOKEN_ENDPOINT,
|
|
453
|
+
authentication: "post"
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
refreshAccessToken: (refreshToken) => refreshAccessToken({
|
|
457
|
+
refreshToken,
|
|
458
|
+
options: {
|
|
459
|
+
clientId: options.clientId,
|
|
460
|
+
clientSecret: options.clientSecret
|
|
461
|
+
},
|
|
462
|
+
tokenEndpoint: XBOX_TOKEN_ENDPOINT,
|
|
463
|
+
authentication: "post"
|
|
464
|
+
}),
|
|
465
|
+
async getUserInfo(tokens) {
|
|
466
|
+
if (!tokens.accessToken) return null;
|
|
467
|
+
let profile = null;
|
|
468
|
+
try {
|
|
469
|
+
profile = await fetchXboxProfile(tokens.accessToken, {
|
|
470
|
+
relyingParty,
|
|
471
|
+
sandboxId
|
|
472
|
+
});
|
|
473
|
+
} catch (error) {
|
|
474
|
+
tryGetCurrentAuthEndpointContext()?.context.logger.error("[authentication] Xbox Live couldn't be reached", error);
|
|
475
|
+
}
|
|
476
|
+
if (!profile) return null;
|
|
477
|
+
const mapped = await options.mapProfileToUser?.(profile);
|
|
478
|
+
return {
|
|
479
|
+
user: {
|
|
480
|
+
name: profile.gtg || `Xbox ${profile.xid}`,
|
|
481
|
+
email: placeholderEmail(profile.xid, emailDomain),
|
|
482
|
+
emailVerified: false,
|
|
483
|
+
...mapped
|
|
484
|
+
},
|
|
485
|
+
data: profile
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
disableSignUp: options.disableSignUp,
|
|
489
|
+
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
490
|
+
options: {
|
|
491
|
+
clientId: options.clientId,
|
|
492
|
+
clientSecret: options.clientSecret,
|
|
493
|
+
redirectURI: options.redirectURI,
|
|
494
|
+
disableSignUp: options.disableSignUp,
|
|
495
|
+
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
496
|
+
overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
emailDomain
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// src/options.ts
|
|
503
|
+
const fail = createFail("authentication");
|
|
504
|
+
function emailDomain(providerId, domain) {
|
|
505
|
+
const resolved = (domain ?? `${providerId}.invalid`).toLowerCase();
|
|
506
|
+
if (!isValidEmailDomain(resolved)) fail(`"${providerId}.emailDomain" must be a domain name, got ${JSON.stringify(domain)}.`);
|
|
507
|
+
return resolved;
|
|
508
|
+
}
|
|
509
|
+
function requireClient(providerId, options) {
|
|
510
|
+
if (typeof options.clientId !== "string" || options.clientId === "") fail(`"${providerId}.clientId" is required.`);
|
|
511
|
+
if (typeof options.clientSecret !== "string" || options.clientSecret === "") fail(`"${providerId}.clientSecret" is required.`);
|
|
512
|
+
}
|
|
513
|
+
/** Validates the options once, at creation, and builds the providers. */
|
|
514
|
+
function resolveProviders(options) {
|
|
515
|
+
const definitions = [];
|
|
516
|
+
if (options.steam) {
|
|
517
|
+
if (options.steam.apiKey !== void 0 && options.steam.apiKey.trim() === "") fail("\"steam.apiKey\" is empty; leave it out to sign in without loading profiles.");
|
|
518
|
+
definitions.push(steam(options.steam, emailDomain("steam", options.steam.emailDomain)));
|
|
519
|
+
}
|
|
520
|
+
if (options.epic) {
|
|
521
|
+
requireClient("epic", options.epic);
|
|
522
|
+
definitions.push(epic(options.epic, emailDomain("epic", options.epic.emailDomain)));
|
|
523
|
+
}
|
|
524
|
+
if (options.xbox) {
|
|
525
|
+
requireClient("xbox", options.xbox);
|
|
526
|
+
definitions.push(xbox(options.xbox, emailDomain("xbox", options.xbox.emailDomain)));
|
|
527
|
+
}
|
|
528
|
+
if (definitions.length === 0) fail("Configure at least one provider: steam, epic, or xbox.");
|
|
529
|
+
const domains = definitions.map((definition) => definition.emailDomain);
|
|
530
|
+
if (new Set(domains).size !== domains.length) fail("Each provider needs an email domain of its own.");
|
|
531
|
+
return definitions;
|
|
532
|
+
}
|
|
533
|
+
// src/index.ts
|
|
534
|
+
/**
|
|
535
|
+
* Adds Steam, Epic Games, and Xbox as social providers. They are used like
|
|
536
|
+
* better-auth's built-in ones: `signIn.social({ provider: "steam" })`,
|
|
537
|
+
* `linkSocial`, callbacks at `/callback/<provider>`, account linking,
|
|
538
|
+
* `callbackURL` checks, and sign-up options all work the same way.
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```ts
|
|
542
|
+
* import { betterAuth } from "better-auth";
|
|
543
|
+
* import { authentication } from "@checodo/better-auth-authentication";
|
|
544
|
+
*
|
|
545
|
+
* export const auth = betterAuth({
|
|
546
|
+
* plugins: [
|
|
547
|
+
* authentication({
|
|
548
|
+
* steam: { apiKey: process.env.STEAM_API_KEY },
|
|
549
|
+
* epic: { clientId: "...", clientSecret: "..." },
|
|
550
|
+
* }),
|
|
551
|
+
* ],
|
|
552
|
+
* });
|
|
553
|
+
*
|
|
554
|
+
* // client
|
|
555
|
+
* await authClient.signIn.social({ provider: "steam", callbackURL: "/dashboard" });
|
|
556
|
+
* ```
|
|
557
|
+
*/
|
|
558
|
+
const authentication = (options) => {
|
|
559
|
+
const definitions = resolveProviders(options);
|
|
560
|
+
return {
|
|
561
|
+
id: "authentication",
|
|
562
|
+
version,
|
|
563
|
+
init(ctx) {
|
|
564
|
+
const ids = new Set(definitions.map((definition) => definition.provider.id));
|
|
565
|
+
for (const existing of ctx.socialProviders) if (ids.has(existing.id)) ctx.logger.warn(`[authentication] the "${existing.id}" provider replaces a social provider with the same id`);
|
|
566
|
+
return {
|
|
567
|
+
context: { socialProviders: [...definitions.map((definition) => definition.provider), ...ctx.socialProviders.filter((provider) => !ids.has(provider.id))] },
|
|
568
|
+
options: { databaseHooks: createReservedEmailHooks(definitions) }
|
|
569
|
+
};
|
|
570
|
+
},
|
|
571
|
+
hooks: { before: definitions.flatMap((definition) => definition.hooks?.before ?? []) },
|
|
572
|
+
$ERROR_CODES: AUTHENTICATION_ERROR_CODES,
|
|
573
|
+
options
|
|
574
|
+
};
|
|
575
|
+
};
|
|
576
|
+
export { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES, authentication };
|
|
577
|
+
|
|
578
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../package.json","../src/utils/email.ts","../src/hooks/reserved-email.ts","../../plugin-utils/src/options.ts","../src/providers/epic.ts","../src/providers/steam.ts","../src/providers/xbox.ts","../src/options.ts","../src/index.ts"],"sourcesContent":["","/** Hostname labels: letters, digits, hyphens; at least two labels. */\nconst DOMAIN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9-]{2,63}$/;\n\nexport function isValidEmailDomain(domain: string): boolean {\n return DOMAIN.test(domain);\n}\n\n/**\n * A placeholder email for a user the provider gives no email:\n * `<account id>@<domain>`. Unique per account, and on a domain that can't\n * receive email (`.invalid` by default).\n */\nexport function placeholderEmail(accountId: string, domain: string): string {\n return `${accountId.toLowerCase()}@${domain}`;\n}\n\nexport function hasDomain(email: string, domain: string): boolean {\n return email.toLowerCase().endsWith(`@${domain}`);\n}\n","import type { BetterAuthOptions, GenericEndpointContext } from \"@better-auth/core\";\nimport { getCurrentAdapter } from \"@better-auth/core/context\";\nimport { APIError } from \"better-auth/api\";\nimport { AUTHENTICATION_ERROR_CODES } from \"../error-codes\";\nimport type { ProviderDefinition } from \"../providers/types\";\nimport { hasDomain } from \"../utils/email\";\n\ntype Context = GenericEndpointContext | null | undefined;\n\n/**\n * Placeholder emails (`<account id>@steam.invalid`) belong to the provider\n * they came from:\n *\n * - If anyone could sign up with one, they could take a player's\n * placeholder first and block (or, with careless account linking, take\n * over) that player's sign-in. So users with these domains can only be\n * created or changed by that provider's own callback, or by server code\n * outside a request.\n * - With `overrideUserInfoOnSignIn`, better-auth writes the provider's\n * email back on every sign-in. For a user with a real email (who linked\n * the provider account, or changed their email later) that would replace\n * it with the placeholder; the email change is dropped instead.\n */\nexport function createReservedEmailHooks(\n definitions: ProviderDefinition[],\n): BetterAuthOptions[\"databaseHooks\"] {\n /** The provider whose placeholder `email` is, if the request is that provider's callback; throws otherwise. */\n const ownerOf = (email: unknown, context: Context): ProviderDefinition | null => {\n if (typeof email !== \"string\" || !context) return null;\n const owner = definitions.find((definition) => hasDomain(email, definition.emailDomain));\n if (!owner) return null;\n const params = context.params as { id?: unknown } | undefined;\n if (context.path !== \"/callback/:id\" || params?.id !== owner.provider.id) {\n throw APIError.from(\"BAD_REQUEST\", AUTHENTICATION_ERROR_CODES.EMAIL_DOMAIN_RESERVED);\n }\n return owner;\n };\n\n return {\n user: {\n create: {\n before: async (user, context) => {\n ownerOf(user.email, context);\n },\n },\n update: {\n before: async (data, context) => {\n if (!ownerOf(data.email, context) || !context) return;\n // Emails are unique: if no user has the placeholder, this update\n // would replace another email with it.\n const adapter = await getCurrentAdapter(context.context.adapter);\n const holder = await adapter.findOne({\n model: \"user\",\n where: [{ field: \"email\", value: (data.email as string).toLowerCase() }],\n });\n if (!holder) return { data: { email: undefined, emailVerified: undefined } };\n },\n },\n },\n };\n}\n","/**\n * Returns a function that throws an option error prefixed with the plugin\n * id, `[referral] \"codeLength\" must be ...`, for validating options at\n * plugin creation.\n */\nexport function createFail(pluginId: string): (message: string) => never {\n return (message) => {\n throw new Error(`[${pluginId}] ${message}`);\n };\n}\n","import { tryGetCurrentAuthEndpointContext } from \"@better-auth/core/context\";\nimport type { OAuthProvider } from \"@better-auth/core/oauth2\";\nimport {\n createAuthorizationURL,\n refreshAccessToken,\n validateAuthorizationCode,\n validateToken,\n} from \"@better-auth/core/oauth2\";\nimport type { EpicOptions, EpicProfile } from \"../types\";\nimport { placeholderEmail } from \"../utils/email\";\nimport type { ProviderDefinition } from \"./types\";\n\n/**\n * From Epic's discovery document,\n * https://api.epicgames.dev/epic/oauth/v1/.well-known/openid-configuration\n */\nexport const EPIC_ISSUER = \"https://api.epicgames.dev/epic/oauth/v1\";\nexport const EPIC_AUTHORIZATION_ENDPOINT = \"https://www.epicgames.com/id/authorize\";\nexport const EPIC_TOKEN_ENDPOINT = \"https://api.epicgames.dev/epic/oauth/v1/token\";\nexport const EPIC_JWKS_ENDPOINT = \"https://api.epicgames.dev/epic/oauth/v1/.well-known/jwks.json\";\n/** `basic_profile` is required by Epic Account Services; `openid` returns an ID token. */\nconst DEFAULT_SCOPES = [\"basic_profile\", \"openid\"];\n\n/**\n * Epic Games (Epic Account Services) over OpenID Connect. The player is\n * identified by the ID token, verified against Epic's keys, issuer, and\n * this client id. Epic shares no email address.\n */\nexport function epic(options: EpicOptions, emailDomain: string): ProviderDefinition {\n const provider: OAuthProvider<EpicProfile> = {\n id: \"epic\",\n name: \"Epic Games\",\n issuer: EPIC_ISSUER,\n accountSubject: ({ profile }) => profile.sub,\n createAuthorizationURL({ state, scopes, codeVerifier, redirectURI, additionalParams }) {\n const allScopes = options.disableDefaultScope ? [] : [...DEFAULT_SCOPES];\n if (options.scope) allScopes.push(...options.scope);\n if (scopes) allScopes.push(...scopes);\n return createAuthorizationURL({\n id: \"epic\",\n options,\n authorizationEndpoint: EPIC_AUTHORIZATION_ENDPOINT,\n scopes: [...new Set(allScopes)],\n state,\n codeVerifier: options.pkce ? codeVerifier : undefined,\n redirectURI,\n prompt: options.prompt,\n additionalParams,\n });\n },\n validateAuthorizationCode({ code, codeVerifier, redirectURI }) {\n return validateAuthorizationCode({\n code,\n codeVerifier: options.pkce ? codeVerifier : undefined,\n redirectURI,\n options,\n tokenEndpoint: EPIC_TOKEN_ENDPOINT,\n authentication: \"basic\",\n });\n },\n refreshAccessToken: (refreshToken) =>\n refreshAccessToken({\n refreshToken,\n options: { clientId: options.clientId, clientSecret: options.clientSecret },\n tokenEndpoint: EPIC_TOKEN_ENDPOINT,\n authentication: \"basic\",\n }),\n async getUserInfo(tokens) {\n if (!tokens.idToken) {\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n '[authentication] Epic returned no ID token; keep the \"openid\" scope',\n );\n return null;\n }\n let payload: EpicProfile;\n try {\n ({ payload } = await validateToken<EpicProfile>(tokens.idToken, EPIC_JWKS_ENDPOINT, {\n issuer: EPIC_ISSUER,\n audience: options.clientId,\n requiredClaims: [\"sub\"],\n }));\n } catch (error) {\n // `null` fails the sign-in with better-auth's error redirect, not a 500.\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n \"[authentication] Epic's ID token didn't verify\",\n error,\n );\n return null;\n }\n const mapped = await options.mapProfileToUser?.(payload);\n return {\n user: {\n name: payload.dn || `Epic ${payload.sub}`,\n email: placeholderEmail(payload.sub, emailDomain),\n emailVerified: false,\n ...mapped,\n },\n data: payload,\n };\n },\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n options: {\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n redirectURI: options.redirectURI,\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn,\n },\n };\n return { provider: provider as unknown as OAuthProvider, emailDomain };\n}\n","import type { HookEndpointContext } from \"@better-auth/core\";\nimport { tryGetCurrentAuthEndpointContext } from \"@better-auth/core/context\";\nimport type { OAuth2Tokens, OAuthProvider } from \"@better-auth/core/oauth2\";\nimport { createAuthMiddleware } from \"better-auth/api\";\nimport { generateRandomString } from \"better-auth/crypto\";\nimport { AUTHENTICATION_CALLBACK_ERRORS } from \"../error-codes\";\nimport type { SteamOptions, SteamProfile } from \"../types\";\nimport { placeholderEmail } from \"../utils/email\";\nimport type { ProviderDefinition } from \"./types\";\n\nexport const STEAM_OPENID_ENDPOINT = \"https://steamcommunity.com/openid/login\";\nconst OPENID_NS = \"http://specs.openid.net/auth/2.0\";\nconst IDENTIFIER_SELECT = \"http://specs.openid.net/auth/2.0/identifier_select\";\nconst CLAIMED_ID = /^https:\\/\\/steamcommunity\\.com\\/openid\\/id\\/(\\d{17})$/;\n/** OpenID 2.0 requires these to be signed in a positive assertion. */\nconst REQUIRED_SIGNED = [\n \"op_endpoint\",\n \"return_to\",\n \"response_nonce\",\n \"assoc_handle\",\n \"claimed_id\",\n \"identity\",\n];\n/** How long the one-time code from the callback hook is valid. */\nconst CODE_TTL_MS = 60_000;\nconst STEAM_TIMEOUT_MS = 10_000;\n\nconst codeIdentifier = (code: string) => `steam-openid:${code}`;\n\n/** Where Steam sends the player back: better-auth's own callback, with the OAuth state. */\nexport function steamReturnTo(redirectURI: string, state: string): string {\n const url = new URL(redirectURI);\n url.searchParams.set(\"state\", state);\n return url.toString();\n}\n\n/** Steam's sign-in page for an OpenID 2.0 `checkid_setup` request. */\nexport function steamAuthorizationURL(returnTo: string): URL {\n const url = new URL(STEAM_OPENID_ENDPOINT);\n url.searchParams.set(\"openid.ns\", OPENID_NS);\n url.searchParams.set(\"openid.mode\", \"checkid_setup\");\n url.searchParams.set(\"openid.return_to\", returnTo);\n url.searchParams.set(\"openid.realm\", new URL(returnTo).origin);\n url.searchParams.set(\"openid.identity\", IDENTIFIER_SELECT);\n url.searchParams.set(\"openid.claimed_id\", IDENTIFIER_SELECT);\n return url;\n}\n\n/** Parses OpenID's key-value form (`key:value` lines). */\nfunction parseKeyValue(text: string): Map<string, string> {\n const result = new Map<string, string>();\n for (const line of text.split(\"\\n\")) {\n const separator = line.indexOf(\":\");\n if (separator > 0) result.set(line.slice(0, separator), line.slice(separator + 1).trim());\n }\n return result;\n}\n\n/**\n * Checks Steam's positive assertion and returns the SteamID64, or `null`.\n * Everything in the query is untrusted until Steam confirms it: the\n * fields are checked against what this request expects, then sent back to\n * Steam (`check_authentication`), which verifies the signature and that\n * the nonce wasn't used before.\n */\nexport async function verifySteamAssertion(\n query: Record<string, unknown>,\n expectedReturnTo: string,\n): Promise<string | null> {\n const get = (key: string) => {\n const value = query[`openid.${key}`];\n return typeof value === \"string\" ? value : undefined;\n };\n if (get(\"ns\") !== OPENID_NS || get(\"mode\") !== \"id_res\") return null;\n if (get(\"op_endpoint\") !== STEAM_OPENID_ENDPOINT) return null;\n if (get(\"return_to\") !== expectedReturnTo) return null;\n const claimedId = get(\"claimed_id\");\n const steamId = claimedId ? CLAIMED_ID.exec(claimedId)?.[1] : undefined;\n if (!steamId || get(\"identity\") !== claimedId) return null;\n const signed = new Set((get(\"signed\") ?? \"\").split(\",\"));\n if (!REQUIRED_SIGNED.every((field) => signed.has(field)) || !get(\"sig\")) return null;\n\n const body = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (key.startsWith(\"openid.\") && typeof value === \"string\") body.set(key, value);\n }\n body.set(\"openid.mode\", \"check_authentication\");\n const response = await fetch(STEAM_OPENID_ENDPOINT, {\n method: \"POST\",\n body,\n redirect: \"error\",\n signal: AbortSignal.timeout(STEAM_TIMEOUT_MS),\n });\n if (!response.ok) return null;\n const answer = parseKeyValue(await response.text());\n return answer.get(\"ns\") === OPENID_NS && answer.get(\"is_valid\") === \"true\" ? steamId : null;\n}\n\n/** Loads the player's public profile; `null` if it can't be loaded. */\nasync function fetchSteamProfile(\n steamId: string,\n apiKey: string,\n apiBaseURL: string,\n): Promise<SteamProfile | null> {\n const url = new URL(\"/ISteamUser/GetPlayerSummaries/v2/\", apiBaseURL);\n url.searchParams.set(\"key\", apiKey);\n url.searchParams.set(\"steamids\", steamId);\n const response = await fetch(url, { signal: AbortSignal.timeout(STEAM_TIMEOUT_MS) });\n if (!response.ok) return null;\n const data = (await response.json()) as { response?: { players?: SteamProfile[] } };\n const player = data.response?.players?.find((p) => p.steamid === steamId);\n return player ?? null;\n}\n\n/**\n * Steam uses OpenID 2.0, not OAuth: it sends the player back with a signed\n * assertion instead of a code. Steam's `return_to` is better-auth's own\n * `/callback/steam`; a before-hook there verifies the assertion and swaps\n * it for a one-time `code`, which `validateAuthorizationCode` redeems. The\n * rest is better-auth's callback, like for any provider: state and CSRF\n * check, user and account, session, redirect.\n */\nexport function steam(options: SteamOptions, emailDomain: string): ProviderDefinition {\n const apiBaseURL = options.apiBaseURL ?? \"https://api.steampowered.com\";\n\n const provider: OAuthProvider<SteamProfile> = {\n id: \"steam\",\n name: \"Steam\",\n accountSubject: ({ profile }) => profile.steamid,\n createAuthorizationURL({ state, redirectURI }) {\n return steamAuthorizationURL(steamReturnTo(redirectURI, state));\n },\n async validateAuthorizationCode({ code }): Promise<OAuth2Tokens | null> {\n const ctx = tryGetCurrentAuthEndpointContext();\n if (!ctx) return null;\n const verified = await ctx.context.internalAdapter.consumeVerificationValue(\n codeIdentifier(code),\n );\n if (!verified || new Date(verified.expiresAt).getTime() < Date.now()) return null;\n return { tokenType: \"steam-openid\", raw: { steamid: verified.value } };\n },\n async getUserInfo(tokens) {\n const steamid = tokens.raw?.steamid;\n if (typeof steamid !== \"string\") return null;\n let profile: SteamProfile = { steamid };\n if (options.apiKey) {\n try {\n profile = (await fetchSteamProfile(steamid, options.apiKey, apiBaseURL)) ?? profile;\n } catch {\n // The profile is cosmetic; the sign-in is verified without it.\n // Not logged in detail: the request URL contains the API key.\n tryGetCurrentAuthEndpointContext()?.context.logger.warn(\n \"[authentication] could not load the Steam profile\",\n );\n }\n }\n const mapped = await options.mapProfileToUser?.(profile);\n return {\n user: {\n name: profile.personaname || `Steam ${steamid}`,\n email: placeholderEmail(steamid, emailDomain),\n emailVerified: false,\n image: profile.avatarfull,\n ...mapped,\n },\n data: profile,\n };\n },\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n options: {\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn,\n },\n };\n\n const callbackHook = {\n matcher: (context: HookEndpointContext) =>\n context.path === \"/callback/:id\" && (context.params as { id?: unknown })?.id === \"steam\",\n handler: createAuthMiddleware(async (ctx) => {\n const query = (ctx.query ?? {}) as Record<string, unknown>;\n const state = typeof query.state === \"string\" ? query.state : undefined;\n // A `code` in the query never reaches the callback: on failure it is\n // blanked (and `error` makes the callback stop first), on success\n // replaced by the hook's own.\n const fail = (error: string) => ({ context: { query: { state, code: \"\", error } } });\n // Steam always returns with GET. better-auth turns a POST to the\n // callback into a GET redirect, which comes back here.\n if (!state || ctx.method !== \"GET\") {\n return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_VERIFICATION_FAILED);\n }\n if (query[\"openid.mode\"] === \"cancel\") {\n return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_SIGN_IN_CANCELLED);\n }\n\n let steamId: string | null = null;\n try {\n const expected = steamReturnTo(`${ctx.context.baseURL}/callback/steam`, state);\n steamId = await verifySteamAssertion(query, expected);\n } catch (error) {\n ctx.context.logger.error(\"[authentication] Steam verification failed\", error);\n }\n if (!steamId) return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_VERIFICATION_FAILED);\n\n const code = generateRandomString(32);\n await ctx.context.internalAdapter.createVerificationValue({\n identifier: codeIdentifier(code),\n value: steamId,\n expiresAt: new Date(Date.now() + CODE_TTL_MS),\n });\n return { context: { query: { state, code } } };\n }),\n };\n\n return {\n provider: provider as unknown as OAuthProvider,\n emailDomain,\n hooks: { before: [callbackHook] },\n };\n}\n","import { tryGetCurrentAuthEndpointContext } from \"@better-auth/core/context\";\nimport type { OAuthProvider } from \"@better-auth/core/oauth2\";\nimport {\n createAuthorizationURL,\n refreshAccessToken,\n validateAuthorizationCode,\n} from \"@better-auth/core/oauth2\";\nimport type { XboxOptions, XboxProfile } from \"../types\";\nimport { placeholderEmail } from \"../utils/email\";\nimport type { ProviderDefinition } from \"./types\";\n\n/**\n * Microsoft's documented flow for Xbox sign-in on websites (\"Xbox services\n * sign-in for title websites\", Microsoft Game Development Kit): Microsoft\n * account OAuth on the `consumers` tenant with `xboxlive.signin`, then an\n * Xbox user token, then an XSTS token whose claims name the player.\n */\nexport const XBOX_AUTHORIZATION_ENDPOINT =\n \"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize\";\nexport const XBOX_TOKEN_ENDPOINT = \"https://login.microsoftonline.com/consumers/oauth2/v2.0/token\";\nexport const XBOX_USER_AUTH_ENDPOINT = \"https://user.auth.xboxlive.com/user/authenticate\";\nexport const XBOX_XSTS_ENDPOINT = \"https://xsts.auth.xboxlive.com/xsts/authorize\";\nconst XBOX_TIMEOUT_MS = 10_000;\n\ninterface XboxTokenResponse {\n Token?: string;\n DisplayClaims?: { xui?: Record<string, string>[] };\n}\n\nasync function postXbox(url: string, body: unknown): Promise<XboxTokenResponse | null> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n \"x-xbl-contract-version\": \"1\",\n },\n body: JSON.stringify(body),\n redirect: \"error\",\n signal: AbortSignal.timeout(XBOX_TIMEOUT_MS),\n });\n if (!response.ok) {\n // XErr names the reason, e.g. 2148916233: the account has no Xbox\n // profile. Only the status and that code are logged, never the body.\n const xErr = response.headers.get(\"x-err\") ?? \"none\";\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n `[authentication] Xbox token request to ${new URL(url).host} failed (${response.status}, XErr ${xErr})`,\n );\n return null;\n }\n try {\n return (await response.json()) as XboxTokenResponse;\n } catch {\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n `[authentication] Xbox token response from ${new URL(url).host} isn't JSON`,\n );\n return null;\n }\n}\n\n/** Exchanges a Microsoft access token for the player's XSTS claims (XUID, gamertag). */\nexport async function fetchXboxProfile(\n accessToken: string,\n { relyingParty, sandboxId }: { relyingParty: string; sandboxId: string },\n): Promise<XboxProfile | null> {\n const user = await postXbox(XBOX_USER_AUTH_ENDPOINT, {\n RelyingParty: \"http://auth.xboxlive.com\",\n TokenType: \"JWT\",\n Properties: {\n AuthMethod: \"RPS\",\n SiteName: \"user.auth.xboxlive.com\",\n RpsTicket: `d=${accessToken}`,\n },\n });\n if (!user?.Token) return null;\n const xsts = await postXbox(XBOX_XSTS_ENDPOINT, {\n RelyingParty: relyingParty,\n TokenType: \"JWT\",\n Properties: { SandboxId: sandboxId, UserTokens: [user.Token] },\n });\n const claims = xsts?.DisplayClaims?.xui?.[0];\n if (!claims?.xid) {\n if (xsts) {\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n \"[authentication] the XSTS token has no XUID (xid claim); check the relying party\",\n );\n }\n return null;\n }\n return claims as XboxProfile;\n}\n\n/**\n * Xbox, for apps enrolled in the Xbox developer program (Microsoft only\n * grants `xboxlive.signin` to them). The player is identified by the XUID\n * from XSTS; Xbox shares no email address.\n */\nexport function xbox(options: XboxOptions, emailDomain: string): ProviderDefinition {\n const relyingParty = options.relyingParty ?? \"http://xboxlive.com\";\n const sandboxId = options.sandboxId ?? \"RETAIL\";\n // Microsoft allows no other scopes in this request.\n const scopes = [\"xboxlive.signin\", ...(options.offlineAccess ? [\"xboxlive.offline_access\"] : [])];\n\n const provider: OAuthProvider<XboxProfile> = {\n id: \"xbox\",\n name: \"Xbox\",\n accountSubject: ({ profile }) => profile.xid,\n createAuthorizationURL({ state, codeVerifier, redirectURI, additionalParams }) {\n return createAuthorizationURL({\n id: \"xbox\",\n options,\n authorizationEndpoint: XBOX_AUTHORIZATION_ENDPOINT,\n scopes,\n state,\n codeVerifier,\n redirectURI,\n prompt: options.prompt,\n additionalParams,\n });\n },\n validateAuthorizationCode({ code, codeVerifier, redirectURI }) {\n return validateAuthorizationCode({\n code,\n codeVerifier,\n redirectURI,\n options,\n tokenEndpoint: XBOX_TOKEN_ENDPOINT,\n authentication: \"post\",\n });\n },\n refreshAccessToken: (refreshToken) =>\n refreshAccessToken({\n refreshToken,\n options: { clientId: options.clientId, clientSecret: options.clientSecret },\n tokenEndpoint: XBOX_TOKEN_ENDPOINT,\n authentication: \"post\",\n }),\n async getUserInfo(tokens) {\n if (!tokens.accessToken) return null;\n let profile: XboxProfile | null = null;\n try {\n profile = await fetchXboxProfile(tokens.accessToken, { relyingParty, sandboxId });\n } catch (error) {\n // Network failure or timeout: fail the sign-in like any other refusal.\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n \"[authentication] Xbox Live couldn't be reached\",\n error,\n );\n }\n if (!profile) return null;\n const mapped = await options.mapProfileToUser?.(profile);\n return {\n user: {\n name: profile.gtg || `Xbox ${profile.xid}`,\n email: placeholderEmail(profile.xid, emailDomain),\n emailVerified: false,\n ...mapped,\n },\n data: profile,\n };\n },\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n options: {\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n redirectURI: options.redirectURI,\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn,\n },\n };\n return { provider: provider as unknown as OAuthProvider, emailDomain };\n}\n","import { createFail } from \"@repo/plugin-utils/options\";\nimport { epic } from \"./providers/epic\";\nimport { steam } from \"./providers/steam\";\nimport type { ProviderDefinition } from \"./providers/types\";\nimport { xbox } from \"./providers/xbox\";\nimport type { AuthenticationOptions } from \"./types\";\nimport { isValidEmailDomain } from \"./utils/email\";\n\nconst fail = createFail(\"authentication\");\n\nfunction emailDomain(providerId: string, domain: string | undefined): string {\n const resolved = (domain ?? `${providerId}.invalid`).toLowerCase();\n if (!isValidEmailDomain(resolved)) {\n fail(`\"${providerId}.emailDomain\" must be a domain name, got ${JSON.stringify(domain)}.`);\n }\n return resolved;\n}\n\nfunction requireClient(\n providerId: string,\n options: { clientId?: unknown; clientSecret?: unknown },\n) {\n if (typeof options.clientId !== \"string\" || options.clientId === \"\") {\n fail(`\"${providerId}.clientId\" is required.`);\n }\n if (typeof options.clientSecret !== \"string\" || options.clientSecret === \"\") {\n fail(`\"${providerId}.clientSecret\" is required.`);\n }\n}\n\n/** Validates the options once, at creation, and builds the providers. */\nexport function resolveProviders(options: AuthenticationOptions): ProviderDefinition[] {\n const definitions: ProviderDefinition[] = [];\n if (options.steam) {\n if (options.steam.apiKey !== undefined && options.steam.apiKey.trim() === \"\") {\n fail('\"steam.apiKey\" is empty; leave it out to sign in without loading profiles.');\n }\n definitions.push(steam(options.steam, emailDomain(\"steam\", options.steam.emailDomain)));\n }\n if (options.epic) {\n requireClient(\"epic\", options.epic);\n definitions.push(epic(options.epic, emailDomain(\"epic\", options.epic.emailDomain)));\n }\n if (options.xbox) {\n requireClient(\"xbox\", options.xbox);\n definitions.push(xbox(options.xbox, emailDomain(\"xbox\", options.xbox.emailDomain)));\n }\n if (definitions.length === 0) fail(\"Configure at least one provider: steam, epic, or xbox.\");\n\n const domains = definitions.map((definition) => definition.emailDomain);\n if (new Set(domains).size !== domains.length) {\n fail(\"Each provider needs an email domain of its own.\");\n }\n return definitions;\n}\n","import type { BetterAuthPlugin } from \"@better-auth/core\";\nimport { version } from \"../package.json\";\nimport { AUTHENTICATION_ERROR_CODES } from \"./error-codes\";\nimport { createReservedEmailHooks } from \"./hooks/reserved-email\";\nimport { resolveProviders } from \"./options\";\nimport type { AuthenticationOptions } from \"./types\";\n\nexport {\n AUTHENTICATION_CALLBACK_ERRORS,\n AUTHENTICATION_ERROR_CODES,\n type AuthenticationErrorCode,\n} from \"./error-codes\";\nexport type {\n AuthenticationOptions,\n AuthenticationProviderId,\n CommonProviderOptions,\n EpicOptions,\n EpicProfile,\n SteamOptions,\n SteamProfile,\n XboxOptions,\n XboxProfile,\n} from \"./types\";\n\n/**\n * Adds Steam, Epic Games, and Xbox as social providers. They are used like\n * better-auth's built-in ones: `signIn.social({ provider: \"steam\" })`,\n * `linkSocial`, callbacks at `/callback/<provider>`, account linking,\n * `callbackURL` checks, and sign-up options all work the same way.\n *\n * @example\n * ```ts\n * import { betterAuth } from \"better-auth\";\n * import { authentication } from \"@checodo/better-auth-authentication\";\n *\n * export const auth = betterAuth({\n * plugins: [\n * authentication({\n * steam: { apiKey: process.env.STEAM_API_KEY },\n * epic: { clientId: \"...\", clientSecret: \"...\" },\n * }),\n * ],\n * });\n *\n * // client\n * await authClient.signIn.social({ provider: \"steam\", callbackURL: \"/dashboard\" });\n * ```\n */\nexport const authentication = (options: AuthenticationOptions) => {\n const definitions = resolveProviders(options);\n const before = definitions.flatMap((definition) => definition.hooks?.before ?? []);\n\n return {\n id: \"authentication\",\n version,\n init(ctx) {\n const ids = new Set(definitions.map((definition) => definition.provider.id));\n for (const existing of ctx.socialProviders) {\n if (ids.has(existing.id)) {\n ctx.logger.warn(\n `[authentication] the \"${existing.id}\" provider replaces a social provider with the same id`,\n );\n }\n }\n return {\n context: {\n socialProviders: [\n ...definitions.map((definition) => definition.provider),\n ...ctx.socialProviders.filter((provider) => !ids.has(provider.id)),\n ],\n },\n options: { databaseHooks: createReservedEmailHooks(definitions) },\n };\n },\n hooks: { before },\n $ERROR_CODES: AUTHENTICATION_ERROR_CODES,\n options,\n } satisfies BetterAuthPlugin;\n};\n\nexport type AuthenticationPlugin = ReturnType<typeof authentication>;\n\n/** Lets `auth.$context.getPlugin(\"authentication\")` infer this plugin's exact type. */\ndeclare module \"@better-auth/core\" {\n interface BetterAuthPluginRegistry<AuthOptions, Options> {\n authentication: {\n creator: typeof authentication;\n };\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checodo/better-auth-authentication",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sign in with Steam, Epic Games, and Xbox in better-auth, like any built-in social provider.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Clas Heinemeyer",
|
|
9
|
+
"email": "the.checodo@gmail.com",
|
|
10
|
+
"url": "https://checodo.dev"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/checodo/better-auth-plugins/tree/main/packages/authentication",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/checodo/better-auth-plugins.git",
|
|
16
|
+
"directory": "packages/authentication"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/checodo/better-auth-plugins/issues"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"better-auth",
|
|
23
|
+
"better-auth-plugin",
|
|
24
|
+
"oauth",
|
|
25
|
+
"steam",
|
|
26
|
+
"epic-games",
|
|
27
|
+
"xbox",
|
|
28
|
+
"auth"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./client": {
|
|
37
|
+
"types": "./dist/client.d.ts",
|
|
38
|
+
"default": "./dist/client.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"main": "./dist/index.js",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"CHANGELOG.md"
|
|
47
|
+
],
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@better-auth/core": "^1.7.20",
|
|
50
|
+
"better-auth": "^1.7.20"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"zod": "^4.5.4"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@better-auth/core": "^1.7.20",
|
|
57
|
+
"@checodo/better-auth-waitlist": "0.1.0",
|
|
58
|
+
"@repo/plugin-utils": "0.0.0",
|
|
59
|
+
"@repo/tooling": "0.0.0",
|
|
60
|
+
"@types/node": "^24.19.2",
|
|
61
|
+
"better-auth": "^1.7.20",
|
|
62
|
+
"tsdown": "^0.23.7",
|
|
63
|
+
"typescript": "^5.9.3",
|
|
64
|
+
"vitest": "^5.1.0"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsdown",
|
|
71
|
+
"dev": "tsdown --watch --no-clean",
|
|
72
|
+
"test": "vitest run",
|
|
73
|
+
"test:watch": "vitest",
|
|
74
|
+
"typecheck": "tsc --noEmit",
|
|
75
|
+
"clean": "rm -rf dist .turbo",
|
|
76
|
+
"lint:package": "publint && attw --pack . --profile esm-only"
|
|
77
|
+
}
|
|
6
78
|
}
|