@draftlab/auth 0.8.0 → 0.10.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/client.mjs +4 -1
- package/dist/core.d.mts +2 -21
- package/dist/core.mjs +44 -3
- package/dist/provider/apple.d.mts +6 -0
- package/dist/provider/apple.mjs +13 -0
- package/dist/provider/discord.d.mts +6 -0
- package/dist/provider/discord.mjs +13 -0
- package/dist/provider/facebook.d.mts +6 -0
- package/dist/provider/facebook.mjs +13 -0
- package/dist/provider/github.mjs +7 -0
- package/dist/provider/gitlab.d.mts +6 -0
- package/dist/provider/gitlab.mjs +19 -0
- package/dist/provider/google.mjs +7 -0
- package/dist/provider/linkedin.d.mts +6 -0
- package/dist/provider/linkedin.mjs +13 -0
- package/dist/provider/microsoft.d.mts +6 -0
- package/dist/provider/microsoft.mjs +13 -0
- package/dist/provider/oauth2.mjs +10 -0
- package/dist/provider/reddit.d.mts +6 -0
- package/dist/provider/reddit.mjs +13 -0
- package/dist/provider/slack.d.mts +6 -0
- package/dist/provider/slack.mjs +13 -0
- package/dist/provider/spotify.d.mts +6 -0
- package/dist/provider/spotify.mjs +13 -0
- package/dist/provider/twitch.d.mts +6 -0
- package/dist/provider/twitch.mjs +13 -0
- package/dist/provider/vercel.d.mts +177 -0
- package/dist/provider/vercel.mjs +230 -0
- package/package.json +3 -3
package/dist/client.mjs
CHANGED
|
@@ -201,7 +201,10 @@ const createClient = (input) => {
|
|
|
201
201
|
},
|
|
202
202
|
async verify(subjects, token, options) {
|
|
203
203
|
try {
|
|
204
|
-
const jwtResult = await jwtVerify(token, await getJWKS(), {
|
|
204
|
+
const jwtResult = await jwtVerify(token, await getJWKS(), {
|
|
205
|
+
issuer: options?.issuer ?? issuer,
|
|
206
|
+
...options?.audience && { audience: options.audience }
|
|
207
|
+
});
|
|
205
208
|
const validated = await subjects[jwtResult.payload.type]?.["~standard"].validate(jwtResult.payload.properties);
|
|
206
209
|
if (!validated?.issues && jwtResult.payload.mode === "access") return {
|
|
207
210
|
success: true,
|
package/dist/core.d.mts
CHANGED
|
@@ -6,6 +6,7 @@ import { StorageAdapter } from "./storage/storage.mjs";
|
|
|
6
6
|
import { Plugin } from "./plugin/types.mjs";
|
|
7
7
|
import { Provider } from "./provider/provider.mjs";
|
|
8
8
|
import { Theme } from "./themes/theme.mjs";
|
|
9
|
+
import { AuthorizationState } from "./types.mjs";
|
|
9
10
|
import { Router } from "@draftlab/auth-router";
|
|
10
11
|
|
|
11
12
|
//#region src/core.d.ts
|
|
@@ -27,26 +28,6 @@ interface OnSuccessResponder<T$1 extends {
|
|
|
27
28
|
subject?: string;
|
|
28
29
|
}): Promise<Response>;
|
|
29
30
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Authorization state for OAuth 2.0 flows.
|
|
32
|
-
*/
|
|
33
|
-
interface AuthorizationState {
|
|
34
|
-
/** OAuth redirect URI */
|
|
35
|
-
redirect_uri: string;
|
|
36
|
-
/** OAuth response type */
|
|
37
|
-
response_type: string;
|
|
38
|
-
/** OAuth state parameter for CSRF protection */
|
|
39
|
-
state: string;
|
|
40
|
-
/** OAuth client identifier */
|
|
41
|
-
client_id: string;
|
|
42
|
-
/** OAuth audience parameter */
|
|
43
|
-
audience: string;
|
|
44
|
-
/** PKCE challenge data for code verification */
|
|
45
|
-
pkce?: {
|
|
46
|
-
challenge: string;
|
|
47
|
-
method: "S256";
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
31
|
/**
|
|
51
32
|
* Main issuer input configuration interface.
|
|
52
33
|
*/
|
|
@@ -129,4 +110,4 @@ declare const issuer: <Providers extends Record<string, Provider<unknown>>, Subj
|
|
|
129
110
|
};
|
|
130
111
|
}>;
|
|
131
112
|
//#endregion
|
|
132
|
-
export {
|
|
113
|
+
export { OnSuccessResponder, issuer };
|
package/dist/core.mjs
CHANGED
|
@@ -139,7 +139,8 @@ const issuer = (input) => {
|
|
|
139
139
|
clientID: value.clientID,
|
|
140
140
|
subject: value.subject,
|
|
141
141
|
ttl: value.ttl,
|
|
142
|
-
nextToken: generateSecureToken()
|
|
142
|
+
nextToken: generateSecureToken(),
|
|
143
|
+
timeUsed: value.timeUsed
|
|
143
144
|
};
|
|
144
145
|
const refreshKey = [
|
|
145
146
|
"oauth:refresh",
|
|
@@ -231,6 +232,7 @@ const issuer = (input) => {
|
|
|
231
232
|
subject,
|
|
232
233
|
redirectURI: authorization.redirect_uri,
|
|
233
234
|
clientID: authorization.client_id,
|
|
235
|
+
scopes: authorization.scopes,
|
|
234
236
|
pkce: authorization.pkce,
|
|
235
237
|
ttl: {
|
|
236
238
|
access: subjectOpts?.ttl?.access ?? ttlAccess,
|
|
@@ -492,13 +494,51 @@ const issuer = (input) => {
|
|
|
492
494
|
credentials: false
|
|
493
495
|
})],
|
|
494
496
|
handler: async (c) => {
|
|
495
|
-
const
|
|
497
|
+
const form = await c.formData();
|
|
498
|
+
const token = form.get("token")?.toString();
|
|
499
|
+
const tokenTypeHint = form.get("token_type_hint")?.toString();
|
|
496
500
|
if (!token) {
|
|
497
501
|
const error$1 = new OauthError("invalid_request", "Missing token parameter");
|
|
498
502
|
return c.json(error$1.toJSON(), { status: 400 });
|
|
499
503
|
}
|
|
500
504
|
try {
|
|
501
|
-
|
|
505
|
+
if (tokenTypeHint === "refresh_token") {
|
|
506
|
+
const splits$1 = token.split(":");
|
|
507
|
+
const tokenPart$1 = splits$1.pop();
|
|
508
|
+
if (tokenPart$1 && splits$1.length > 0) {
|
|
509
|
+
const key = [
|
|
510
|
+
"oauth:refresh",
|
|
511
|
+
splits$1.join(":"),
|
|
512
|
+
tokenPart$1
|
|
513
|
+
];
|
|
514
|
+
if (await Storage.get(storage, key)) {
|
|
515
|
+
await Storage.remove(storage, key);
|
|
516
|
+
const expiresAt$1 = Date.now() + ttlRefreshRetention * 1e3;
|
|
517
|
+
await Revocation.revoke(storage, token, expiresAt$1);
|
|
518
|
+
return c.json({});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else if (tokenTypeHint === "access_token") {
|
|
522
|
+
const expiresAt$1 = Date.now() + ttlAccess * 1e3;
|
|
523
|
+
await Revocation.revoke(storage, token, expiresAt$1);
|
|
524
|
+
return c.json({});
|
|
525
|
+
}
|
|
526
|
+
const splits = token.split(":");
|
|
527
|
+
const tokenPart = splits.pop();
|
|
528
|
+
if (tokenPart && splits.length > 0) {
|
|
529
|
+
const key = [
|
|
530
|
+
"oauth:refresh",
|
|
531
|
+
splits.join(":"),
|
|
532
|
+
tokenPart
|
|
533
|
+
];
|
|
534
|
+
if (await Storage.get(storage, key)) {
|
|
535
|
+
await Storage.remove(storage, key);
|
|
536
|
+
const expiresAt$1 = Date.now() + ttlRefreshRetention * 1e3;
|
|
537
|
+
await Revocation.revoke(storage, token, expiresAt$1);
|
|
538
|
+
return c.json({});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const expiresAt = Date.now() + ttlAccess * 1e3;
|
|
502
542
|
await Revocation.revoke(storage, token, expiresAt);
|
|
503
543
|
return c.json({});
|
|
504
544
|
} catch (_err) {
|
|
@@ -526,6 +566,7 @@ const issuer = (input) => {
|
|
|
526
566
|
client_id,
|
|
527
567
|
audience,
|
|
528
568
|
scope,
|
|
569
|
+
scopes: scope ? scope.split(" ").filter(Boolean) : void 0,
|
|
529
570
|
...code_challenge && code_challenge_method && { pkce: {
|
|
530
571
|
challenge: code_challenge,
|
|
531
572
|
method: code_challenge_method
|
|
@@ -99,6 +99,12 @@ interface AppleConfig extends Oauth2WrappedConfig {
|
|
|
99
99
|
* }
|
|
100
100
|
* })
|
|
101
101
|
* ```
|
|
102
|
+
*
|
|
103
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
104
|
+
* - Development: `http://localhost:3000/auth/apple/callback`
|
|
105
|
+
* - Production: `https://yourapp.com/auth/apple/callback`
|
|
106
|
+
*
|
|
107
|
+
* Register this URL in your Apple Developer Portal.
|
|
102
108
|
*/
|
|
103
109
|
declare const AppleProvider: (config: AppleConfig) => Provider<Oauth2UserData>;
|
|
104
110
|
//#endregion
|
package/dist/provider/apple.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* apple: AppleProvider({
|
|
16
17
|
* clientID: process.env.APPLE_CLIENT_ID,
|
|
17
18
|
* clientSecret: process.env.APPLE_CLIENT_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/apple/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/apple/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your Apple Developer Portal.
|
|
30
|
+
*
|
|
24
31
|
* ## Setup Instructions
|
|
25
32
|
*
|
|
26
33
|
* ### 1. Create App ID
|
|
@@ -135,6 +142,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
135
142
|
* }
|
|
136
143
|
* })
|
|
137
144
|
* ```
|
|
145
|
+
*
|
|
146
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
147
|
+
* - Development: `http://localhost:3000/auth/apple/callback`
|
|
148
|
+
* - Production: `https://yourapp.com/auth/apple/callback`
|
|
149
|
+
*
|
|
150
|
+
* Register this URL in your Apple Developer Portal.
|
|
138
151
|
*/
|
|
139
152
|
const AppleProvider = (config) => {
|
|
140
153
|
return Oauth2Provider({
|
|
@@ -134,6 +134,12 @@ interface DiscordConfig extends Oauth2WrappedConfig {
|
|
|
134
134
|
* }
|
|
135
135
|
* })
|
|
136
136
|
* ```
|
|
137
|
+
*
|
|
138
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
139
|
+
* - Development: `http://localhost:3000/auth/discord/callback`
|
|
140
|
+
* - Production: `https://yourapp.com/auth/discord/callback`
|
|
141
|
+
*
|
|
142
|
+
* Register this URL in your Discord Developer Portal.
|
|
137
143
|
*/
|
|
138
144
|
declare const DiscordProvider: (config: DiscordConfig) => Provider<Oauth2UserData>;
|
|
139
145
|
//#endregion
|
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* discord: DiscordProvider({
|
|
16
17
|
* clientID: process.env.DISCORD_CLIENT_ID,
|
|
17
18
|
* clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/discord/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/discord/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your Discord Developer Portal.
|
|
30
|
+
*
|
|
24
31
|
* ## Common Scopes
|
|
25
32
|
*
|
|
26
33
|
* - `identify` - Access to user's basic account information
|
|
@@ -127,6 +134,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
127
134
|
* }
|
|
128
135
|
* })
|
|
129
136
|
* ```
|
|
137
|
+
*
|
|
138
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
139
|
+
* - Development: `http://localhost:3000/auth/discord/callback`
|
|
140
|
+
* - Production: `https://yourapp.com/auth/discord/callback`
|
|
141
|
+
*
|
|
142
|
+
* Register this URL in your Discord Developer Portal.
|
|
130
143
|
*/
|
|
131
144
|
const DiscordProvider = (config) => {
|
|
132
145
|
return Oauth2Provider({
|
|
@@ -130,6 +130,12 @@ interface FacebookConfig extends Oauth2WrappedConfig {
|
|
|
130
130
|
* }
|
|
131
131
|
* })
|
|
132
132
|
* ```
|
|
133
|
+
*
|
|
134
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
135
|
+
* - Development: `http://localhost:3000/auth/facebook/callback`
|
|
136
|
+
* - Production: `https://yourapp.com/auth/facebook/callback`
|
|
137
|
+
*
|
|
138
|
+
* Register this URL in your Facebook App Dashboard.
|
|
133
139
|
*/
|
|
134
140
|
declare const FacebookProvider: (config: FacebookConfig) => Provider<Oauth2UserData>;
|
|
135
141
|
//#endregion
|
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* facebook: FacebookProvider({
|
|
16
17
|
* clientID: process.env.FACEBOOK_APP_ID,
|
|
17
18
|
* clientSecret: process.env.FACEBOOK_APP_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/facebook/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/facebook/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your Facebook App Dashboard.
|
|
30
|
+
*
|
|
24
31
|
* ## Configuration Options
|
|
25
32
|
*
|
|
26
33
|
* - Access tokens for Facebook Graph API calls
|
|
@@ -121,6 +128,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
121
128
|
* }
|
|
122
129
|
* })
|
|
123
130
|
* ```
|
|
131
|
+
*
|
|
132
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
133
|
+
* - Development: `http://localhost:3000/auth/facebook/callback`
|
|
134
|
+
* - Production: `https://yourapp.com/auth/facebook/callback`
|
|
135
|
+
*
|
|
136
|
+
* Register this URL in your Facebook App Dashboard.
|
|
124
137
|
*/
|
|
125
138
|
const FacebookProvider = (config) => {
|
|
126
139
|
return Oauth2Provider({
|
package/dist/provider/github.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
11
11
|
* import { GithubProvider } from "@draftlab/auth/provider/github"
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
|
+
* basePath: "/auth", // Important for callback URL
|
|
14
15
|
* providers: {
|
|
15
16
|
* github: GithubProvider({
|
|
16
17
|
* clientID: process.env.GITHUB_CLIENT_ID,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/github/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/github/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your GitHub App/OAuth App settings.
|
|
30
|
+
*
|
|
24
31
|
* ## GitHub App vs OAuth App
|
|
25
32
|
*
|
|
26
33
|
* This provider works with both GitHub OAuth Apps and GitHub Apps:
|
|
@@ -94,6 +94,12 @@ interface GitlabConfig extends Oauth2WrappedConfig {
|
|
|
94
94
|
* }
|
|
95
95
|
* })
|
|
96
96
|
* ```
|
|
97
|
+
*
|
|
98
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
99
|
+
* - Development: `http://localhost:3000/auth/gitlab/callback`
|
|
100
|
+
* - Production: `https://yourapp.com/auth/gitlab/callback`
|
|
101
|
+
*
|
|
102
|
+
* Register this URL in your GitLab Application settings.
|
|
97
103
|
*/
|
|
98
104
|
declare const GitlabProvider: (config: GitlabConfig) => Provider<Oauth2UserData>;
|
|
99
105
|
//#endregion
|
package/dist/provider/gitlab.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* gitlab: GitlabProvider({
|
|
16
17
|
* clientID: process.env.GITLAB_CLIENT_ID,
|
|
17
18
|
* clientSecret: process.env.GITLAB_CLIENT_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/gitlab/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/gitlab/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your GitLab Application settings.
|
|
30
|
+
*
|
|
24
31
|
* ## Common Scopes
|
|
25
32
|
*
|
|
26
33
|
* - `read_user` - Access user profile
|
|
@@ -47,6 +54,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
47
54
|
* })
|
|
48
55
|
* ```
|
|
49
56
|
*
|
|
57
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
58
|
+
* - Development: `http://localhost:3000/auth/gitlab/callback`
|
|
59
|
+
* - Production: `https://yourapp.com/auth/gitlab/callback`
|
|
60
|
+
*
|
|
61
|
+
* Register this URL in your GitLab Application settings.
|
|
62
|
+
*
|
|
50
63
|
* ## User Data Access
|
|
51
64
|
*
|
|
52
65
|
* ```ts
|
|
@@ -112,6 +125,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
112
125
|
* }
|
|
113
126
|
* })
|
|
114
127
|
* ```
|
|
128
|
+
*
|
|
129
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
130
|
+
* - Development: `http://localhost:3000/auth/gitlab/callback`
|
|
131
|
+
* - Production: `https://yourapp.com/auth/gitlab/callback`
|
|
132
|
+
*
|
|
133
|
+
* Register this URL in your GitLab Application settings.
|
|
115
134
|
*/
|
|
116
135
|
const GitlabProvider = (config) => {
|
|
117
136
|
return Oauth2Provider({
|
package/dist/provider/google.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
11
11
|
* import { GoogleProvider } from "@draftlab/auth/provider/google"
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
|
+
* basePath: "/auth", // Important for callback URL
|
|
14
15
|
* providers: {
|
|
15
16
|
* google: GoogleProvider({
|
|
16
17
|
* clientID: process.env.GOOGLE_CLIENT_ID,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/google/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/google/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your Google Cloud Console OAuth 2.0 credentials.
|
|
30
|
+
*
|
|
24
31
|
* ## Configuration Options
|
|
25
32
|
*
|
|
26
33
|
* - Access tokens for Google API calls
|
|
@@ -120,6 +120,12 @@ interface LinkedInConfig extends Oauth2WrappedConfig {
|
|
|
120
120
|
* }
|
|
121
121
|
* })
|
|
122
122
|
* ```
|
|
123
|
+
*
|
|
124
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
125
|
+
* - Development: `http://localhost:3000/auth/linkedin/callback`
|
|
126
|
+
* - Production: `https://yourapp.com/auth/linkedin/callback`
|
|
127
|
+
*
|
|
128
|
+
* Register this URL in your LinkedIn Developer Portal.
|
|
123
129
|
*/
|
|
124
130
|
declare const LinkedInProvider: (config: LinkedInConfig) => Provider<Oauth2UserData>;
|
|
125
131
|
//#endregion
|
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* linkedin: LinkedInProvider({
|
|
16
17
|
* clientID: process.env.LINKEDIN_CLIENT_ID,
|
|
17
18
|
* clientSecret: process.env.LINKEDIN_CLIENT_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/linkedin/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/linkedin/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your LinkedIn Developer Portal.
|
|
30
|
+
*
|
|
24
31
|
* ## Common Scopes
|
|
25
32
|
*
|
|
26
33
|
* - `r_liteprofile` - Access to basic profile information
|
|
@@ -113,6 +120,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
113
120
|
* }
|
|
114
121
|
* })
|
|
115
122
|
* ```
|
|
123
|
+
*
|
|
124
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
125
|
+
* - Development: `http://localhost:3000/auth/linkedin/callback`
|
|
126
|
+
* - Production: `https://yourapp.com/auth/linkedin/callback`
|
|
127
|
+
*
|
|
128
|
+
* Register this URL in your LinkedIn Developer Portal.
|
|
116
129
|
*/
|
|
117
130
|
const LinkedInProvider = (config) => {
|
|
118
131
|
return Oauth2Provider({
|
|
@@ -166,6 +166,12 @@ interface MicrosoftConfig extends Oauth2WrappedConfig {
|
|
|
166
166
|
* }
|
|
167
167
|
* })
|
|
168
168
|
* ```
|
|
169
|
+
*
|
|
170
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
171
|
+
* - Development: `http://localhost:3000/auth/microsoft/callback`
|
|
172
|
+
* - Production: `https://yourapp.com/auth/microsoft/callback`
|
|
173
|
+
*
|
|
174
|
+
* Register this URL in your Azure Portal App Registration.
|
|
169
175
|
*/
|
|
170
176
|
declare const MicrosoftProvider: (config: MicrosoftConfig) => Provider<Oauth2UserData>;
|
|
171
177
|
//#endregion
|
|
@@ -13,6 +13,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
13
13
|
*
|
|
14
14
|
* export default issuer({
|
|
15
15
|
* providers: {
|
|
16
|
+
* basePath: "/auth", // Important for callback URL
|
|
16
17
|
* microsoft: MicrosoftProvider({
|
|
17
18
|
* tenant: "common", // or specific tenant ID
|
|
18
19
|
* clientID: process.env.MICROSOFT_CLIENT_ID,
|
|
@@ -23,6 +24,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
23
24
|
* })
|
|
24
25
|
* ```
|
|
25
26
|
*
|
|
27
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
28
|
+
* - Development: `http://localhost:3000/auth/microsoft/callback`
|
|
29
|
+
* - Production: `https://yourapp.com/auth/microsoft/callback`
|
|
30
|
+
*
|
|
31
|
+
* Register this URL in your Azure Portal App Registration.
|
|
32
|
+
*
|
|
26
33
|
* ## Tenant Configuration
|
|
27
34
|
*
|
|
28
35
|
* - `common` - Both personal and work/school accounts
|
|
@@ -148,6 +155,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
148
155
|
* }
|
|
149
156
|
* })
|
|
150
157
|
* ```
|
|
158
|
+
*
|
|
159
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
160
|
+
* - Development: `http://localhost:3000/auth/microsoft/callback`
|
|
161
|
+
* - Production: `https://yourapp.com/auth/microsoft/callback`
|
|
162
|
+
*
|
|
163
|
+
* Register this URL in your Azure Portal App Registration.
|
|
151
164
|
*/
|
|
152
165
|
const MicrosoftProvider = (config) => {
|
|
153
166
|
return Oauth2Provider({
|
package/dist/provider/oauth2.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { getRelativeUrl } from "../util.mjs";
|
|
|
2
2
|
import { OauthError } from "../error.mjs";
|
|
3
3
|
import { generatePKCE } from "../pkce.mjs";
|
|
4
4
|
import { generateSecureToken, timingSafeCompare } from "../random.mjs";
|
|
5
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
5
6
|
|
|
6
7
|
//#region src/provider/oauth2.ts
|
|
7
8
|
/**
|
|
@@ -70,6 +71,15 @@ const Oauth2Provider = (config) => {
|
|
|
70
71
|
if (!response.ok) throw new Error(`Token request failed with status ${response.status}`);
|
|
71
72
|
const tokenData = await response.json();
|
|
72
73
|
if (tokenData.error) throw new OauthError(tokenData.error, tokenData.error_description || "");
|
|
74
|
+
if (tokenData.id_token && config.endpoint.jwks) try {
|
|
75
|
+
const jwks = createRemoteJWKSet(new URL(config.endpoint.jwks));
|
|
76
|
+
await jwtVerify(tokenData.id_token, jwks, {
|
|
77
|
+
issuer: config.endpoint.authorization.split("/").slice(0, 3).join("/"),
|
|
78
|
+
clockTolerance: 60
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new OauthError("invalid_request", `ID token validation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
82
|
+
}
|
|
73
83
|
return await ctx.success(c, {
|
|
74
84
|
clientID: config.clientID,
|
|
75
85
|
tokenset: {
|
|
@@ -95,6 +95,12 @@ interface RedditConfig extends Oauth2WrappedConfig {
|
|
|
95
95
|
* }
|
|
96
96
|
* })
|
|
97
97
|
* ```
|
|
98
|
+
*
|
|
99
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
100
|
+
* - Development: `http://localhost:3000/auth/reddit/callback`
|
|
101
|
+
* - Production: `https://yourapp.com/auth/reddit/callback`
|
|
102
|
+
*
|
|
103
|
+
* Register this URL in your Reddit App Preferences.
|
|
98
104
|
*/
|
|
99
105
|
declare const RedditProvider: (config: RedditConfig) => Provider<Oauth2UserData>;
|
|
100
106
|
//#endregion
|
package/dist/provider/reddit.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* reddit: RedditProvider({
|
|
16
17
|
* clientID: process.env.REDDIT_CLIENT_ID,
|
|
17
18
|
* clientSecret: process.env.REDDIT_CLIENT_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/reddit/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/reddit/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your Reddit App Preferences.
|
|
30
|
+
*
|
|
24
31
|
* ## Common Scopes
|
|
25
32
|
*
|
|
26
33
|
* - `identity` - Access user's identity information
|
|
@@ -98,6 +105,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
98
105
|
* }
|
|
99
106
|
* })
|
|
100
107
|
* ```
|
|
108
|
+
*
|
|
109
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
110
|
+
* - Development: `http://localhost:3000/auth/reddit/callback`
|
|
111
|
+
* - Production: `https://yourapp.com/auth/reddit/callback`
|
|
112
|
+
*
|
|
113
|
+
* Register this URL in your Reddit App Preferences.
|
|
101
114
|
*/
|
|
102
115
|
const RedditProvider = (config) => {
|
|
103
116
|
return Oauth2Provider({
|
|
@@ -102,6 +102,12 @@ interface SlackConfig extends Oauth2WrappedConfig {
|
|
|
102
102
|
* }
|
|
103
103
|
* })
|
|
104
104
|
* ```
|
|
105
|
+
*
|
|
106
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
107
|
+
* - Development: `http://localhost:3000/auth/slack/callback`
|
|
108
|
+
* - Production: `https://yourapp.com/auth/slack/callback`
|
|
109
|
+
*
|
|
110
|
+
* Register this URL in your Slack App settings.
|
|
105
111
|
*/
|
|
106
112
|
declare const SlackProvider: (config: SlackConfig) => Provider<Oauth2UserData>;
|
|
107
113
|
//#endregion
|
package/dist/provider/slack.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* slack: SlackProvider({
|
|
16
17
|
* clientID: process.env.SLACK_CLIENT_ID,
|
|
17
18
|
* clientSecret: process.env.SLACK_CLIENT_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/slack/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/slack/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your Slack App settings.
|
|
30
|
+
*
|
|
24
31
|
* ## Common Scopes
|
|
25
32
|
*
|
|
26
33
|
* - `users:read` - Access to user profiles
|
|
@@ -109,6 +116,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
109
116
|
* }
|
|
110
117
|
* })
|
|
111
118
|
* ```
|
|
119
|
+
*
|
|
120
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
121
|
+
* - Development: `http://localhost:3000/auth/slack/callback`
|
|
122
|
+
* - Production: `https://yourapp.com/auth/slack/callback`
|
|
123
|
+
*
|
|
124
|
+
* Register this URL in your Slack App settings.
|
|
112
125
|
*/
|
|
113
126
|
const SlackProvider = (config) => {
|
|
114
127
|
return Oauth2Provider({
|
|
@@ -101,6 +101,12 @@ interface SpotifyConfig extends Oauth2WrappedConfig {
|
|
|
101
101
|
* }
|
|
102
102
|
* })
|
|
103
103
|
* ```
|
|
104
|
+
*
|
|
105
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
106
|
+
* - Development: `http://localhost:3000/auth/spotify/callback`
|
|
107
|
+
* - Production: `https://yourapp.com/auth/spotify/callback`
|
|
108
|
+
*
|
|
109
|
+
* Register this URL in your Spotify Developer Dashboard.
|
|
104
110
|
*/
|
|
105
111
|
declare const SpotifyProvider: (config: SpotifyConfig) => Provider<Oauth2UserData>;
|
|
106
112
|
//#endregion
|
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* spotify: SpotifyProvider({
|
|
16
17
|
* clientID: process.env.SPOTIFY_CLIENT_ID,
|
|
17
18
|
* clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/spotify/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/spotify/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your Spotify Developer Dashboard.
|
|
30
|
+
*
|
|
24
31
|
* ## Common Scopes
|
|
25
32
|
*
|
|
26
33
|
* - `user-read-private` - Access user's private data
|
|
@@ -106,6 +113,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
106
113
|
* }
|
|
107
114
|
* })
|
|
108
115
|
* ```
|
|
116
|
+
*
|
|
117
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
118
|
+
* - Development: `http://localhost:3000/auth/spotify/callback`
|
|
119
|
+
* - Production: `https://yourapp.com/auth/spotify/callback`
|
|
120
|
+
*
|
|
121
|
+
* Register this URL in your Spotify Developer Dashboard.
|
|
109
122
|
*/
|
|
110
123
|
const SpotifyProvider = (config) => {
|
|
111
124
|
return Oauth2Provider({
|
|
@@ -96,6 +96,12 @@ interface TwitchConfig extends Oauth2WrappedConfig {
|
|
|
96
96
|
* }
|
|
97
97
|
* })
|
|
98
98
|
* ```
|
|
99
|
+
*
|
|
100
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
101
|
+
* - Development: `http://localhost:3000/auth/twitch/callback`
|
|
102
|
+
* - Production: `https://yourapp.com/auth/twitch/callback`
|
|
103
|
+
*
|
|
104
|
+
* Register this URL in your Twitch Developer Console.
|
|
99
105
|
*/
|
|
100
106
|
declare const TwitchProvider: (config: TwitchConfig) => Provider<Oauth2UserData>;
|
|
101
107
|
//#endregion
|
package/dist/provider/twitch.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
12
12
|
*
|
|
13
13
|
* export default issuer({
|
|
14
14
|
* providers: {
|
|
15
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
16
|
* twitch: TwitchProvider({
|
|
16
17
|
* clientID: process.env.TWITCH_CLIENT_ID,
|
|
17
18
|
* clientSecret: process.env.TWITCH_CLIENT_SECRET,
|
|
@@ -21,6 +22,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Development: `http://localhost:3000/auth/twitch/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/twitch/callback`
|
|
28
|
+
*
|
|
29
|
+
* Register this URL in your Twitch Developer Console.
|
|
30
|
+
*
|
|
24
31
|
* ## Common Scopes
|
|
25
32
|
*
|
|
26
33
|
* - `user:read:email` - Access user's email address
|
|
@@ -102,6 +109,12 @@ import { Oauth2Provider } from "./oauth2.mjs";
|
|
|
102
109
|
* }
|
|
103
110
|
* })
|
|
104
111
|
* ```
|
|
112
|
+
*
|
|
113
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
114
|
+
* - Development: `http://localhost:3000/auth/twitch/callback`
|
|
115
|
+
* - Production: `https://yourapp.com/auth/twitch/callback`
|
|
116
|
+
*
|
|
117
|
+
* Register this URL in your Twitch Developer Console.
|
|
105
118
|
*/
|
|
106
119
|
const TwitchProvider = (config) => {
|
|
107
120
|
return Oauth2Provider({
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Provider } from "./provider.mjs";
|
|
2
|
+
import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/provider/vercel.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration options for Vercel OAuth 2.0 + OpenID Connect provider.
|
|
8
|
+
* Extends the base OAuth 2.0 configuration with Vercel-specific documentation.
|
|
9
|
+
*/
|
|
10
|
+
interface VercelConfig extends Oauth2WrappedConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Vercel OAuth App client ID.
|
|
13
|
+
* Found in your Vercel App settings under the Authentication tab.
|
|
14
|
+
*
|
|
15
|
+
* To create an app:
|
|
16
|
+
* 1. Go to Team Settings → Apps → Create
|
|
17
|
+
* 2. Configure app details and callback URLs
|
|
18
|
+
* 3. Copy the Client ID from the Authentication tab
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* {
|
|
23
|
+
* clientID: "oac_abc123xyz789" // Vercel OAuth App Client ID
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
readonly clientID: string;
|
|
28
|
+
/**
|
|
29
|
+
* Vercel OAuth App client secret.
|
|
30
|
+
* Generated in your Vercel App settings under the Authentication tab.
|
|
31
|
+
* Keep this secure and never expose it to client-side code.
|
|
32
|
+
*
|
|
33
|
+
* To generate:
|
|
34
|
+
* 1. Go to your app's Authentication tab
|
|
35
|
+
* 2. Click "Generate Client Secret"
|
|
36
|
+
* 3. Copy and store securely (shown only once)
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* {
|
|
41
|
+
* clientSecret: process.env.VERCEL_CLIENT_SECRET
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
readonly clientSecret: string;
|
|
46
|
+
/**
|
|
47
|
+
* OpenID Connect scopes to request.
|
|
48
|
+
* Controls what user information is included in the ID Token.
|
|
49
|
+
*
|
|
50
|
+
* Available scopes (must be enabled in Vercel App dashboard first):
|
|
51
|
+
* - `openid`: Required for ID Token issuance
|
|
52
|
+
* - `email`: User's email address
|
|
53
|
+
* - `profile`: Name, username, and avatar
|
|
54
|
+
* - `offline_access`: Refresh token for long-lived access (optional)
|
|
55
|
+
*
|
|
56
|
+
* **Important**: Enable scopes in: Vercel App → Permissions page
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* {
|
|
61
|
+
* // Basic scopes (usually sufficient)
|
|
62
|
+
* scopes: ["openid", "email", "profile"]
|
|
63
|
+
*
|
|
64
|
+
* // With refresh token support (enable offline_access in dashboard first)
|
|
65
|
+
* scopes: ["openid", "email", "profile", "offline_access"]
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
readonly scopes: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Additional query parameters for Vercel OAuth authorization.
|
|
72
|
+
* Useful for customizing the authorization flow.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* {
|
|
77
|
+
* query: {
|
|
78
|
+
* prompt: "consent" // Force consent screen every time
|
|
79
|
+
* }
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
readonly query?: Record<string, string>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Creates a Vercel OAuth 2.0 + OpenID Connect authentication provider.
|
|
87
|
+
* Implements "Sign in with Vercel" for user authentication.
|
|
88
|
+
*
|
|
89
|
+
* This provider uses the standard OAuth 2.0 Authorization Code Grant flow
|
|
90
|
+
* with PKCE (Proof Key for Code Exchange) for enhanced security.
|
|
91
|
+
*
|
|
92
|
+
* @param config - Vercel OAuth 2.0 configuration
|
|
93
|
+
* @returns OAuth 2.0 provider configured for Vercel
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* // Basic Vercel authentication (email + profile)
|
|
98
|
+
* const basicVercel = VercelProvider({
|
|
99
|
+
* clientID: process.env.VERCEL_CLIENT_ID,
|
|
100
|
+
* clientSecret: process.env.VERCEL_CLIENT_SECRET,
|
|
101
|
+
* scopes: ["openid", "email", "profile"]
|
|
102
|
+
* })
|
|
103
|
+
*
|
|
104
|
+
* // Vercel with refresh token support
|
|
105
|
+
* const vercelWithRefresh = VercelProvider({
|
|
106
|
+
* clientID: process.env.VERCEL_CLIENT_ID,
|
|
107
|
+
* clientSecret: process.env.VERCEL_CLIENT_SECRET,
|
|
108
|
+
* scopes: ["openid", "email", "profile", "offline_access"]
|
|
109
|
+
* })
|
|
110
|
+
*
|
|
111
|
+
* // Minimal setup (only user ID in ID Token)
|
|
112
|
+
* const minimalVercel = VercelProvider({
|
|
113
|
+
* clientID: process.env.VERCEL_CLIENT_ID,
|
|
114
|
+
* clientSecret: process.env.VERCEL_CLIENT_SECRET,
|
|
115
|
+
* scopes: ["openid"] // Only sub claim in ID Token
|
|
116
|
+
* })
|
|
117
|
+
*
|
|
118
|
+
* // Using the tokens in your app
|
|
119
|
+
* export default issuer({
|
|
120
|
+
* providers: { vercel: vercelWithRefresh },
|
|
121
|
+
* success: async (ctx, value) => {
|
|
122
|
+
* if (value.provider === "vercel") {
|
|
123
|
+
* const idToken = value.tokenset.raw.id_token as string | undefined
|
|
124
|
+
* const accessToken = value.tokenset.access
|
|
125
|
+
* const refreshToken = value.tokenset.refresh
|
|
126
|
+
*
|
|
127
|
+
* if (idToken) {
|
|
128
|
+
* // Decode ID Token to access user claims
|
|
129
|
+
* // (Already validated by oauth2.ts - signature, issuer, audience, exp)
|
|
130
|
+
* const claims = JSON.parse(
|
|
131
|
+
* Buffer.from(idToken.split('.')[1], 'base64').toString()
|
|
132
|
+
* )
|
|
133
|
+
*
|
|
134
|
+
* // Claims available (depending on scopes):
|
|
135
|
+
* // - sub: Vercel user ID (always present)
|
|
136
|
+
* // - email: user@example.com (if email scope)
|
|
137
|
+
* // - name: "John Doe" (if profile scope)
|
|
138
|
+
* // - picture: "https://..." (if profile scope)
|
|
139
|
+
* // - preferred_username: "johndoe" (if profile scope)
|
|
140
|
+
*
|
|
141
|
+
* // Optionally call Vercel API for more data
|
|
142
|
+
* const userRes = await fetch('https://api.vercel.com/v2/user', {
|
|
143
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
144
|
+
* })
|
|
145
|
+
* const user = await userRes.json()
|
|
146
|
+
*
|
|
147
|
+
* // Get user's teams
|
|
148
|
+
* const teamsRes = await fetch('https://api.vercel.com/v2/teams', {
|
|
149
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
150
|
+
* })
|
|
151
|
+
* const teams = await teamsRes.json()
|
|
152
|
+
*
|
|
153
|
+
* return ctx.subject("user", {
|
|
154
|
+
* vercelId: claims.sub,
|
|
155
|
+
* email: claims.email,
|
|
156
|
+
* name: claims.name,
|
|
157
|
+
* username: claims.preferred_username,
|
|
158
|
+
* avatar: claims.picture,
|
|
159
|
+
* teamCount: teams.teams?.length || 0
|
|
160
|
+
* })
|
|
161
|
+
* }
|
|
162
|
+
* }
|
|
163
|
+
* }
|
|
164
|
+
* })
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* - Requires creating a Vercel App in Team Settings → Apps
|
|
169
|
+
* - PKCE is enabled by default for enhanced security
|
|
170
|
+
* - ID Token is automatically validated (signature, issuer, audience, expiration)
|
|
171
|
+
* - Access tokens expire after 1 hour
|
|
172
|
+
* - Refresh tokens rotate on each use and last 30 days
|
|
173
|
+
* - The `openid` scope is required for ID Token issuance
|
|
174
|
+
*/
|
|
175
|
+
declare const VercelProvider: (config: VercelConfig) => Provider<Oauth2UserData>;
|
|
176
|
+
//#endregion
|
|
177
|
+
export { VercelConfig, VercelProvider };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { Oauth2Provider } from "./oauth2.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/provider/vercel.ts
|
|
4
|
+
/**
|
|
5
|
+
* Vercel OAuth 2.0 + OpenID Connect authentication provider for Draft Auth.
|
|
6
|
+
* Implements "Sign in with Vercel" for user authentication.
|
|
7
|
+
*
|
|
8
|
+
* ## Quick Setup
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { VercelProvider } from "@draftlab/auth/provider/vercel"
|
|
12
|
+
*
|
|
13
|
+
* export default issuer({
|
|
14
|
+
* basePath: "/auth", // Important for callback URL
|
|
15
|
+
* providers: {
|
|
16
|
+
* vercel: VercelProvider({
|
|
17
|
+
* clientID: process.env.VERCEL_CLIENT_ID,
|
|
18
|
+
* clientSecret: process.env.VERCEL_CLIENT_SECRET,
|
|
19
|
+
* scopes: ["openid", "email", "profile"]
|
|
20
|
+
* })
|
|
21
|
+
* }
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* **Callback URL Pattern**: `{baseURL}{basePath}/{provider}/callback`
|
|
26
|
+
* - Example: `http://localhost:3000/auth/vercel/callback`
|
|
27
|
+
* - Production: `https://yourapp.com/auth/vercel/callback`
|
|
28
|
+
*
|
|
29
|
+
* ## Creating a Vercel App
|
|
30
|
+
*
|
|
31
|
+
* Before using this provider, create a Vercel App in your dashboard:
|
|
32
|
+
*
|
|
33
|
+
* 1. Go to **Team Settings** → **Apps** → **Create**
|
|
34
|
+
* 2. Fill in app name, description, and logo
|
|
35
|
+
* 3. Add **Authorization Callback URLs**:
|
|
36
|
+
* - Development: `http://localhost:3000/auth/vercel/callback`
|
|
37
|
+
* - Production: `https://yourapp.com/auth/vercel/callback`
|
|
38
|
+
* - Pattern: `{baseURL}{basePath}/{provider}/callback`
|
|
39
|
+
* 4. Configure **Scopes** in the app's Permissions page:
|
|
40
|
+
* - ✅ openid (Required)
|
|
41
|
+
* - ✅ email
|
|
42
|
+
* - ✅ profile
|
|
43
|
+
* - ✅ offline_access (optional, for refresh tokens)
|
|
44
|
+
* 5. Generate a **Client Secret** in the Authentication tab
|
|
45
|
+
* 6. Copy the **Client ID** and **Client Secret**
|
|
46
|
+
*
|
|
47
|
+
* **Important**: You must enable the scopes in the Vercel App dashboard before requesting them!
|
|
48
|
+
*
|
|
49
|
+
* ## Available Scopes
|
|
50
|
+
*
|
|
51
|
+
* - `openid` - **Required**. Enables ID Token issuance for user identification
|
|
52
|
+
* - `email` - Access user's email address in ID Token
|
|
53
|
+
* - `profile` - Access user's name, username, and avatar in ID Token
|
|
54
|
+
* - `offline_access` - Issue a Refresh Token for long-lived access (30 days)
|
|
55
|
+
*
|
|
56
|
+
* ## Tokens Returned
|
|
57
|
+
*
|
|
58
|
+
* - **ID Token**: Signed JWT with user identity claims (verified automatically)
|
|
59
|
+
* - **Access Token**: Bearer token for Vercel API calls (1 hour duration)
|
|
60
|
+
* - **Refresh Token**: Rotates on each use (30 days, requires offline_access scope)
|
|
61
|
+
*
|
|
62
|
+
* ## User Data Access
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* success: async (ctx, value) => {
|
|
66
|
+
* if (value.provider === "vercel") {
|
|
67
|
+
* // ID Token is automatically validated (signature, issuer, audience, expiration)
|
|
68
|
+
* const idToken = value.tokenset.raw.id_token as string | undefined
|
|
69
|
+
* const accessToken = value.tokenset.access
|
|
70
|
+
* const refreshToken = value.tokenset.refresh
|
|
71
|
+
*
|
|
72
|
+
* // Decode ID Token to access user claims
|
|
73
|
+
* if (idToken) {
|
|
74
|
+
* const claims = JSON.parse(
|
|
75
|
+
* Buffer.from(idToken.split('.')[1], 'base64').toString()
|
|
76
|
+
* )
|
|
77
|
+
*
|
|
78
|
+
* // Claims available (depending on scopes):
|
|
79
|
+
* // - sub: Unique Vercel user ID (always present)
|
|
80
|
+
* // - email: User's email (if email scope granted)
|
|
81
|
+
* // - name: User's full name (if profile scope granted)
|
|
82
|
+
* // - picture: Avatar URL (if profile scope granted)
|
|
83
|
+
* // - preferred_username: Vercel username (if profile scope granted)
|
|
84
|
+
*
|
|
85
|
+
* return ctx.subject("user", {
|
|
86
|
+
* email: claims.email || claims.sub
|
|
87
|
+
* })
|
|
88
|
+
* }
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* ## Calling Vercel API
|
|
94
|
+
*
|
|
95
|
+
* Use the access token to call Vercel's REST API:
|
|
96
|
+
*
|
|
97
|
+
* ```ts
|
|
98
|
+
* // Get user information
|
|
99
|
+
* const userRes = await fetch('https://api.vercel.com/v2/user', {
|
|
100
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
101
|
+
* })
|
|
102
|
+
*
|
|
103
|
+
* // Get user's teams
|
|
104
|
+
* const teamsRes = await fetch('https://api.vercel.com/v2/teams', {
|
|
105
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
106
|
+
* })
|
|
107
|
+
*
|
|
108
|
+
* // Get projects (requires appropriate permissions)
|
|
109
|
+
* const projectsRes = await fetch('https://api.vercel.com/v9/projects', {
|
|
110
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
111
|
+
* })
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* ## Consent Page
|
|
115
|
+
*
|
|
116
|
+
* The first time a user signs in, Vercel shows a consent page with:
|
|
117
|
+
* - Your app's name and logo
|
|
118
|
+
* - Requested scopes and permissions
|
|
119
|
+
* - Allow/Cancel buttons
|
|
120
|
+
*
|
|
121
|
+
* If the user grants access, they're redirected back with an authorization code.
|
|
122
|
+
* If they cancel, they're redirected with an error parameter.
|
|
123
|
+
*
|
|
124
|
+
* @packageDocumentation
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* Creates a Vercel OAuth 2.0 + OpenID Connect authentication provider.
|
|
128
|
+
* Implements "Sign in with Vercel" for user authentication.
|
|
129
|
+
*
|
|
130
|
+
* This provider uses the standard OAuth 2.0 Authorization Code Grant flow
|
|
131
|
+
* with PKCE (Proof Key for Code Exchange) for enhanced security.
|
|
132
|
+
*
|
|
133
|
+
* @param config - Vercel OAuth 2.0 configuration
|
|
134
|
+
* @returns OAuth 2.0 provider configured for Vercel
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* // Basic Vercel authentication (email + profile)
|
|
139
|
+
* const basicVercel = VercelProvider({
|
|
140
|
+
* clientID: process.env.VERCEL_CLIENT_ID,
|
|
141
|
+
* clientSecret: process.env.VERCEL_CLIENT_SECRET,
|
|
142
|
+
* scopes: ["openid", "email", "profile"]
|
|
143
|
+
* })
|
|
144
|
+
*
|
|
145
|
+
* // Vercel with refresh token support
|
|
146
|
+
* const vercelWithRefresh = VercelProvider({
|
|
147
|
+
* clientID: process.env.VERCEL_CLIENT_ID,
|
|
148
|
+
* clientSecret: process.env.VERCEL_CLIENT_SECRET,
|
|
149
|
+
* scopes: ["openid", "email", "profile", "offline_access"]
|
|
150
|
+
* })
|
|
151
|
+
*
|
|
152
|
+
* // Minimal setup (only user ID in ID Token)
|
|
153
|
+
* const minimalVercel = VercelProvider({
|
|
154
|
+
* clientID: process.env.VERCEL_CLIENT_ID,
|
|
155
|
+
* clientSecret: process.env.VERCEL_CLIENT_SECRET,
|
|
156
|
+
* scopes: ["openid"] // Only sub claim in ID Token
|
|
157
|
+
* })
|
|
158
|
+
*
|
|
159
|
+
* // Using the tokens in your app
|
|
160
|
+
* export default issuer({
|
|
161
|
+
* providers: { vercel: vercelWithRefresh },
|
|
162
|
+
* success: async (ctx, value) => {
|
|
163
|
+
* if (value.provider === "vercel") {
|
|
164
|
+
* const idToken = value.tokenset.raw.id_token as string | undefined
|
|
165
|
+
* const accessToken = value.tokenset.access
|
|
166
|
+
* const refreshToken = value.tokenset.refresh
|
|
167
|
+
*
|
|
168
|
+
* if (idToken) {
|
|
169
|
+
* // Decode ID Token to access user claims
|
|
170
|
+
* // (Already validated by oauth2.ts - signature, issuer, audience, exp)
|
|
171
|
+
* const claims = JSON.parse(
|
|
172
|
+
* Buffer.from(idToken.split('.')[1], 'base64').toString()
|
|
173
|
+
* )
|
|
174
|
+
*
|
|
175
|
+
* // Claims available (depending on scopes):
|
|
176
|
+
* // - sub: Vercel user ID (always present)
|
|
177
|
+
* // - email: user@example.com (if email scope)
|
|
178
|
+
* // - name: "John Doe" (if profile scope)
|
|
179
|
+
* // - picture: "https://..." (if profile scope)
|
|
180
|
+
* // - preferred_username: "johndoe" (if profile scope)
|
|
181
|
+
*
|
|
182
|
+
* // Optionally call Vercel API for more data
|
|
183
|
+
* const userRes = await fetch('https://api.vercel.com/v2/user', {
|
|
184
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
185
|
+
* })
|
|
186
|
+
* const user = await userRes.json()
|
|
187
|
+
*
|
|
188
|
+
* // Get user's teams
|
|
189
|
+
* const teamsRes = await fetch('https://api.vercel.com/v2/teams', {
|
|
190
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
191
|
+
* })
|
|
192
|
+
* const teams = await teamsRes.json()
|
|
193
|
+
*
|
|
194
|
+
* return ctx.subject("user", {
|
|
195
|
+
* vercelId: claims.sub,
|
|
196
|
+
* email: claims.email,
|
|
197
|
+
* name: claims.name,
|
|
198
|
+
* username: claims.preferred_username,
|
|
199
|
+
* avatar: claims.picture,
|
|
200
|
+
* teamCount: teams.teams?.length || 0
|
|
201
|
+
* })
|
|
202
|
+
* }
|
|
203
|
+
* }
|
|
204
|
+
* }
|
|
205
|
+
* })
|
|
206
|
+
* ```
|
|
207
|
+
*
|
|
208
|
+
* @remarks
|
|
209
|
+
* - Requires creating a Vercel App in Team Settings → Apps
|
|
210
|
+
* - PKCE is enabled by default for enhanced security
|
|
211
|
+
* - ID Token is automatically validated (signature, issuer, audience, expiration)
|
|
212
|
+
* - Access tokens expire after 1 hour
|
|
213
|
+
* - Refresh tokens rotate on each use and last 30 days
|
|
214
|
+
* - The `openid` scope is required for ID Token issuance
|
|
215
|
+
*/
|
|
216
|
+
const VercelProvider = (config) => {
|
|
217
|
+
return Oauth2Provider({
|
|
218
|
+
...config,
|
|
219
|
+
type: "vercel",
|
|
220
|
+
endpoint: {
|
|
221
|
+
authorization: "https://vercel.com/oauth/authorize",
|
|
222
|
+
token: "https://api.vercel.com/login/oauth/token",
|
|
223
|
+
jwks: "https://vercel.com/.well-known/jwks.json"
|
|
224
|
+
},
|
|
225
|
+
pkce: true
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
export { VercelProvider };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@draftlab/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core implementation for @draftlab/auth",
|
|
6
6
|
"author": "Matheus Pergoli",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^24.10.1",
|
|
41
41
|
"@types/qrcode": "^1.5.6",
|
|
42
|
-
"tsdown": "^0.16.
|
|
42
|
+
"tsdown": "^0.16.8",
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"@draftlab/tsconfig": "0.1.0"
|
|
45
45
|
},
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"preact": "^10.27.2",
|
|
64
64
|
"preact-render-to-string": "^6.6.3",
|
|
65
65
|
"qrcode": "^1.5.4",
|
|
66
|
-
"@draftlab/auth-router": "0.
|
|
66
|
+
"@draftlab/auth-router": "0.3.0"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": ">=18"
|