@nixxie-cms/auth-oauth 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +35 -0
- package/dist/declarations/src/index.d.ts +29 -0
- package/dist/declarations/src/index.d.ts.map +1 -0
- package/dist/declarations/src/providers.d.ts +3 -0
- package/dist/declarations/src/providers.d.ts.map +1 -0
- package/dist/declarations/src/types.d.ts +48 -0
- package/dist/declarations/src/types.d.ts.map +1 -0
- package/dist/nixxie-cms-auth-oauth.cjs.d.ts +2 -0
- package/dist/nixxie-cms-auth-oauth.cjs.js +217 -0
- package/dist/nixxie-cms-auth-oauth.esm.js +211 -0
- package/package.json +33 -0
- package/src/index.ts +121 -0
- package/src/providers.ts +84 -0
- package/src/types.ts +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nixxie International DMCC
|
|
4
|
+
Portions Copyright (c) 2023 Thinkmill Labs Pty Ltd and contributors
|
|
5
|
+
(this software is derived from the KeystoneJS project, https://keystonejs.com)
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @nixxie-cms/auth-oauth
|
|
2
|
+
|
|
3
|
+
Social / OAuth 2.0 sign-in helpers for Nixxie CMS. Built-in providers for Google, GitHub, GitLab,
|
|
4
|
+
Discord, Microsoft and Facebook, plus a `definition` escape hatch for any OAuth 2.0 / OIDC
|
|
5
|
+
provider. No SDK dependencies — it talks to the provider endpoints with `fetch`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createOAuth } from '@nixxie-cms/auth-oauth'
|
|
9
|
+
|
|
10
|
+
const oauth = createOAuth({
|
|
11
|
+
providers: {
|
|
12
|
+
google: {
|
|
13
|
+
provider: 'google',
|
|
14
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
15
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
16
|
+
redirectUri: 'http://localhost:3000/auth/google/callback',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Wire it into Express via `server.extendExpressApp`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
app.get('/auth/google', (req, res) => {
|
|
26
|
+
const state = oauth.createState()
|
|
27
|
+
// persist `state` in the session, then:
|
|
28
|
+
res.redirect(oauth.authorizationUrl('google', { state }))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
app.get('/auth/google/callback', async (req, res) => {
|
|
32
|
+
const { profile } = await oauth.callback('google', req.query.code as string)
|
|
33
|
+
// upsert a user keyed by profile.id / profile.email and start a session
|
|
34
|
+
})
|
|
35
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { providers as builtInProviders } from "./providers.js";
|
|
2
|
+
import type { OAuthConfig, OAuthProfile, OAuthTokens } from "./types.js";
|
|
3
|
+
export declare class OAuth {
|
|
4
|
+
private config;
|
|
5
|
+
constructor(config: OAuthConfig);
|
|
6
|
+
/** Names of the configured providers. */
|
|
7
|
+
list(): string[];
|
|
8
|
+
private get;
|
|
9
|
+
/** Generate a random `state` value to protect against CSRF. Persist it in the session. */
|
|
10
|
+
createState(): string;
|
|
11
|
+
/** Build the URL to redirect the user to in order to begin the flow. */
|
|
12
|
+
authorizationUrl(name: string, options?: {
|
|
13
|
+
state?: string;
|
|
14
|
+
extraParams?: Record<string, string>;
|
|
15
|
+
}): string;
|
|
16
|
+
/** Exchange the authorization `code` returned to your callback for tokens. */
|
|
17
|
+
exchangeCode(name: string, code: string): Promise<OAuthTokens>;
|
|
18
|
+
/** Fetch the normalised user profile using an access token. */
|
|
19
|
+
fetchProfile(name: string, tokens: OAuthTokens): Promise<OAuthProfile>;
|
|
20
|
+
/** Convenience: exchange the code and fetch the profile in one call. */
|
|
21
|
+
callback(name: string, code: string): Promise<{
|
|
22
|
+
tokens: OAuthTokens;
|
|
23
|
+
profile: OAuthProfile;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export declare function createOAuth(config: OAuthConfig): OAuth;
|
|
27
|
+
export { builtInProviders as providers };
|
|
28
|
+
export type { OAuthConfig, OAuthProviderConfig, OAuthProviderDefinition, OAuthProviderName, OAuthProfile, OAuthTokens, } from "./types.js";
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"../../../src","sources":["index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,IAAI,gBAAgB,EAAE,uBAAmB;AAC3D,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EAGZ,WAAW,EACZ,mBAAe;AAQhB,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAa;gBAEf,MAAM,EAAE,WAAW;IAI/B,yCAAyC;IACzC,IAAI,IAAI,MAAM,EAAE;IAIhB,OAAO,CAAC,GAAG;IAMX,0FAA0F;IAC1F,WAAW,IAAI,MAAM;IAIrB,wEAAwE;IACxE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,MAAM;IAa1G,8EAA8E;IACxE,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IA+BpE,+DAA+D;IACzD,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAc5E,wEAAwE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC;CAKpG;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAEtD;AAED,OAAO,EAAE,gBAAgB,IAAI,SAAS,EAAE,CAAA;AACxC,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,mBAAe"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"../../../src","sources":["providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,mBAAe;AAEzE,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAiFxE,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type OAuthProviderName = 'google' | 'github' | 'gitlab' | 'discord' | 'microsoft' | 'facebook';
|
|
2
|
+
/** Endpoints + behaviour for an OAuth 2.0 / OpenID Connect provider. */
|
|
3
|
+
export type OAuthProviderDefinition = {
|
|
4
|
+
/** Authorization endpoint (where the user is redirected to consent). */
|
|
5
|
+
authorizationUrl: string;
|
|
6
|
+
/** Token endpoint (where the auth code is exchanged for tokens). */
|
|
7
|
+
tokenUrl: string;
|
|
8
|
+
/** Userinfo endpoint used to fetch the profile. */
|
|
9
|
+
userInfoUrl: string;
|
|
10
|
+
/** Default scopes requested when the caller doesn't override them. */
|
|
11
|
+
defaultScopes: string[];
|
|
12
|
+
/** Normalise the raw userinfo response into a common profile shape. */
|
|
13
|
+
profile: (raw: any) => OAuthProfile;
|
|
14
|
+
};
|
|
15
|
+
export type OAuthProfile = {
|
|
16
|
+
/** Stable provider-specific user id. */
|
|
17
|
+
id: string;
|
|
18
|
+
email?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
avatarUrl?: string;
|
|
21
|
+
/** The untouched provider response. */
|
|
22
|
+
raw: any;
|
|
23
|
+
};
|
|
24
|
+
export type OAuthTokens = {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
refreshToken?: string;
|
|
27
|
+
expiresIn?: number;
|
|
28
|
+
tokenType?: string;
|
|
29
|
+
scope?: string;
|
|
30
|
+
idToken?: string;
|
|
31
|
+
};
|
|
32
|
+
export type OAuthProviderConfig = {
|
|
33
|
+
/** Built-in provider name, or omit and supply `definition` for a custom provider. */
|
|
34
|
+
provider?: OAuthProviderName;
|
|
35
|
+
/** Custom provider definition (overrides `provider`). */
|
|
36
|
+
definition?: OAuthProviderDefinition;
|
|
37
|
+
clientId: string;
|
|
38
|
+
clientSecret: string;
|
|
39
|
+
/** Redirect/callback URL registered with the provider. */
|
|
40
|
+
redirectUri: string;
|
|
41
|
+
/** Override the provider's default scopes. */
|
|
42
|
+
scopes?: string[];
|
|
43
|
+
};
|
|
44
|
+
export type OAuthConfig = {
|
|
45
|
+
/** Map of named providers, keyed by however you want to reference them in routes. */
|
|
46
|
+
providers: Record<string, OAuthProviderConfig>;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"../../../src","sources":["types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,WAAW,GACX,UAAU,CAAA;AAEd,wEAAwE;AACxE,MAAM,MAAM,uBAAuB,GAAG;IACpC,wEAAwE;IACxE,gBAAgB,EAAE,MAAM,CAAA;IACxB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAA;IAChB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,uEAAuE;IACvE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,YAAY,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uCAAuC;IACvC,GAAG,EAAE,GAAG,CAAA;CACT,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,qFAAqF;IACrF,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,yDAAyD;IACzD,UAAU,CAAC,EAAE,uBAAuB,CAAA;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAA;IACnB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;CAC/C,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export * from "./declarations/src/index.js";
|
|
2
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibml4eGllLWNtcy1hdXRoLW9hdXRoLmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi9kZWNsYXJhdGlvbnMvc3JjL2luZGV4LmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEifQ==
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var node_crypto = require('node:crypto');
|
|
6
|
+
|
|
7
|
+
const providers = {
|
|
8
|
+
google: {
|
|
9
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
10
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
11
|
+
userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
12
|
+
defaultScopes: ['openid', 'email', 'profile'],
|
|
13
|
+
profile: raw => ({
|
|
14
|
+
id: raw.sub,
|
|
15
|
+
email: raw.email,
|
|
16
|
+
name: raw.name,
|
|
17
|
+
avatarUrl: raw.picture,
|
|
18
|
+
raw
|
|
19
|
+
})
|
|
20
|
+
},
|
|
21
|
+
github: {
|
|
22
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
23
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
24
|
+
userInfoUrl: 'https://api.github.com/user',
|
|
25
|
+
defaultScopes: ['read:user', 'user:email'],
|
|
26
|
+
profile: raw => {
|
|
27
|
+
var _raw$name;
|
|
28
|
+
return {
|
|
29
|
+
id: String(raw.id),
|
|
30
|
+
email: raw.email,
|
|
31
|
+
name: (_raw$name = raw.name) !== null && _raw$name !== void 0 ? _raw$name : raw.login,
|
|
32
|
+
avatarUrl: raw.avatar_url,
|
|
33
|
+
raw
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
gitlab: {
|
|
38
|
+
authorizationUrl: 'https://gitlab.com/oauth/authorize',
|
|
39
|
+
tokenUrl: 'https://gitlab.com/oauth/token',
|
|
40
|
+
userInfoUrl: 'https://gitlab.com/api/v4/user',
|
|
41
|
+
defaultScopes: ['read_user'],
|
|
42
|
+
profile: raw => {
|
|
43
|
+
var _raw$name2;
|
|
44
|
+
return {
|
|
45
|
+
id: String(raw.id),
|
|
46
|
+
email: raw.email,
|
|
47
|
+
name: (_raw$name2 = raw.name) !== null && _raw$name2 !== void 0 ? _raw$name2 : raw.username,
|
|
48
|
+
avatarUrl: raw.avatar_url,
|
|
49
|
+
raw
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
discord: {
|
|
54
|
+
authorizationUrl: 'https://discord.com/oauth2/authorize',
|
|
55
|
+
tokenUrl: 'https://discord.com/api/oauth2/token',
|
|
56
|
+
userInfoUrl: 'https://discord.com/api/users/@me',
|
|
57
|
+
defaultScopes: ['identify', 'email'],
|
|
58
|
+
profile: raw => {
|
|
59
|
+
var _raw$global_name;
|
|
60
|
+
return {
|
|
61
|
+
id: raw.id,
|
|
62
|
+
email: raw.email,
|
|
63
|
+
name: (_raw$global_name = raw.global_name) !== null && _raw$global_name !== void 0 ? _raw$global_name : raw.username,
|
|
64
|
+
avatarUrl: raw.avatar ? `https://cdn.discordapp.com/avatars/${raw.id}/${raw.avatar}.png` : undefined,
|
|
65
|
+
raw
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
microsoft: {
|
|
70
|
+
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
|
71
|
+
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
|
72
|
+
userInfoUrl: 'https://graph.microsoft.com/oidc/userinfo',
|
|
73
|
+
defaultScopes: ['openid', 'email', 'profile'],
|
|
74
|
+
profile: raw => ({
|
|
75
|
+
id: raw.sub,
|
|
76
|
+
email: raw.email,
|
|
77
|
+
name: raw.name,
|
|
78
|
+
avatarUrl: raw.picture,
|
|
79
|
+
raw
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
facebook: {
|
|
83
|
+
authorizationUrl: 'https://www.facebook.com/v19.0/dialog/oauth',
|
|
84
|
+
tokenUrl: 'https://graph.facebook.com/v19.0/oauth/access_token',
|
|
85
|
+
userInfoUrl: 'https://graph.facebook.com/me?fields=id,name,email,picture',
|
|
86
|
+
defaultScopes: ['email', 'public_profile'],
|
|
87
|
+
profile: raw => {
|
|
88
|
+
var _raw$picture;
|
|
89
|
+
return {
|
|
90
|
+
id: raw.id,
|
|
91
|
+
email: raw.email,
|
|
92
|
+
name: raw.name,
|
|
93
|
+
avatarUrl: (_raw$picture = raw.picture) === null || _raw$picture === void 0 || (_raw$picture = _raw$picture.data) === null || _raw$picture === void 0 ? void 0 : _raw$picture.url,
|
|
94
|
+
raw
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function resolveDefinition(cfg) {
|
|
101
|
+
if (cfg.definition) return cfg.definition;
|
|
102
|
+
if (cfg.provider) return providers[cfg.provider];
|
|
103
|
+
throw new Error('OAuth provider config must specify either `provider` or `definition`');
|
|
104
|
+
}
|
|
105
|
+
class OAuth {
|
|
106
|
+
constructor(config) {
|
|
107
|
+
this.config = config;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Names of the configured providers. */
|
|
111
|
+
list() {
|
|
112
|
+
return Object.keys(this.config.providers);
|
|
113
|
+
}
|
|
114
|
+
get(name) {
|
|
115
|
+
const cfg = this.config.providers[name];
|
|
116
|
+
if (!cfg) throw new Error(`Unknown OAuth provider: ${name}`);
|
|
117
|
+
return {
|
|
118
|
+
cfg,
|
|
119
|
+
def: resolveDefinition(cfg)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Generate a random `state` value to protect against CSRF. Persist it in the session. */
|
|
124
|
+
createState() {
|
|
125
|
+
return node_crypto.randomBytes(16).toString('hex');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Build the URL to redirect the user to in order to begin the flow. */
|
|
129
|
+
authorizationUrl(name, options) {
|
|
130
|
+
var _cfg$scopes;
|
|
131
|
+
const {
|
|
132
|
+
cfg,
|
|
133
|
+
def
|
|
134
|
+
} = this.get(name);
|
|
135
|
+
const params = new URLSearchParams({
|
|
136
|
+
response_type: 'code',
|
|
137
|
+
client_id: cfg.clientId,
|
|
138
|
+
redirect_uri: cfg.redirectUri,
|
|
139
|
+
scope: ((_cfg$scopes = cfg.scopes) !== null && _cfg$scopes !== void 0 ? _cfg$scopes : def.defaultScopes).join(' '),
|
|
140
|
+
...(options !== null && options !== void 0 && options.state ? {
|
|
141
|
+
state: options.state
|
|
142
|
+
} : {}),
|
|
143
|
+
...(options === null || options === void 0 ? void 0 : options.extraParams)
|
|
144
|
+
});
|
|
145
|
+
return `${def.authorizationUrl}?${params}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Exchange the authorization `code` returned to your callback for tokens. */
|
|
149
|
+
async exchangeCode(name, code) {
|
|
150
|
+
var _data$error_descripti;
|
|
151
|
+
const {
|
|
152
|
+
cfg,
|
|
153
|
+
def
|
|
154
|
+
} = this.get(name);
|
|
155
|
+
const res = await fetch(def.tokenUrl, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
159
|
+
Accept: 'application/json',
|
|
160
|
+
// GitHub's API rejects requests without a User-Agent (HTTP 403); harmless for others.
|
|
161
|
+
'User-Agent': 'nixxie-cms-oauth'
|
|
162
|
+
},
|
|
163
|
+
body: new URLSearchParams({
|
|
164
|
+
grant_type: 'authorization_code',
|
|
165
|
+
code,
|
|
166
|
+
client_id: cfg.clientId,
|
|
167
|
+
client_secret: cfg.clientSecret,
|
|
168
|
+
redirect_uri: cfg.redirectUri
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) throw new Error(`OAuth token exchange failed (${res.status}): ${await res.text()}`);
|
|
172
|
+
const data = await res.json();
|
|
173
|
+
if (data.error) throw new Error(`OAuth token exchange error: ${(_data$error_descripti = data.error_description) !== null && _data$error_descripti !== void 0 ? _data$error_descripti : data.error}`);
|
|
174
|
+
return {
|
|
175
|
+
accessToken: data.access_token,
|
|
176
|
+
refreshToken: data.refresh_token,
|
|
177
|
+
expiresIn: data.expires_in,
|
|
178
|
+
tokenType: data.token_type,
|
|
179
|
+
scope: data.scope,
|
|
180
|
+
idToken: data.id_token
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Fetch the normalised user profile using an access token. */
|
|
185
|
+
async fetchProfile(name, tokens) {
|
|
186
|
+
const {
|
|
187
|
+
def
|
|
188
|
+
} = this.get(name);
|
|
189
|
+
const res = await fetch(def.userInfoUrl, {
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: `Bearer ${tokens.accessToken}`,
|
|
192
|
+
Accept: 'application/json',
|
|
193
|
+
// GitHub's API rejects requests without a User-Agent (HTTP 403); harmless for others.
|
|
194
|
+
'User-Agent': 'nixxie-cms-oauth'
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
if (!res.ok) throw new Error(`OAuth userinfo failed (${res.status}): ${await res.text()}`);
|
|
198
|
+
return def.profile(await res.json());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Convenience: exchange the code and fetch the profile in one call. */
|
|
202
|
+
async callback(name, code) {
|
|
203
|
+
const tokens = await this.exchangeCode(name, code);
|
|
204
|
+
const profile = await this.fetchProfile(name, tokens);
|
|
205
|
+
return {
|
|
206
|
+
tokens,
|
|
207
|
+
profile
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function createOAuth(config) {
|
|
212
|
+
return new OAuth(config);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
exports.OAuth = OAuth;
|
|
216
|
+
exports.createOAuth = createOAuth;
|
|
217
|
+
exports.providers = providers;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const providers = {
|
|
4
|
+
google: {
|
|
5
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
6
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
7
|
+
userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
8
|
+
defaultScopes: ['openid', 'email', 'profile'],
|
|
9
|
+
profile: raw => ({
|
|
10
|
+
id: raw.sub,
|
|
11
|
+
email: raw.email,
|
|
12
|
+
name: raw.name,
|
|
13
|
+
avatarUrl: raw.picture,
|
|
14
|
+
raw
|
|
15
|
+
})
|
|
16
|
+
},
|
|
17
|
+
github: {
|
|
18
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
19
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
20
|
+
userInfoUrl: 'https://api.github.com/user',
|
|
21
|
+
defaultScopes: ['read:user', 'user:email'],
|
|
22
|
+
profile: raw => {
|
|
23
|
+
var _raw$name;
|
|
24
|
+
return {
|
|
25
|
+
id: String(raw.id),
|
|
26
|
+
email: raw.email,
|
|
27
|
+
name: (_raw$name = raw.name) !== null && _raw$name !== void 0 ? _raw$name : raw.login,
|
|
28
|
+
avatarUrl: raw.avatar_url,
|
|
29
|
+
raw
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
gitlab: {
|
|
34
|
+
authorizationUrl: 'https://gitlab.com/oauth/authorize',
|
|
35
|
+
tokenUrl: 'https://gitlab.com/oauth/token',
|
|
36
|
+
userInfoUrl: 'https://gitlab.com/api/v4/user',
|
|
37
|
+
defaultScopes: ['read_user'],
|
|
38
|
+
profile: raw => {
|
|
39
|
+
var _raw$name2;
|
|
40
|
+
return {
|
|
41
|
+
id: String(raw.id),
|
|
42
|
+
email: raw.email,
|
|
43
|
+
name: (_raw$name2 = raw.name) !== null && _raw$name2 !== void 0 ? _raw$name2 : raw.username,
|
|
44
|
+
avatarUrl: raw.avatar_url,
|
|
45
|
+
raw
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
discord: {
|
|
50
|
+
authorizationUrl: 'https://discord.com/oauth2/authorize',
|
|
51
|
+
tokenUrl: 'https://discord.com/api/oauth2/token',
|
|
52
|
+
userInfoUrl: 'https://discord.com/api/users/@me',
|
|
53
|
+
defaultScopes: ['identify', 'email'],
|
|
54
|
+
profile: raw => {
|
|
55
|
+
var _raw$global_name;
|
|
56
|
+
return {
|
|
57
|
+
id: raw.id,
|
|
58
|
+
email: raw.email,
|
|
59
|
+
name: (_raw$global_name = raw.global_name) !== null && _raw$global_name !== void 0 ? _raw$global_name : raw.username,
|
|
60
|
+
avatarUrl: raw.avatar ? `https://cdn.discordapp.com/avatars/${raw.id}/${raw.avatar}.png` : undefined,
|
|
61
|
+
raw
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
microsoft: {
|
|
66
|
+
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
|
67
|
+
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
|
68
|
+
userInfoUrl: 'https://graph.microsoft.com/oidc/userinfo',
|
|
69
|
+
defaultScopes: ['openid', 'email', 'profile'],
|
|
70
|
+
profile: raw => ({
|
|
71
|
+
id: raw.sub,
|
|
72
|
+
email: raw.email,
|
|
73
|
+
name: raw.name,
|
|
74
|
+
avatarUrl: raw.picture,
|
|
75
|
+
raw
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
facebook: {
|
|
79
|
+
authorizationUrl: 'https://www.facebook.com/v19.0/dialog/oauth',
|
|
80
|
+
tokenUrl: 'https://graph.facebook.com/v19.0/oauth/access_token',
|
|
81
|
+
userInfoUrl: 'https://graph.facebook.com/me?fields=id,name,email,picture',
|
|
82
|
+
defaultScopes: ['email', 'public_profile'],
|
|
83
|
+
profile: raw => {
|
|
84
|
+
var _raw$picture;
|
|
85
|
+
return {
|
|
86
|
+
id: raw.id,
|
|
87
|
+
email: raw.email,
|
|
88
|
+
name: raw.name,
|
|
89
|
+
avatarUrl: (_raw$picture = raw.picture) === null || _raw$picture === void 0 || (_raw$picture = _raw$picture.data) === null || _raw$picture === void 0 ? void 0 : _raw$picture.url,
|
|
90
|
+
raw
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function resolveDefinition(cfg) {
|
|
97
|
+
if (cfg.definition) return cfg.definition;
|
|
98
|
+
if (cfg.provider) return providers[cfg.provider];
|
|
99
|
+
throw new Error('OAuth provider config must specify either `provider` or `definition`');
|
|
100
|
+
}
|
|
101
|
+
class OAuth {
|
|
102
|
+
constructor(config) {
|
|
103
|
+
this.config = config;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Names of the configured providers. */
|
|
107
|
+
list() {
|
|
108
|
+
return Object.keys(this.config.providers);
|
|
109
|
+
}
|
|
110
|
+
get(name) {
|
|
111
|
+
const cfg = this.config.providers[name];
|
|
112
|
+
if (!cfg) throw new Error(`Unknown OAuth provider: ${name}`);
|
|
113
|
+
return {
|
|
114
|
+
cfg,
|
|
115
|
+
def: resolveDefinition(cfg)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Generate a random `state` value to protect against CSRF. Persist it in the session. */
|
|
120
|
+
createState() {
|
|
121
|
+
return randomBytes(16).toString('hex');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Build the URL to redirect the user to in order to begin the flow. */
|
|
125
|
+
authorizationUrl(name, options) {
|
|
126
|
+
var _cfg$scopes;
|
|
127
|
+
const {
|
|
128
|
+
cfg,
|
|
129
|
+
def
|
|
130
|
+
} = this.get(name);
|
|
131
|
+
const params = new URLSearchParams({
|
|
132
|
+
response_type: 'code',
|
|
133
|
+
client_id: cfg.clientId,
|
|
134
|
+
redirect_uri: cfg.redirectUri,
|
|
135
|
+
scope: ((_cfg$scopes = cfg.scopes) !== null && _cfg$scopes !== void 0 ? _cfg$scopes : def.defaultScopes).join(' '),
|
|
136
|
+
...(options !== null && options !== void 0 && options.state ? {
|
|
137
|
+
state: options.state
|
|
138
|
+
} : {}),
|
|
139
|
+
...(options === null || options === void 0 ? void 0 : options.extraParams)
|
|
140
|
+
});
|
|
141
|
+
return `${def.authorizationUrl}?${params}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Exchange the authorization `code` returned to your callback for tokens. */
|
|
145
|
+
async exchangeCode(name, code) {
|
|
146
|
+
var _data$error_descripti;
|
|
147
|
+
const {
|
|
148
|
+
cfg,
|
|
149
|
+
def
|
|
150
|
+
} = this.get(name);
|
|
151
|
+
const res = await fetch(def.tokenUrl, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
155
|
+
Accept: 'application/json',
|
|
156
|
+
// GitHub's API rejects requests without a User-Agent (HTTP 403); harmless for others.
|
|
157
|
+
'User-Agent': 'nixxie-cms-oauth'
|
|
158
|
+
},
|
|
159
|
+
body: new URLSearchParams({
|
|
160
|
+
grant_type: 'authorization_code',
|
|
161
|
+
code,
|
|
162
|
+
client_id: cfg.clientId,
|
|
163
|
+
client_secret: cfg.clientSecret,
|
|
164
|
+
redirect_uri: cfg.redirectUri
|
|
165
|
+
})
|
|
166
|
+
});
|
|
167
|
+
if (!res.ok) throw new Error(`OAuth token exchange failed (${res.status}): ${await res.text()}`);
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
if (data.error) throw new Error(`OAuth token exchange error: ${(_data$error_descripti = data.error_description) !== null && _data$error_descripti !== void 0 ? _data$error_descripti : data.error}`);
|
|
170
|
+
return {
|
|
171
|
+
accessToken: data.access_token,
|
|
172
|
+
refreshToken: data.refresh_token,
|
|
173
|
+
expiresIn: data.expires_in,
|
|
174
|
+
tokenType: data.token_type,
|
|
175
|
+
scope: data.scope,
|
|
176
|
+
idToken: data.id_token
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Fetch the normalised user profile using an access token. */
|
|
181
|
+
async fetchProfile(name, tokens) {
|
|
182
|
+
const {
|
|
183
|
+
def
|
|
184
|
+
} = this.get(name);
|
|
185
|
+
const res = await fetch(def.userInfoUrl, {
|
|
186
|
+
headers: {
|
|
187
|
+
Authorization: `Bearer ${tokens.accessToken}`,
|
|
188
|
+
Accept: 'application/json',
|
|
189
|
+
// GitHub's API rejects requests without a User-Agent (HTTP 403); harmless for others.
|
|
190
|
+
'User-Agent': 'nixxie-cms-oauth'
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) throw new Error(`OAuth userinfo failed (${res.status}): ${await res.text()}`);
|
|
194
|
+
return def.profile(await res.json());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Convenience: exchange the code and fetch the profile in one call. */
|
|
198
|
+
async callback(name, code) {
|
|
199
|
+
const tokens = await this.exchangeCode(name, code);
|
|
200
|
+
const profile = await this.fetchProfile(name, tokens);
|
|
201
|
+
return {
|
|
202
|
+
tokens,
|
|
203
|
+
profile
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function createOAuth(config) {
|
|
208
|
+
return new OAuth(config);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export { OAuth, createOAuth, providers };
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nixxie-cms/auth-oauth",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "dist/nixxie-cms-auth-oauth.cjs.js",
|
|
6
|
+
"module": "dist/nixxie-cms-auth-oauth.esm.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/nixxie-cms-auth-oauth.cjs.js",
|
|
10
|
+
"module": "./dist/nixxie-cms-auth-oauth.esm.js",
|
|
11
|
+
"default": "./dist/nixxie-cms-auth-oauth.cjs.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@babel/runtime": "^7.24.7"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@nixxie-cms/core": "^1.0.1"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@nixxie-cms/core": "^1.0.1"
|
|
23
|
+
},
|
|
24
|
+
"preconstruct": {
|
|
25
|
+
"entrypoints": [
|
|
26
|
+
"index.ts"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/nixxiecms/nixxie/tree/main/packages/auth-oauth"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { providers as builtInProviders } from './providers'
|
|
3
|
+
import type {
|
|
4
|
+
OAuthConfig,
|
|
5
|
+
OAuthProfile,
|
|
6
|
+
OAuthProviderConfig,
|
|
7
|
+
OAuthProviderDefinition,
|
|
8
|
+
OAuthTokens,
|
|
9
|
+
} from './types'
|
|
10
|
+
|
|
11
|
+
function resolveDefinition(cfg: OAuthProviderConfig): OAuthProviderDefinition {
|
|
12
|
+
if (cfg.definition) return cfg.definition
|
|
13
|
+
if (cfg.provider) return builtInProviders[cfg.provider]
|
|
14
|
+
throw new Error('OAuth provider config must specify either `provider` or `definition`')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class OAuth {
|
|
18
|
+
private config: OAuthConfig
|
|
19
|
+
|
|
20
|
+
constructor(config: OAuthConfig) {
|
|
21
|
+
this.config = config
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Names of the configured providers. */
|
|
25
|
+
list(): string[] {
|
|
26
|
+
return Object.keys(this.config.providers)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private get(name: string): { cfg: OAuthProviderConfig; def: OAuthProviderDefinition } {
|
|
30
|
+
const cfg = this.config.providers[name]
|
|
31
|
+
if (!cfg) throw new Error(`Unknown OAuth provider: ${name}`)
|
|
32
|
+
return { cfg, def: resolveDefinition(cfg) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Generate a random `state` value to protect against CSRF. Persist it in the session. */
|
|
36
|
+
createState(): string {
|
|
37
|
+
return randomBytes(16).toString('hex')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Build the URL to redirect the user to in order to begin the flow. */
|
|
41
|
+
authorizationUrl(name: string, options?: { state?: string; extraParams?: Record<string, string> }): string {
|
|
42
|
+
const { cfg, def } = this.get(name)
|
|
43
|
+
const params = new URLSearchParams({
|
|
44
|
+
response_type: 'code',
|
|
45
|
+
client_id: cfg.clientId,
|
|
46
|
+
redirect_uri: cfg.redirectUri,
|
|
47
|
+
scope: (cfg.scopes ?? def.defaultScopes).join(' '),
|
|
48
|
+
...(options?.state ? { state: options.state } : {}),
|
|
49
|
+
...options?.extraParams,
|
|
50
|
+
})
|
|
51
|
+
return `${def.authorizationUrl}?${params}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Exchange the authorization `code` returned to your callback for tokens. */
|
|
55
|
+
async exchangeCode(name: string, code: string): Promise<OAuthTokens> {
|
|
56
|
+
const { cfg, def } = this.get(name)
|
|
57
|
+
const res = await fetch(def.tokenUrl, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
61
|
+
Accept: 'application/json',
|
|
62
|
+
// GitHub's API rejects requests without a User-Agent (HTTP 403); harmless for others.
|
|
63
|
+
'User-Agent': 'nixxie-cms-oauth',
|
|
64
|
+
},
|
|
65
|
+
body: new URLSearchParams({
|
|
66
|
+
grant_type: 'authorization_code',
|
|
67
|
+
code,
|
|
68
|
+
client_id: cfg.clientId,
|
|
69
|
+
client_secret: cfg.clientSecret,
|
|
70
|
+
redirect_uri: cfg.redirectUri,
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
if (!res.ok) throw new Error(`OAuth token exchange failed (${res.status}): ${await res.text()}`)
|
|
74
|
+
const data: any = await res.json()
|
|
75
|
+
if (data.error) throw new Error(`OAuth token exchange error: ${data.error_description ?? data.error}`)
|
|
76
|
+
return {
|
|
77
|
+
accessToken: data.access_token,
|
|
78
|
+
refreshToken: data.refresh_token,
|
|
79
|
+
expiresIn: data.expires_in,
|
|
80
|
+
tokenType: data.token_type,
|
|
81
|
+
scope: data.scope,
|
|
82
|
+
idToken: data.id_token,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Fetch the normalised user profile using an access token. */
|
|
87
|
+
async fetchProfile(name: string, tokens: OAuthTokens): Promise<OAuthProfile> {
|
|
88
|
+
const { def } = this.get(name)
|
|
89
|
+
const res = await fetch(def.userInfoUrl, {
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: `Bearer ${tokens.accessToken}`,
|
|
92
|
+
Accept: 'application/json',
|
|
93
|
+
// GitHub's API rejects requests without a User-Agent (HTTP 403); harmless for others.
|
|
94
|
+
'User-Agent': 'nixxie-cms-oauth',
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
if (!res.ok) throw new Error(`OAuth userinfo failed (${res.status}): ${await res.text()}`)
|
|
98
|
+
return def.profile(await res.json())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Convenience: exchange the code and fetch the profile in one call. */
|
|
102
|
+
async callback(name: string, code: string): Promise<{ tokens: OAuthTokens; profile: OAuthProfile }> {
|
|
103
|
+
const tokens = await this.exchangeCode(name, code)
|
|
104
|
+
const profile = await this.fetchProfile(name, tokens)
|
|
105
|
+
return { tokens, profile }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createOAuth(config: OAuthConfig): OAuth {
|
|
110
|
+
return new OAuth(config)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { builtInProviders as providers }
|
|
114
|
+
export type {
|
|
115
|
+
OAuthConfig,
|
|
116
|
+
OAuthProviderConfig,
|
|
117
|
+
OAuthProviderDefinition,
|
|
118
|
+
OAuthProviderName,
|
|
119
|
+
OAuthProfile,
|
|
120
|
+
OAuthTokens,
|
|
121
|
+
} from './types'
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { OAuthProviderDefinition, OAuthProviderName } from './types'
|
|
2
|
+
|
|
3
|
+
export const providers: Record<OAuthProviderName, OAuthProviderDefinition> = {
|
|
4
|
+
google: {
|
|
5
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
6
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
7
|
+
userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
8
|
+
defaultScopes: ['openid', 'email', 'profile'],
|
|
9
|
+
profile: raw => ({
|
|
10
|
+
id: raw.sub,
|
|
11
|
+
email: raw.email,
|
|
12
|
+
name: raw.name,
|
|
13
|
+
avatarUrl: raw.picture,
|
|
14
|
+
raw,
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
github: {
|
|
18
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
19
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
20
|
+
userInfoUrl: 'https://api.github.com/user',
|
|
21
|
+
defaultScopes: ['read:user', 'user:email'],
|
|
22
|
+
profile: raw => ({
|
|
23
|
+
id: String(raw.id),
|
|
24
|
+
email: raw.email,
|
|
25
|
+
name: raw.name ?? raw.login,
|
|
26
|
+
avatarUrl: raw.avatar_url,
|
|
27
|
+
raw,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
gitlab: {
|
|
31
|
+
authorizationUrl: 'https://gitlab.com/oauth/authorize',
|
|
32
|
+
tokenUrl: 'https://gitlab.com/oauth/token',
|
|
33
|
+
userInfoUrl: 'https://gitlab.com/api/v4/user',
|
|
34
|
+
defaultScopes: ['read_user'],
|
|
35
|
+
profile: raw => ({
|
|
36
|
+
id: String(raw.id),
|
|
37
|
+
email: raw.email,
|
|
38
|
+
name: raw.name ?? raw.username,
|
|
39
|
+
avatarUrl: raw.avatar_url,
|
|
40
|
+
raw,
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
discord: {
|
|
44
|
+
authorizationUrl: 'https://discord.com/oauth2/authorize',
|
|
45
|
+
tokenUrl: 'https://discord.com/api/oauth2/token',
|
|
46
|
+
userInfoUrl: 'https://discord.com/api/users/@me',
|
|
47
|
+
defaultScopes: ['identify', 'email'],
|
|
48
|
+
profile: raw => ({
|
|
49
|
+
id: raw.id,
|
|
50
|
+
email: raw.email,
|
|
51
|
+
name: raw.global_name ?? raw.username,
|
|
52
|
+
avatarUrl: raw.avatar
|
|
53
|
+
? `https://cdn.discordapp.com/avatars/${raw.id}/${raw.avatar}.png`
|
|
54
|
+
: undefined,
|
|
55
|
+
raw,
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
microsoft: {
|
|
59
|
+
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
|
60
|
+
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
|
61
|
+
userInfoUrl: 'https://graph.microsoft.com/oidc/userinfo',
|
|
62
|
+
defaultScopes: ['openid', 'email', 'profile'],
|
|
63
|
+
profile: raw => ({
|
|
64
|
+
id: raw.sub,
|
|
65
|
+
email: raw.email,
|
|
66
|
+
name: raw.name,
|
|
67
|
+
avatarUrl: raw.picture,
|
|
68
|
+
raw,
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
facebook: {
|
|
72
|
+
authorizationUrl: 'https://www.facebook.com/v19.0/dialog/oauth',
|
|
73
|
+
tokenUrl: 'https://graph.facebook.com/v19.0/oauth/access_token',
|
|
74
|
+
userInfoUrl: 'https://graph.facebook.com/me?fields=id,name,email,picture',
|
|
75
|
+
defaultScopes: ['email', 'public_profile'],
|
|
76
|
+
profile: raw => ({
|
|
77
|
+
id: raw.id,
|
|
78
|
+
email: raw.email,
|
|
79
|
+
name: raw.name,
|
|
80
|
+
avatarUrl: raw.picture?.data?.url,
|
|
81
|
+
raw,
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type OAuthProviderName =
|
|
2
|
+
| 'google'
|
|
3
|
+
| 'github'
|
|
4
|
+
| 'gitlab'
|
|
5
|
+
| 'discord'
|
|
6
|
+
| 'microsoft'
|
|
7
|
+
| 'facebook'
|
|
8
|
+
|
|
9
|
+
/** Endpoints + behaviour for an OAuth 2.0 / OpenID Connect provider. */
|
|
10
|
+
export type OAuthProviderDefinition = {
|
|
11
|
+
/** Authorization endpoint (where the user is redirected to consent). */
|
|
12
|
+
authorizationUrl: string
|
|
13
|
+
/** Token endpoint (where the auth code is exchanged for tokens). */
|
|
14
|
+
tokenUrl: string
|
|
15
|
+
/** Userinfo endpoint used to fetch the profile. */
|
|
16
|
+
userInfoUrl: string
|
|
17
|
+
/** Default scopes requested when the caller doesn't override them. */
|
|
18
|
+
defaultScopes: string[]
|
|
19
|
+
/** Normalise the raw userinfo response into a common profile shape. */
|
|
20
|
+
profile: (raw: any) => OAuthProfile
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type OAuthProfile = {
|
|
24
|
+
/** Stable provider-specific user id. */
|
|
25
|
+
id: string
|
|
26
|
+
email?: string
|
|
27
|
+
name?: string
|
|
28
|
+
avatarUrl?: string
|
|
29
|
+
/** The untouched provider response. */
|
|
30
|
+
raw: any
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type OAuthTokens = {
|
|
34
|
+
accessToken: string
|
|
35
|
+
refreshToken?: string
|
|
36
|
+
expiresIn?: number
|
|
37
|
+
tokenType?: string
|
|
38
|
+
scope?: string
|
|
39
|
+
idToken?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type OAuthProviderConfig = {
|
|
43
|
+
/** Built-in provider name, or omit and supply `definition` for a custom provider. */
|
|
44
|
+
provider?: OAuthProviderName
|
|
45
|
+
/** Custom provider definition (overrides `provider`). */
|
|
46
|
+
definition?: OAuthProviderDefinition
|
|
47
|
+
clientId: string
|
|
48
|
+
clientSecret: string
|
|
49
|
+
/** Redirect/callback URL registered with the provider. */
|
|
50
|
+
redirectUri: string
|
|
51
|
+
/** Override the provider's default scopes. */
|
|
52
|
+
scopes?: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type OAuthConfig = {
|
|
56
|
+
/** Map of named providers, keyed by however you want to reference them in routes. */
|
|
57
|
+
providers: Record<string, OAuthProviderConfig>
|
|
58
|
+
}
|