@draftlab/auth 0.5.0 → 0.7.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.d.mts CHANGED
@@ -252,8 +252,24 @@ interface VerifyResult<T extends SubjectSchema> {
252
252
  } }[keyof T];
253
253
  }
254
254
  /**
255
- * Options for UserInfo requests.
255
+ * Options for token revocation.
256
256
  */
257
+ interface RevokeOptions {
258
+ /**
259
+ * Optional hint about the token type.
260
+ * Can be "access_token" or "refresh_token".
261
+ *
262
+ * Helps the server optimize token lookup.
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * {
267
+ * tokenTypeHint: "refresh_token"
268
+ * }
269
+ * ```
270
+ */
271
+ tokenTypeHint?: "access_token" | "refresh_token";
272
+ }
257
273
  /**
258
274
  * Draft Auth client with OAuth 2.0 operations.
259
275
  */
@@ -392,6 +408,33 @@ interface Client {
392
408
  * ```
393
409
  */
394
410
  verify<T extends SubjectSchema>(subjects: T, token: string, options?: VerifyOptions): Promise<Result<VerifyResult<T>, InvalidRefreshTokenError | InvalidAccessTokenError | InvalidSubjectError>>;
411
+ /**
412
+ * Revoke a token (access or refresh token).
413
+ *
414
+ * Once revoked, the token cannot be used to access resources or refresh.
415
+ * Useful for implementing logout functionality.
416
+ *
417
+ * @param token - The token to revoke
418
+ * @param opts - Additional revocation options
419
+ * @returns Empty result on success
420
+ *
421
+ * @example Logout with refresh token revocation
422
+ * ```ts
423
+ * const result = await client.revoke(refreshToken, {
424
+ * tokenTypeHint: "refresh_token"
425
+ * })
426
+ *
427
+ * if (result.success) {
428
+ * // Token revoked successfully, user is logged out
429
+ * clearStoredTokens()
430
+ * redirectToHome()
431
+ * } else {
432
+ * // Revocation failed, but still clear tokens on client
433
+ * clearStoredTokens()
434
+ * }
435
+ * ```
436
+ */
437
+ revoke(token: string, opts?: RevokeOptions): Promise<Result<void>>;
395
438
  }
396
439
  /**
397
440
  * Create a Draft Auth client.
@@ -409,4 +452,4 @@ interface Client {
409
452
  */
410
453
  declare const createClient: (input: ClientInput) => Client;
411
454
  //#endregion
412
- export { AuthorizeOptions, AuthorizeResult, Challenge, Client, ClientInput, RefreshOptions, Result, Tokens, VerifyOptions, VerifyResult, WellKnown, createClient };
455
+ export { AuthorizeOptions, AuthorizeResult, Challenge, Client, ClientInput, RefreshOptions, Result, RevokeOptions, Tokens, VerifyOptions, VerifyResult, WellKnown, createClient };
package/dist/client.mjs CHANGED
@@ -245,6 +245,32 @@ const createClient = (input) => {
245
245
  error: new InvalidAccessTokenError()
246
246
  };
247
247
  }
248
+ },
249
+ async revoke(token, opts) {
250
+ try {
251
+ const wk = await getIssuer();
252
+ const body = new URLSearchParams({
253
+ token,
254
+ ...opts?.tokenTypeHint ? { token_type_hint: opts.tokenTypeHint } : {}
255
+ });
256
+ if ((await f(wk.token_endpoint.replace("/token", "/revoke"), {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
259
+ body: body.toString()
260
+ })).ok) return {
261
+ success: true,
262
+ data: void 0
263
+ };
264
+ return {
265
+ success: false,
266
+ error: /* @__PURE__ */ new Error("Failed to revoke token")
267
+ };
268
+ } catch (error) {
269
+ return {
270
+ success: false,
271
+ error
272
+ };
273
+ }
248
274
  }
249
275
  };
250
276
  return client;
package/dist/core.mjs CHANGED
@@ -6,6 +6,7 @@ import { generateSecureToken } from "./random.mjs";
6
6
  import { Storage } from "./storage/storage.mjs";
7
7
  import { encryptionKeys, signingKeys } from "./keys.mjs";
8
8
  import { PluginManager } from "./plugin/manager.mjs";
9
+ import { Revocation } from "./revocation.mjs";
9
10
  import { setTheme } from "./themes/theme.mjs";
10
11
  import { Select } from "./ui/select.mjs";
11
12
  import { CompactEncrypt, SignJWT, compactDecrypt } from "jose";
@@ -384,7 +385,12 @@ const issuer = (input) => {
384
385
  const error$1 = new OauthError("invalid_request", "Missing refresh_token");
385
386
  return c.json(error$1.toJSON(), { status: 400 });
386
387
  }
387
- const splits = refreshToken.toString().split(":");
388
+ const refreshTokenStr = refreshToken.toString();
389
+ if (await Revocation.isRevoked(storage, refreshTokenStr)) {
390
+ const error$1 = new OauthError("invalid_grant", "Refresh token has been revoked");
391
+ return c.json(error$1.toJSON(), { status: 400 });
392
+ }
393
+ const splits = refreshTokenStr.split(":");
388
394
  const token = splits.pop();
389
395
  if (!token) throw new Error("Invalid refresh token format");
390
396
  const subject = splits.join(":");
@@ -458,6 +464,31 @@ const issuer = (input) => {
458
464
  }, { status: 400 });
459
465
  }
460
466
  });
467
+ app.post("/revoke", {
468
+ middleware: [cors({
469
+ origin: "*",
470
+ allowHeaders: ["Content-Type"],
471
+ allowMethods: ["POST"],
472
+ credentials: false
473
+ })],
474
+ handler: async (c) => {
475
+ const token = (await c.formData()).get("token")?.toString();
476
+ if (!token) {
477
+ const error$1 = new OauthError("invalid_request", "Missing token parameter");
478
+ return c.json(error$1.toJSON(), { status: 400 });
479
+ }
480
+ try {
481
+ const expiresAt = Date.now() + ttlRefresh * 1e3;
482
+ await Revocation.revoke(storage, token, expiresAt);
483
+ return c.json({});
484
+ } catch (_err) {
485
+ return c.json({
486
+ error: "server_error",
487
+ error_description: "Token revocation failed"
488
+ }, { status: 500 });
489
+ }
490
+ }
491
+ });
461
492
  app.get("/authorize", async (c) => {
462
493
  const provider = c.query("provider");
463
494
  const response_type = c.query("response_type");
@@ -0,0 +1,105 @@
1
+ import { Provider } from "./provider.mjs";
2
+ import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
3
+
4
+ //#region src/provider/apple.d.ts
5
+
6
+ /**
7
+ * Configuration options for Apple OAuth 2.0 provider.
8
+ * Extends the base OAuth 2.0 configuration with Apple-specific documentation.
9
+ */
10
+ interface AppleConfig extends Oauth2WrappedConfig {
11
+ /**
12
+ * Apple Service ID (app identifier for your Sign in with Apple implementation).
13
+ * Get this from your Apple Developer account when creating a Service ID.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * {
18
+ * clientID: "com.example.app.signin"
19
+ * }
20
+ * ```
21
+ */
22
+ readonly clientID: string;
23
+ /**
24
+ * Apple client secret (JWT token signed with your private key).
25
+ * This is different from other providers - Apple requires a JWT token
26
+ * generated from your private key.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * {
31
+ * clientSecret: process.env.APPLE_CLIENT_SECRET
32
+ * }
33
+ * ```
34
+ */
35
+ readonly clientSecret: string;
36
+ /**
37
+ * Apple OAuth scopes to request access for.
38
+ * Apple only supports "name" and "email" scopes.
39
+ *
40
+ * Important: Apple only provides user data (name, email) on the FIRST authorization.
41
+ * Subsequent authorizations won't include this data.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * {
46
+ * scopes: ["name", "email"]
47
+ * }
48
+ * ```
49
+ */
50
+ readonly scopes: string[];
51
+ }
52
+ /**
53
+ * Creates an Apple OAuth 2.0 authentication provider.
54
+ * Allows users to authenticate using their Apple accounts.
55
+ *
56
+ * @param config - Apple OAuth 2.0 configuration
57
+ * @returns OAuth 2.0 provider configured for Apple
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // Basic Apple authentication
62
+ * const basicApple = AppleProvider({
63
+ * clientID: process.env.APPLE_CLIENT_ID,
64
+ * clientSecret: process.env.APPLE_CLIENT_SECRET
65
+ * })
66
+ *
67
+ * // Apple with name and email scopes
68
+ * const appleWithScopes = AppleProvider({
69
+ * clientID: process.env.APPLE_CLIENT_ID,
70
+ * clientSecret: process.env.APPLE_CLIENT_SECRET,
71
+ * scopes: ["name", "email"]
72
+ * })
73
+ *
74
+ * // Using the tokens and id_token
75
+ * export default issuer({
76
+ * providers: { apple: appleWithScopes },
77
+ * success: async (ctx, value) => {
78
+ * if (value.provider === "apple") {
79
+ * // Apple returns user data in the initial authorization response
80
+ * // You need to decode the id_token to extract user information
81
+ *
82
+ * // The id_token contains:
83
+ * // - sub: unique Apple user identifier
84
+ * // - email: user email (only on first authorization)
85
+ * // - email_verified: whether email is verified
86
+ * // - is_private_email: whether user used private relay
87
+ *
88
+ * // Decode and verify the id_token using jose:
89
+ * // const verified = await jwtVerify(value.tokenset.id, jwks)
90
+ * // const user = verified.payload
91
+ *
92
+ * return ctx.subject("user", {
93
+ * appleId: user.sub,
94
+ * email: user.email,
95
+ * emailVerified: user.email_verified,
96
+ * isPrivateEmail: user.is_private_email
97
+ * })
98
+ * }
99
+ * }
100
+ * })
101
+ * ```
102
+ */
103
+ declare const AppleProvider: (config: AppleConfig) => Provider<Oauth2UserData>;
104
+ //#endregion
105
+ export { AppleConfig, AppleProvider };
@@ -0,0 +1,151 @@
1
+ import { Oauth2Provider } from "./oauth2.mjs";
2
+
3
+ //#region src/provider/apple.ts
4
+ /**
5
+ * Apple authentication provider for Draft Auth.
6
+ * Implements OAuth 2.0 flow for authenticating users with their Apple accounts.
7
+ *
8
+ * ## Quick Setup
9
+ *
10
+ * ```ts
11
+ * import { AppleProvider } from "@draftlab/auth/provider/apple"
12
+ *
13
+ * export default issuer({
14
+ * providers: {
15
+ * apple: AppleProvider({
16
+ * clientID: process.env.APPLE_CLIENT_ID,
17
+ * clientSecret: process.env.APPLE_CLIENT_SECRET,
18
+ * scopes: ["name", "email"]
19
+ * })
20
+ * }
21
+ * })
22
+ * ```
23
+ *
24
+ * ## Setup Instructions
25
+ *
26
+ * ### 1. Create App ID
27
+ * - Go to [Apple Developer](https://developer.apple.com)
28
+ * - Create a new App ID with "Sign in with Apple" capability
29
+ *
30
+ * ### 2. Create Service ID
31
+ * - Create a new Service ID (this is your clientID)
32
+ * - Configure "Sign in with Apple"
33
+ * - Add your redirect URI
34
+ *
35
+ * ### 3. Create Private Key
36
+ * - Create a private key for "Sign in with Apple"
37
+ * - Download the .p8 file (this is used to create your clientSecret)
38
+ *
39
+ * ## Client Secret Generation
40
+ *
41
+ * Apple requires a JWT token as the client secret. You'll need:
42
+ * - Key ID from the private key
43
+ * - Team ID from your Apple Developer account
44
+ * - Private key (.p8 file)
45
+ *
46
+ * Use a library to generate the JWT (valid for ~15 minutes):
47
+ *
48
+ * ```ts
49
+ * import { SignJWT } from "jose"
50
+ *
51
+ * const secret = await new SignJWT({
52
+ * iss: "YOUR_TEAM_ID",
53
+ * aud: "https://appleid.apple.com",
54
+ * sub: process.env.APPLE_CLIENT_ID,
55
+ * iat: Math.floor(Date.now() / 1000),
56
+ * exp: Math.floor(Date.now() / 1000) + 15 * 60
57
+ * })
58
+ * .setProtectedHeader({ alg: "ES256", kid: "YOUR_KEY_ID" })
59
+ * .sign(privateKey)
60
+ * ```
61
+ *
62
+ * ## Common Scopes
63
+ *
64
+ * - `name` - Access user's name (first and last name)
65
+ * - `email` - Access user's email address
66
+ *
67
+ * Note: Apple only returns user data on the first authorization. Subsequent authorizations won't include name/email.
68
+ *
69
+ * ## User Data Access
70
+ *
71
+ * ```ts
72
+ * success: async (ctx, value) => {
73
+ * if (value.provider === "apple") {
74
+ * const accessToken = value.tokenset.access
75
+ *
76
+ * // Apple doesn't provide a userinfo endpoint
77
+ * // User data is returned in the authorization response
78
+ * // You need to parse the id_token JWT to get user info
79
+ *
80
+ * // For subsequent logins without name/email, use the subject (user_id)
81
+ * // from the ID token to identify the user
82
+ * }
83
+ * }
84
+ * ```
85
+ *
86
+ * @packageDocumentation
87
+ */
88
+ /**
89
+ * Creates an Apple OAuth 2.0 authentication provider.
90
+ * Allows users to authenticate using their Apple accounts.
91
+ *
92
+ * @param config - Apple OAuth 2.0 configuration
93
+ * @returns OAuth 2.0 provider configured for Apple
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * // Basic Apple authentication
98
+ * const basicApple = AppleProvider({
99
+ * clientID: process.env.APPLE_CLIENT_ID,
100
+ * clientSecret: process.env.APPLE_CLIENT_SECRET
101
+ * })
102
+ *
103
+ * // Apple with name and email scopes
104
+ * const appleWithScopes = AppleProvider({
105
+ * clientID: process.env.APPLE_CLIENT_ID,
106
+ * clientSecret: process.env.APPLE_CLIENT_SECRET,
107
+ * scopes: ["name", "email"]
108
+ * })
109
+ *
110
+ * // Using the tokens and id_token
111
+ * export default issuer({
112
+ * providers: { apple: appleWithScopes },
113
+ * success: async (ctx, value) => {
114
+ * if (value.provider === "apple") {
115
+ * // Apple returns user data in the initial authorization response
116
+ * // You need to decode the id_token to extract user information
117
+ *
118
+ * // The id_token contains:
119
+ * // - sub: unique Apple user identifier
120
+ * // - email: user email (only on first authorization)
121
+ * // - email_verified: whether email is verified
122
+ * // - is_private_email: whether user used private relay
123
+ *
124
+ * // Decode and verify the id_token using jose:
125
+ * // const verified = await jwtVerify(value.tokenset.id, jwks)
126
+ * // const user = verified.payload
127
+ *
128
+ * return ctx.subject("user", {
129
+ * appleId: user.sub,
130
+ * email: user.email,
131
+ * emailVerified: user.email_verified,
132
+ * isPrivateEmail: user.is_private_email
133
+ * })
134
+ * }
135
+ * }
136
+ * })
137
+ * ```
138
+ */
139
+ const AppleProvider = (config) => {
140
+ return Oauth2Provider({
141
+ ...config,
142
+ type: "apple",
143
+ endpoint: {
144
+ authorization: "https://appleid.apple.com/auth/authorize",
145
+ token: "https://appleid.apple.com/auth/token"
146
+ }
147
+ });
148
+ };
149
+
150
+ //#endregion
151
+ export { AppleProvider };
@@ -0,0 +1,100 @@
1
+ import { Provider } from "./provider.mjs";
2
+ import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
3
+
4
+ //#region src/provider/gitlab.d.ts
5
+
6
+ /**
7
+ * Configuration options for GitLab OAuth 2.0 provider.
8
+ * Extends the base OAuth 2.0 configuration with GitLab-specific documentation.
9
+ */
10
+ interface GitlabConfig extends Oauth2WrappedConfig {
11
+ /**
12
+ * GitLab application client ID.
13
+ * Get this from your GitLab application settings.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * {
18
+ * clientID: "abcdef123456"
19
+ * }
20
+ * ```
21
+ */
22
+ readonly clientID: string;
23
+ /**
24
+ * GitLab application client secret.
25
+ * Keep this secure and never expose it to client-side code.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * {
30
+ * clientSecret: process.env.GITLAB_CLIENT_SECRET
31
+ * }
32
+ * ```
33
+ */
34
+ readonly clientSecret: string;
35
+ /**
36
+ * GitLab OAuth scopes to request access for.
37
+ * Determines what data and actions your app can access.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * {
42
+ * scopes: [
43
+ * "read_user", // Access user profile
44
+ * "read_api", // Read-access to API
45
+ * "read_repository" // Access repositories
46
+ * ]
47
+ * }
48
+ * ```
49
+ */
50
+ readonly scopes: string[];
51
+ }
52
+ /**
53
+ * Creates a GitLab OAuth 2.0 authentication provider.
54
+ * Allows users to authenticate using their GitLab accounts (gitlab.com or self-hosted).
55
+ *
56
+ * @param config - GitLab OAuth 2.0 configuration
57
+ * @returns OAuth 2.0 provider configured for GitLab
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // Basic GitLab.com authentication
62
+ * const basicGitlab = GitlabProvider({
63
+ * clientID: process.env.GITLAB_CLIENT_ID,
64
+ * clientSecret: process.env.GITLAB_CLIENT_SECRET
65
+ * })
66
+ *
67
+ * // GitLab with read access
68
+ * const gitlabWithRead = GitlabProvider({
69
+ * clientID: process.env.GITLAB_CLIENT_ID,
70
+ * clientSecret: process.env.GITLAB_CLIENT_SECRET,
71
+ * scopes: ["read_user", "read_api"]
72
+ * })
73
+ *
74
+ * // Using the access token to fetch user data
75
+ * export default issuer({
76
+ * providers: { gitlab: gitlabWithRead },
77
+ * success: async (ctx, value) => {
78
+ * if (value.provider === "gitlab") {
79
+ * const token = value.tokenset.access
80
+ *
81
+ * const userRes = await fetch('https://gitlab.com/api/v4/user', {
82
+ * headers: { Authorization: `Bearer ${token}` }
83
+ * })
84
+ * const user = await userRes.json()
85
+ *
86
+ * return ctx.subject("user", {
87
+ * gitlabId: user.id,
88
+ * username: user.username,
89
+ * email: user.email,
90
+ * name: user.name,
91
+ * avatar: user.avatar_url
92
+ * })
93
+ * }
94
+ * }
95
+ * })
96
+ * ```
97
+ */
98
+ declare const GitlabProvider: (config: GitlabConfig) => Provider<Oauth2UserData>;
99
+ //#endregion
100
+ export { GitlabConfig, GitlabProvider };
@@ -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 };