@draftlab/auth 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/{node.js → node.mjs} +2 -4
- package/dist/{allow.js → allow.mjs} +1 -1
- package/dist/{client.d.ts → client.d.mts} +47 -4
- package/dist/{client.js → client.mjs} +81 -10
- package/dist/{core.d.ts → core.d.mts} +10 -10
- package/dist/{core.js → core.mjs} +104 -56
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +3 -0
- package/dist/{keys.d.ts → keys.d.mts} +1 -1
- package/dist/{keys.js → keys.mjs} +6 -8
- package/dist/{pkce.js → pkce.mjs} +5 -10
- package/dist/plugin/{builder.d.ts → builder.d.mts} +1 -1
- package/dist/plugin/{manager.d.ts → manager.d.mts} +2 -2
- package/dist/plugin/{manager.js → manager.mjs} +1 -1
- package/dist/plugin/{plugin.d.ts → plugin.d.mts} +1 -1
- package/dist/plugin/{types.d.ts → types.d.mts} +1 -1
- package/dist/provider/apple.d.mts +105 -0
- package/dist/provider/apple.mjs +151 -0
- package/dist/provider/{code.d.ts → code.d.mts} +1 -1
- package/dist/provider/{code.js → code.mjs} +2 -3
- package/dist/provider/{discord.d.ts → discord.d.mts} +2 -2
- package/dist/provider/{discord.js → discord.mjs} +59 -1
- package/dist/provider/{facebook.d.ts → facebook.d.mts} +2 -2
- package/dist/provider/{facebook.js → facebook.mjs} +57 -1
- package/dist/provider/{github.d.ts → github.d.mts} +2 -2
- package/dist/provider/{github.js → github.mjs} +79 -1
- package/dist/provider/gitlab.d.mts +100 -0
- package/dist/provider/gitlab.mjs +128 -0
- package/dist/provider/{google.d.ts → google.d.mts} +2 -2
- package/dist/provider/{google.js → google.mjs} +45 -1
- package/dist/provider/{linkedin.d.ts → linkedin.d.mts} +2 -2
- package/dist/provider/{linkedin.js → linkedin.mjs} +57 -1
- package/dist/provider/{magiclink.d.ts → magiclink.d.mts} +1 -1
- package/dist/provider/{magiclink.js → magiclink.mjs} +4 -6
- package/dist/provider/{microsoft.d.ts → microsoft.d.mts} +2 -2
- package/dist/provider/{microsoft.js → microsoft.mjs} +68 -1
- package/dist/provider/{oauth2.d.ts → oauth2.d.mts} +1 -1
- package/dist/provider/{oauth2.js → oauth2.mjs} +4 -4
- package/dist/provider/{passkey.d.ts → passkey.d.mts} +1 -1
- package/dist/provider/{passkey.js → passkey.mjs} +8 -13
- package/dist/provider/{password.d.ts → password.d.mts} +1 -1
- package/dist/provider/{password.js → password.mjs} +31 -44
- package/dist/provider/{provider.d.ts → provider.d.mts} +1 -1
- package/dist/provider/reddit.d.mts +101 -0
- package/dist/provider/reddit.mjs +114 -0
- package/dist/provider/slack.d.mts +108 -0
- package/dist/provider/slack.mjs +125 -0
- package/dist/provider/spotify.d.mts +107 -0
- package/dist/provider/spotify.mjs +122 -0
- package/dist/provider/{totp.d.ts → totp.d.mts} +1 -1
- package/dist/provider/{totp.js → totp.mjs} +51 -14
- package/dist/provider/twitch.d.mts +102 -0
- package/dist/provider/twitch.mjs +118 -0
- package/dist/{random.js → random.mjs} +1 -2
- package/dist/revocation.d.mts +55 -0
- package/dist/revocation.mjs +63 -0
- package/dist/storage/{memory.d.ts → memory.d.mts} +1 -1
- package/dist/storage/{memory.js → memory.mjs} +3 -5
- package/dist/storage/{storage.d.ts → storage.d.mts} +27 -10
- package/dist/storage/storage.mjs +104 -0
- package/dist/storage/{turso.d.ts → turso.d.mts} +1 -1
- package/dist/storage/{turso.js → turso.mjs} +1 -1
- package/dist/storage/{unstorage.d.ts → unstorage.d.mts} +1 -1
- package/dist/storage/{unstorage.js → unstorage.mjs} +11 -4
- package/dist/{subject.d.ts → subject.d.mts} +1 -1
- package/dist/ui/{base.d.ts → base.d.mts} +1 -1
- package/dist/ui/{base.js → base.mjs} +1 -1
- package/dist/ui/{code.d.ts → code.d.mts} +1 -1
- package/dist/ui/{code.js → code.mjs} +3 -4
- package/dist/ui/{magiclink.d.ts → magiclink.d.mts} +1 -1
- package/dist/ui/{magiclink.js → magiclink.mjs} +3 -4
- package/dist/ui/{passkey.d.ts → passkey.d.mts} +1 -1
- package/dist/ui/{passkey.js → passkey.mjs} +2 -2
- package/dist/ui/{password.d.ts → password.d.mts} +1 -1
- package/dist/ui/{password.js → password.mjs} +3 -4
- package/dist/ui/{select.d.ts → select.d.mts} +1 -1
- package/dist/ui/{select.js → select.mjs} +2 -2
- package/dist/ui/{totp.d.ts → totp.d.mts} +1 -1
- package/dist/ui/{totp.js → totp.mjs} +2 -2
- package/dist/{util.js → util.mjs} +2 -5
- package/package.json +17 -16
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -3
- package/dist/storage/storage.js +0 -62
- /package/dist/adapters/{node.d.ts → node.d.mts} +0 -0
- /package/dist/{allow.d.ts → allow.d.mts} +0 -0
- /package/dist/{error.d.ts → error.d.mts} +0 -0
- /package/dist/{error.js → error.mjs} +0 -0
- /package/dist/{pkce.d.ts → pkce.d.mts} +0 -0
- /package/dist/plugin/{builder.js → builder.mjs} +0 -0
- /package/dist/plugin/{plugin.js → plugin.mjs} +0 -0
- /package/dist/plugin/{types.js → types.mjs} +0 -0
- /package/dist/provider/{provider.js → provider.mjs} +0 -0
- /package/dist/{random.d.ts → random.d.mts} +0 -0
- /package/dist/{subject.js → subject.mjs} +0 -0
- /package/dist/themes/{theme.d.ts → theme.d.mts} +0 -0
- /package/dist/themes/{theme.js → theme.mjs} +0 -0
- /package/dist/{types.d.ts → types.d.mts} +0 -0
- /package/dist/{types.js → types.mjs} +0 -0
- /package/dist/ui/{form.d.ts → form.d.mts} +0 -0
- /package/dist/ui/{form.js → form.mjs} +0 -0
- /package/dist/ui/{icon.d.ts → icon.d.mts} +0 -0
- /package/dist/ui/{icon.js → icon.mjs} +0 -0
- /package/dist/{util.d.ts → util.d.mts} +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Oauth2Provider } from "./oauth2.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/provider/gitlab.ts
|
|
4
|
+
/**
|
|
5
|
+
* GitLab authentication provider for Draft Auth.
|
|
6
|
+
* Implements OAuth 2.0 flow for authenticating users with their GitLab accounts.
|
|
7
|
+
*
|
|
8
|
+
* ## Quick Setup
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { GitlabProvider } from "@draftlab/auth/provider/gitlab"
|
|
12
|
+
*
|
|
13
|
+
* export default issuer({
|
|
14
|
+
* providers: {
|
|
15
|
+
* gitlab: GitlabProvider({
|
|
16
|
+
* clientID: process.env.GITLAB_CLIENT_ID,
|
|
17
|
+
* clientSecret: process.env.GITLAB_CLIENT_SECRET,
|
|
18
|
+
* scopes: ["read_user", "read_api"]
|
|
19
|
+
* })
|
|
20
|
+
* }
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* ## Common Scopes
|
|
25
|
+
*
|
|
26
|
+
* - `read_user` - Access user profile
|
|
27
|
+
* - `read_api` - Read-access to the API
|
|
28
|
+
* - `read_repository` - Access to project repositories
|
|
29
|
+
* - `write_repository` - Write access to repositories
|
|
30
|
+
* - `api` - Full API access
|
|
31
|
+
* - `read_user_email` - Access user email
|
|
32
|
+
*
|
|
33
|
+
* ## Self-Hosted GitLab
|
|
34
|
+
*
|
|
35
|
+
* For self-hosted GitLab instances, you can override the endpoint URLs:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const selfHostedGitlab = Oauth2Provider({
|
|
39
|
+
* clientID: process.env.GITLAB_CLIENT_ID,
|
|
40
|
+
* clientSecret: process.env.GITLAB_CLIENT_SECRET,
|
|
41
|
+
* scopes: ["read_user"],
|
|
42
|
+
* type: "gitlab",
|
|
43
|
+
* endpoint: {
|
|
44
|
+
* authorization: "https://your-gitlab.com/oauth/authorize",
|
|
45
|
+
* token: "https://your-gitlab.com/oauth/token"
|
|
46
|
+
* }
|
|
47
|
+
* })
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* ## User Data Access
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* success: async (ctx, value) => {
|
|
54
|
+
* if (value.provider === "gitlab") {
|
|
55
|
+
* const accessToken = value.tokenset.access
|
|
56
|
+
*
|
|
57
|
+
* // Fetch user information
|
|
58
|
+
* const userResponse = await fetch('https://gitlab.com/api/v4/user', {
|
|
59
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
60
|
+
* })
|
|
61
|
+
* const user = await userResponse.json()
|
|
62
|
+
*
|
|
63
|
+
* // User info: id, username, email, name, avatar_url
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @packageDocumentation
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Creates a GitLab OAuth 2.0 authentication provider.
|
|
72
|
+
* Allows users to authenticate using their GitLab accounts (gitlab.com or self-hosted).
|
|
73
|
+
*
|
|
74
|
+
* @param config - GitLab OAuth 2.0 configuration
|
|
75
|
+
* @returns OAuth 2.0 provider configured for GitLab
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* // Basic GitLab.com authentication
|
|
80
|
+
* const basicGitlab = GitlabProvider({
|
|
81
|
+
* clientID: process.env.GITLAB_CLIENT_ID,
|
|
82
|
+
* clientSecret: process.env.GITLAB_CLIENT_SECRET
|
|
83
|
+
* })
|
|
84
|
+
*
|
|
85
|
+
* // GitLab with read access
|
|
86
|
+
* const gitlabWithRead = GitlabProvider({
|
|
87
|
+
* clientID: process.env.GITLAB_CLIENT_ID,
|
|
88
|
+
* clientSecret: process.env.GITLAB_CLIENT_SECRET,
|
|
89
|
+
* scopes: ["read_user", "read_api"]
|
|
90
|
+
* })
|
|
91
|
+
*
|
|
92
|
+
* // Using the access token to fetch user data
|
|
93
|
+
* export default issuer({
|
|
94
|
+
* providers: { gitlab: gitlabWithRead },
|
|
95
|
+
* success: async (ctx, value) => {
|
|
96
|
+
* if (value.provider === "gitlab") {
|
|
97
|
+
* const token = value.tokenset.access
|
|
98
|
+
*
|
|
99
|
+
* const userRes = await fetch('https://gitlab.com/api/v4/user', {
|
|
100
|
+
* headers: { Authorization: `Bearer ${token}` }
|
|
101
|
+
* })
|
|
102
|
+
* const user = await userRes.json()
|
|
103
|
+
*
|
|
104
|
+
* return ctx.subject("user", {
|
|
105
|
+
* gitlabId: user.id,
|
|
106
|
+
* username: user.username,
|
|
107
|
+
* email: user.email,
|
|
108
|
+
* name: user.name,
|
|
109
|
+
* avatar: user.avatar_url
|
|
110
|
+
* })
|
|
111
|
+
* }
|
|
112
|
+
* }
|
|
113
|
+
* })
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
const GitlabProvider = (config) => {
|
|
117
|
+
return Oauth2Provider({
|
|
118
|
+
...config,
|
|
119
|
+
type: "gitlab",
|
|
120
|
+
endpoint: {
|
|
121
|
+
authorization: "https://gitlab.com/oauth/authorize",
|
|
122
|
+
token: "https://gitlab.com/oauth/token"
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
export { GitlabProvider };
|
|
@@ -1,7 +1,51 @@
|
|
|
1
|
-
import { Oauth2Provider } from "./oauth2.
|
|
1
|
+
import { Oauth2Provider } from "./oauth2.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/provider/google.ts
|
|
4
4
|
/**
|
|
5
|
+
* Google OAuth 2.0 authentication provider for Draft Auth.
|
|
6
|
+
* Provides access tokens for calling Google APIs on behalf of users.
|
|
7
|
+
*
|
|
8
|
+
* ## Quick Setup
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { GoogleProvider } from "@draftlab/auth/provider/google"
|
|
12
|
+
*
|
|
13
|
+
* export default issuer({
|
|
14
|
+
* providers: {
|
|
15
|
+
* google: GoogleProvider({
|
|
16
|
+
* clientID: process.env.GOOGLE_CLIENT_ID,
|
|
17
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
18
|
+
* scopes: ["profile", "email", "https://www.googleapis.com/auth/calendar.readonly"]
|
|
19
|
+
* })
|
|
20
|
+
* }
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* ## Configuration Options
|
|
25
|
+
*
|
|
26
|
+
* - Access tokens for Google API calls
|
|
27
|
+
* - Refresh tokens for long-lived access
|
|
28
|
+
* - Support for offline access
|
|
29
|
+
* - Custom scopes for specific Google services
|
|
30
|
+
*
|
|
31
|
+
* ## User Data Access
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* success: async (ctx, value) => {
|
|
35
|
+
* if (value.provider === "google") {
|
|
36
|
+
* // Access token for API calls: value.tokenset.access
|
|
37
|
+
* // Refresh token (if requested): value.tokenset.refresh
|
|
38
|
+
* // Use the access token to call Google APIs
|
|
39
|
+
* const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
40
|
+
* headers: { Authorization: `Bearer ${value.tokenset.access}` }
|
|
41
|
+
* })
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @packageDocumentation
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
5
49
|
* Creates a Google OAuth 2.0 authentication provider.
|
|
6
50
|
* Use this when you need access tokens to call Google APIs on behalf of the user.
|
|
7
51
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Provider } from "./provider.
|
|
2
|
-
import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.
|
|
1
|
+
import { Provider } from "./provider.mjs";
|
|
2
|
+
import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/provider/linkedin.d.ts
|
|
5
5
|
|
|
@@ -1,7 +1,63 @@
|
|
|
1
|
-
import { Oauth2Provider } from "./oauth2.
|
|
1
|
+
import { Oauth2Provider } from "./oauth2.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/provider/linkedin.ts
|
|
4
4
|
/**
|
|
5
|
+
* LinkedIn OAuth 2.0 authentication provider for Draft Auth.
|
|
6
|
+
* Provides access tokens for calling LinkedIn APIs on behalf of users.
|
|
7
|
+
*
|
|
8
|
+
* ## Quick Setup
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { LinkedInProvider } from "@draftlab/auth/provider/linkedin"
|
|
12
|
+
*
|
|
13
|
+
* export default issuer({
|
|
14
|
+
* providers: {
|
|
15
|
+
* linkedin: LinkedInProvider({
|
|
16
|
+
* clientID: process.env.LINKEDIN_CLIENT_ID,
|
|
17
|
+
* clientSecret: process.env.LINKEDIN_CLIENT_SECRET,
|
|
18
|
+
* scopes: ["r_liteprofile", "r_emailaddress", "w_member_social"]
|
|
19
|
+
* })
|
|
20
|
+
* }
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* ## Common Scopes
|
|
25
|
+
*
|
|
26
|
+
* - `r_liteprofile` - Access to basic profile information
|
|
27
|
+
* - `r_emailaddress` - Access to user's email address
|
|
28
|
+
* - `r_basicprofile` - Access to full profile information (deprecated)
|
|
29
|
+
* - `w_member_social` - Share content on behalf of user
|
|
30
|
+
* - `r_organization_social` - Access to organization social content
|
|
31
|
+
* - `rw_organization_admin` - Manage organization pages
|
|
32
|
+
*
|
|
33
|
+
* ## User Data Access
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* success: async (ctx, value) => {
|
|
37
|
+
* if (value.provider === "linkedin") {
|
|
38
|
+
* const accessToken = value.tokenset.access
|
|
39
|
+
*
|
|
40
|
+
* // Fetch user profile
|
|
41
|
+
* const profileResponse = await fetch('https://api.linkedin.com/v2/people/~', {
|
|
42
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
43
|
+
* })
|
|
44
|
+
* const profile = await profileResponse.json()
|
|
45
|
+
*
|
|
46
|
+
* // Fetch user email (requires r_emailaddress scope)
|
|
47
|
+
* const emailResponse = await fetch('https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))', {
|
|
48
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
49
|
+
* })
|
|
50
|
+
* const emailData = await emailResponse.json()
|
|
51
|
+
*
|
|
52
|
+
* // User info: profile.localizedFirstName + profile.localizedLastName
|
|
53
|
+
* // Email: emailData.elements[0]['handle~'].emailAddress
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @packageDocumentation
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
5
61
|
* Creates a LinkedIn OAuth 2.0 authentication provider.
|
|
6
62
|
* Use this when you need access tokens to call LinkedIn APIs on behalf of the user.
|
|
7
63
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateUnbiasedDigits, timingSafeCompare } from "../random.
|
|
1
|
+
import { generateUnbiasedDigits, timingSafeCompare } from "../random.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/provider/magiclink.ts
|
|
4
4
|
/**
|
|
@@ -41,8 +41,7 @@ const MagicLinkProvider = (config) => {
|
|
|
41
41
|
const action = formData.get("action")?.toString();
|
|
42
42
|
if (action === "request" || action === "resend") {
|
|
43
43
|
const token = generateToken();
|
|
44
|
-
const
|
|
45
|
-
const { action: _,...claims } = formEntries;
|
|
44
|
+
const { action: _, ...claims } = Object.fromEntries(formData);
|
|
46
45
|
const baseUrl = new URL(c.request.url).origin;
|
|
47
46
|
const magicUrl = new URL(`/auth/${ctx.name}/verify`, baseUrl);
|
|
48
47
|
magicUrl.searchParams.set("token", token);
|
|
@@ -69,13 +68,12 @@ const MagicLinkProvider = (config) => {
|
|
|
69
68
|
if (!timingSafeCompare(storedState.token, token)) return transition(c, { type: "start" }, void 0, { type: "invalid_link" });
|
|
70
69
|
const urlClaims = {};
|
|
71
70
|
for (const [key, value] of url.searchParams) if (key !== "token" && value) urlClaims[key] = value;
|
|
72
|
-
|
|
71
|
+
if (!Object.keys(storedState.claims).every((key) => {
|
|
73
72
|
const urlValue = urlClaims[key];
|
|
74
73
|
const storedValue = storedState.claims[key];
|
|
75
74
|
if (!urlValue || !storedValue) return false;
|
|
76
75
|
return timingSafeCompare(storedValue, urlValue);
|
|
77
|
-
});
|
|
78
|
-
if (!claimsMatch) return transition(c, { type: "start" }, void 0, { type: "invalid_link" });
|
|
76
|
+
})) return transition(c, { type: "start" }, void 0, { type: "invalid_link" });
|
|
79
77
|
await ctx.unset(c, "provider");
|
|
80
78
|
return await ctx.success(c, { claims: storedState.claims });
|
|
81
79
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Provider } from "./provider.
|
|
2
|
-
import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.
|
|
1
|
+
import { Provider } from "./provider.mjs";
|
|
2
|
+
import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/provider/microsoft.d.ts
|
|
5
5
|
|
|
@@ -1,7 +1,74 @@
|
|
|
1
|
-
import { Oauth2Provider } from "./oauth2.
|
|
1
|
+
import { Oauth2Provider } from "./oauth2.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/provider/microsoft.ts
|
|
4
4
|
/**
|
|
5
|
+
* Microsoft OAuth 2.0 authentication provider for Draft Auth.
|
|
6
|
+
* Supports Microsoft personal accounts, work accounts, and Azure AD.
|
|
7
|
+
* Provides access tokens for calling Microsoft Graph APIs on behalf of users.
|
|
8
|
+
*
|
|
9
|
+
* ## Quick Setup
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { MicrosoftProvider } from "@draftlab/auth/provider/microsoft"
|
|
13
|
+
*
|
|
14
|
+
* export default issuer({
|
|
15
|
+
* providers: {
|
|
16
|
+
* microsoft: MicrosoftProvider({
|
|
17
|
+
* tenant: "common", // or specific tenant ID
|
|
18
|
+
* clientID: process.env.MICROSOFT_CLIENT_ID,
|
|
19
|
+
* clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
|
20
|
+
* scopes: ["openid", "profile", "email", "User.Read"]
|
|
21
|
+
* })
|
|
22
|
+
* }
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ## Tenant Configuration
|
|
27
|
+
*
|
|
28
|
+
* - `common` - Both personal and work/school accounts
|
|
29
|
+
* - `organizations` - Work/school accounts only
|
|
30
|
+
* - `consumers` - Personal Microsoft accounts only
|
|
31
|
+
* - `{tenant-id}` - Specific Azure AD tenant only
|
|
32
|
+
*
|
|
33
|
+
* ## Common Scopes
|
|
34
|
+
*
|
|
35
|
+
* - `openid` - Basic OpenID Connect sign-in
|
|
36
|
+
* - `profile` - User's basic profile information
|
|
37
|
+
* - `email` - User's email address
|
|
38
|
+
* - `User.Read` - Read user's profile via Microsoft Graph
|
|
39
|
+
* - `Mail.Read` - Read user's mail
|
|
40
|
+
* - `Calendars.Read` - Read user's calendars
|
|
41
|
+
* - `Files.Read` - Read user's files in OneDrive
|
|
42
|
+
* - `Sites.Read.All` - Read SharePoint sites
|
|
43
|
+
* - `Directory.Read.All` - Read directory data (requires admin consent)
|
|
44
|
+
*
|
|
45
|
+
* ## User Data Access
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* success: async (ctx, value) => {
|
|
49
|
+
* if (value.provider === "microsoft") {
|
|
50
|
+
* const accessToken = value.tokenset.access
|
|
51
|
+
*
|
|
52
|
+
* // Fetch user profile via Microsoft Graph
|
|
53
|
+
* const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
|
|
54
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
55
|
+
* })
|
|
56
|
+
* const user = await userResponse.json()
|
|
57
|
+
*
|
|
58
|
+
* // Fetch user photo (requires User.Read scope)
|
|
59
|
+
* const photoResponse = await fetch('https://graph.microsoft.com/v1.0/me/photo/$value', {
|
|
60
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
61
|
+
* })
|
|
62
|
+
* const photoBlob = await photoResponse.blob()
|
|
63
|
+
*
|
|
64
|
+
* // User info: user.displayName, user.mail, user.userPrincipalName
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @packageDocumentation
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
5
72
|
* Creates a Microsoft OAuth 2.0 authentication provider.
|
|
6
73
|
* Use this when you need access tokens to call Microsoft Graph APIs on behalf of the user.
|
|
7
74
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getRelativeUrl } from "../util.
|
|
2
|
-
import { OauthError } from "../error.
|
|
3
|
-
import { generatePKCE } from "../pkce.
|
|
4
|
-
import { generateSecureToken, timingSafeCompare } from "../random.
|
|
1
|
+
import { getRelativeUrl } from "../util.mjs";
|
|
2
|
+
import { OauthError } from "../error.mjs";
|
|
3
|
+
import { generatePKCE } from "../pkce.mjs";
|
|
4
|
+
import { generateSecureToken, timingSafeCompare } from "../random.mjs";
|
|
5
5
|
|
|
6
6
|
//#region src/provider/oauth2.ts
|
|
7
7
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Storage } from "../storage/storage.
|
|
1
|
+
import { Storage } from "../storage/storage.mjs";
|
|
2
2
|
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
|
|
3
3
|
|
|
4
4
|
//#region src/provider/passkey.ts
|
|
@@ -12,8 +12,7 @@ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthe
|
|
|
12
12
|
const uint8ArrayToBase64Url = (bytes) => {
|
|
13
13
|
let str = "";
|
|
14
14
|
for (const charCode of bytes) str += String.fromCharCode(charCode);
|
|
15
|
-
|
|
16
|
-
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
15
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
17
16
|
};
|
|
18
17
|
/**
|
|
19
18
|
* Converts a Base64URL encoded string back to a Uint8Array.
|
|
@@ -150,8 +149,7 @@ const PasskeyProvider = (config) => {
|
|
|
150
149
|
const username = c.query("username") || userId;
|
|
151
150
|
let user = await getStoredUserById(userId);
|
|
152
151
|
if (config.userCanRegisterPasskey) {
|
|
153
|
-
|
|
154
|
-
if (!isAllowed) return c.json({ error: copy.error_user_not_allowed }, { status: 403 });
|
|
152
|
+
if (!await config.userCanRegisterPasskey(userId, c.request)) return c.json({ error: copy.error_user_not_allowed }, { status: 403 });
|
|
155
153
|
}
|
|
156
154
|
if (!user) {
|
|
157
155
|
user = {
|
|
@@ -252,14 +250,12 @@ const PasskeyProvider = (config) => {
|
|
|
252
250
|
if (!rpID) return c.json({ error: "RP ID for authentication is required." }, { status: 400 });
|
|
253
251
|
const userForAuth = await getStoredUserById(userId);
|
|
254
252
|
if (!userForAuth) return c.json({ error: "User not found for authentication." }, { status: 404 });
|
|
255
|
-
const userPasskeys = await getStoredUserPasskeys(userForAuth.id);
|
|
256
|
-
const allowCredentialsList = userPasskeys.map((pk) => ({
|
|
257
|
-
id: pk.id,
|
|
258
|
-
transports: pk.transports
|
|
259
|
-
}));
|
|
260
253
|
const authOptions = await generateAuthenticationOptions({
|
|
261
254
|
rpID,
|
|
262
|
-
allowCredentials:
|
|
255
|
+
allowCredentials: (await getStoredUserPasskeys(userForAuth.id)).map((pk) => ({
|
|
256
|
+
id: pk.id,
|
|
257
|
+
transports: pk.transports
|
|
258
|
+
})),
|
|
263
259
|
userVerification: authenticatorSelection?.userVerification ?? "preferred",
|
|
264
260
|
timeout
|
|
265
261
|
});
|
|
@@ -290,7 +286,7 @@ const PasskeyProvider = (config) => {
|
|
|
290
286
|
if (!publicKey || typeof counter !== "number" || !transports) return c.json({ error: "Passkey not found for authentication." }, { status: 400 });
|
|
291
287
|
const challenge = authOptions.challenge;
|
|
292
288
|
if (!challenge) return c.json({ error: "Authentication challenge not found." }, { status: 400 });
|
|
293
|
-
const
|
|
289
|
+
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
|
|
294
290
|
response: body,
|
|
295
291
|
expectedChallenge: challenge,
|
|
296
292
|
expectedOrigin: origin || "",
|
|
@@ -302,7 +298,6 @@ const PasskeyProvider = (config) => {
|
|
|
302
298
|
transports
|
|
303
299
|
}
|
|
304
300
|
});
|
|
305
|
-
const { verified, authenticationInfo } = verification;
|
|
306
301
|
if (verified) {
|
|
307
302
|
await updateStoredPasskeyCounter(user.id, passkey.id, authenticationInfo.newCounter);
|
|
308
303
|
return ctx.success(c, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getRelativeUrl } from "../util.
|
|
2
|
-
import { UnknownStateError } from "../error.
|
|
3
|
-
import { generateUnbiasedDigits, timingSafeCompare } from "../random.
|
|
4
|
-
import { Storage } from "../storage/storage.
|
|
1
|
+
import { getRelativeUrl } from "../util.mjs";
|
|
2
|
+
import { UnknownStateError } from "../error.mjs";
|
|
3
|
+
import { generateUnbiasedDigits, timingSafeCompare } from "../random.mjs";
|
|
4
|
+
import { Storage } from "../storage/storage.mjs";
|
|
5
5
|
import * as jose from "jose";
|
|
6
6
|
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
7
7
|
import { TextEncoder } from "node:util";
|
|
@@ -123,12 +123,11 @@ const PasswordProvider = (config) => {
|
|
|
123
123
|
message: validationError
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
if (await Storage.get(ctx.storage, [
|
|
127
127
|
"email",
|
|
128
128
|
email,
|
|
129
129
|
"password"
|
|
130
|
-
]);
|
|
131
|
-
if (existingUser) return transition(provider, { type: "email_taken" });
|
|
130
|
+
])) return transition(provider, { type: "email_taken" });
|
|
132
131
|
const code = generateCode();
|
|
133
132
|
await config.sendCode(email, code);
|
|
134
133
|
return transition({
|
|
@@ -151,12 +150,11 @@ const PasswordProvider = (config) => {
|
|
|
151
150
|
if (action === "verify" && provider.type === "code") {
|
|
152
151
|
const code = formData.get("code")?.toString();
|
|
153
152
|
if (!(code && timingSafeCompare(code, provider.code))) return transition(provider, { type: "invalid_code" });
|
|
154
|
-
|
|
153
|
+
if (await Storage.get(ctx.storage, [
|
|
155
154
|
"email",
|
|
156
155
|
provider.email,
|
|
157
156
|
"password"
|
|
158
|
-
]);
|
|
159
|
-
if (existingUser) return transition({ type: "start" }, { type: "email_taken" });
|
|
157
|
+
])) return transition({ type: "start" }, { type: "email_taken" });
|
|
160
158
|
await Storage.set(ctx.storage, [
|
|
161
159
|
"email",
|
|
162
160
|
provider.email,
|
|
@@ -170,10 +168,9 @@ const PasswordProvider = (config) => {
|
|
|
170
168
|
* GET /change - Display password change form
|
|
171
169
|
*/
|
|
172
170
|
routes.get("/change", async (c) => {
|
|
173
|
-
const redirect = c.query("redirect_uri") || getRelativeUrl(c, "/authorize");
|
|
174
171
|
const state = {
|
|
175
172
|
type: "start",
|
|
176
|
-
redirect
|
|
173
|
+
redirect: c.query("redirect_uri") || getRelativeUrl(c, "/authorize")
|
|
177
174
|
};
|
|
178
175
|
await ctx.set(c, "provider", 3600 * 24, state);
|
|
179
176
|
return ctx.forward(c, await config.change(c.request, state));
|
|
@@ -215,12 +212,11 @@ const PasswordProvider = (config) => {
|
|
|
215
212
|
});
|
|
216
213
|
}
|
|
217
214
|
if (action === "update" && provider.type === "update") {
|
|
218
|
-
|
|
215
|
+
if (!await Storage.get(ctx.storage, [
|
|
219
216
|
"email",
|
|
220
217
|
provider.email,
|
|
221
218
|
"password"
|
|
222
|
-
]);
|
|
223
|
-
if (!existingPassword) return c.redirect(provider.redirect, 302);
|
|
219
|
+
])) return c.redirect(provider.redirect, 302);
|
|
224
220
|
const password = formData.get("password")?.toString();
|
|
225
221
|
const repeat = formData.get("repeat")?.toString();
|
|
226
222
|
if (!password) return transition(provider, { type: "invalid_password" });
|
|
@@ -274,8 +270,7 @@ const PBKDF2Hasher = (opts) => {
|
|
|
274
270
|
const iterations = opts?.iterations ?? 6e5;
|
|
275
271
|
return {
|
|
276
272
|
async hash(password) {
|
|
277
|
-
const
|
|
278
|
-
const passwordBytes = encoder.encode(password);
|
|
273
|
+
const passwordBytes = new TextEncoder().encode(password);
|
|
279
274
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
280
275
|
const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
|
|
281
276
|
const hashBuffer = await crypto.subtle.deriveBits({
|
|
@@ -284,17 +279,14 @@ const PBKDF2Hasher = (opts) => {
|
|
|
284
279
|
salt,
|
|
285
280
|
iterations
|
|
286
281
|
}, keyMaterial, 256);
|
|
287
|
-
const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
|
|
288
|
-
const saltBase64 = jose.base64url.encode(salt);
|
|
289
282
|
return {
|
|
290
|
-
hash:
|
|
291
|
-
salt:
|
|
283
|
+
hash: jose.base64url.encode(new Uint8Array(hashBuffer)),
|
|
284
|
+
salt: jose.base64url.encode(salt),
|
|
292
285
|
iterations
|
|
293
286
|
};
|
|
294
287
|
},
|
|
295
288
|
async verify(password, compare) {
|
|
296
|
-
const
|
|
297
|
-
const passwordBytes = encoder.encode(password);
|
|
289
|
+
const passwordBytes = new TextEncoder().encode(password);
|
|
298
290
|
const salt = jose.base64url.decode(compare.salt);
|
|
299
291
|
const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
|
|
300
292
|
const hashBuffer = await crypto.subtle.deriveBits({
|
|
@@ -303,8 +295,7 @@ const PBKDF2Hasher = (opts) => {
|
|
|
303
295
|
salt,
|
|
304
296
|
iterations: compare.iterations
|
|
305
297
|
}, keyMaterial, 256);
|
|
306
|
-
|
|
307
|
-
return timingSafeCompare(hashBase64, compare.hash);
|
|
298
|
+
return timingSafeCompare(jose.base64url.encode(new Uint8Array(hashBuffer)), compare.hash);
|
|
308
299
|
}
|
|
309
300
|
};
|
|
310
301
|
};
|
|
@@ -324,21 +315,18 @@ const ScryptHasher = (opts) => {
|
|
|
324
315
|
async hash(password) {
|
|
325
316
|
const salt = randomBytes(16);
|
|
326
317
|
const keyLength = 32;
|
|
327
|
-
const derivedKey = await new Promise((resolve, reject) => {
|
|
328
|
-
scrypt(password, salt, keyLength, {
|
|
329
|
-
N,
|
|
330
|
-
r,
|
|
331
|
-
p
|
|
332
|
-
}, (err, derivedKey$1) => {
|
|
333
|
-
if (err) reject(err);
|
|
334
|
-
else resolve(derivedKey$1);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
const hashBase64 = derivedKey.toString("base64");
|
|
338
|
-
const saltBase64 = salt.toString("base64");
|
|
339
318
|
return {
|
|
340
|
-
hash:
|
|
341
|
-
|
|
319
|
+
hash: (await new Promise((resolve, reject) => {
|
|
320
|
+
scrypt(password, salt, keyLength, {
|
|
321
|
+
N,
|
|
322
|
+
r,
|
|
323
|
+
p
|
|
324
|
+
}, (err, derivedKey) => {
|
|
325
|
+
if (err) reject(err);
|
|
326
|
+
else resolve(derivedKey);
|
|
327
|
+
});
|
|
328
|
+
})).toString("base64"),
|
|
329
|
+
salt: salt.toString("base64"),
|
|
342
330
|
N,
|
|
343
331
|
r,
|
|
344
332
|
p
|
|
@@ -347,17 +335,16 @@ const ScryptHasher = (opts) => {
|
|
|
347
335
|
async verify(password, compare) {
|
|
348
336
|
const salt = Buffer.from(compare.salt, "base64");
|
|
349
337
|
const keyLength = 32;
|
|
350
|
-
|
|
338
|
+
return timingSafeEqual(await new Promise((resolve, reject) => {
|
|
351
339
|
scrypt(password, salt, keyLength, {
|
|
352
340
|
N: compare.N,
|
|
353
341
|
r: compare.r,
|
|
354
342
|
p: compare.p
|
|
355
|
-
}, (err, derivedKey
|
|
343
|
+
}, (err, derivedKey) => {
|
|
356
344
|
if (err) reject(err);
|
|
357
|
-
else resolve(derivedKey
|
|
345
|
+
else resolve(derivedKey);
|
|
358
346
|
});
|
|
359
|
-
});
|
|
360
|
-
return timingSafeEqual(derivedKey, Buffer.from(compare.hash, "base64"));
|
|
347
|
+
}), Buffer.from(compare.hash, "base64"));
|
|
361
348
|
}
|
|
362
349
|
};
|
|
363
350
|
};
|