@draftlab/auth 0.10.4 → 0.12.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/core.d.mts +0 -3
- package/dist/core.mjs +0 -36
- package/dist/toolkit/client.d.mts +169 -0
- package/dist/toolkit/client.mjs +209 -0
- package/dist/toolkit/index.d.mts +9 -0
- package/dist/toolkit/index.mjs +9 -0
- package/dist/toolkit/providers/facebook.d.mts +12 -0
- package/dist/toolkit/providers/facebook.mjs +16 -0
- package/dist/toolkit/providers/github.d.mts +12 -0
- package/dist/toolkit/providers/github.mjs +16 -0
- package/dist/toolkit/providers/google.d.mts +12 -0
- package/dist/toolkit/providers/google.mjs +20 -0
- package/dist/toolkit/providers/strategy.d.mts +40 -0
- package/dist/toolkit/storage.d.mts +145 -0
- package/dist/toolkit/storage.mjs +157 -0
- package/dist/toolkit/utils.d.mts +21 -0
- package/dist/toolkit/utils.mjs +30 -0
- package/package.json +3 -3
- package/dist/plugin/builder.d.mts +0 -27
- package/dist/plugin/builder.mjs +0 -93
- package/dist/plugin/manager.d.mts +0 -62
- package/dist/plugin/manager.mjs +0 -160
- package/dist/plugin/plugin.d.mts +0 -42
- package/dist/plugin/types.d.mts +0 -99
- package/dist/plugin/types.mjs +0 -13
- /package/dist/{plugin/plugin.mjs → toolkit/providers/strategy.mjs} +0 -0
package/dist/core.d.mts
CHANGED
|
@@ -3,7 +3,6 @@ import { UnknownStateError } from "./error.mjs";
|
|
|
3
3
|
import { Prettify } from "./util.mjs";
|
|
4
4
|
import { SubjectPayload, SubjectSchema } from "./subject.mjs";
|
|
5
5
|
import { StorageAdapter } from "./storage/storage.mjs";
|
|
6
|
-
import { Plugin } from "./plugin/types.mjs";
|
|
7
6
|
import { Provider } from "./provider/provider.mjs";
|
|
8
7
|
import { Theme } from "./themes/theme.mjs";
|
|
9
8
|
import { AuthorizationState } from "./types.mjs";
|
|
@@ -61,8 +60,6 @@ interface IssuerInput<Providers extends Record<string, Provider<unknown>>, Subje
|
|
|
61
60
|
error?(error: UnknownStateError, req: Request): Promise<Response>;
|
|
62
61
|
/** Client authorization check function */
|
|
63
62
|
allow?(input: AllowCheckInput, req: Request): Promise<boolean>;
|
|
64
|
-
/** Plugin configuration */
|
|
65
|
-
plugins?: Plugin[];
|
|
66
63
|
/**
|
|
67
64
|
* Refresh callback for updating user claims.
|
|
68
65
|
*
|
package/dist/core.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { validatePKCE } from "./pkce.mjs";
|
|
|
5
5
|
import { generateSecureToken } from "./random.mjs";
|
|
6
6
|
import { Storage } from "./storage/storage.mjs";
|
|
7
7
|
import { encryptionKeys, signingKeys } from "./keys.mjs";
|
|
8
|
-
import { PluginManager } from "./plugin/manager.mjs";
|
|
9
8
|
import { Revocation } from "./revocation.mjs";
|
|
10
9
|
import { setTheme } from "./themes/theme.mjs";
|
|
11
10
|
import { Select } from "./ui/select.mjs";
|
|
@@ -188,15 +187,6 @@ const issuer = (input) => {
|
|
|
188
187
|
const authorization = await getAuthorization(ctx);
|
|
189
188
|
const currentProvider = ctx.get("provider") || "unknown";
|
|
190
189
|
if (!authorization.client_id) throw new Error("client_id is required");
|
|
191
|
-
if (manager) try {
|
|
192
|
-
const subjectProperties = properties && typeof properties === "object" ? properties : {};
|
|
193
|
-
await manager.executeSuccessHooks(authorization.client_id, currentProvider, {
|
|
194
|
-
type: currentProvider,
|
|
195
|
-
properties: subjectProperties
|
|
196
|
-
});
|
|
197
|
-
} catch (error$1) {
|
|
198
|
-
console.error("Plugin success hook failed:", error$1);
|
|
199
|
-
}
|
|
200
190
|
return await input.success({ async subject(type, properties$1, subjectOpts) {
|
|
201
191
|
const subject = subjectOpts?.subject ?? await resolveSubject(type, properties$1);
|
|
202
192
|
await successOpts?.invalidate?.(await resolveSubject(type, properties$1));
|
|
@@ -287,22 +277,6 @@ const issuer = (input) => {
|
|
|
287
277
|
storage
|
|
288
278
|
};
|
|
289
279
|
const app = new Router({ basePath: input.basePath });
|
|
290
|
-
const manager = input.plugins && input.plugins.length > 0 ? new PluginManager(input.storage) : null;
|
|
291
|
-
let pluginsInitialized = false;
|
|
292
|
-
if (manager && input.plugins) {
|
|
293
|
-
manager.registerAll(input.plugins);
|
|
294
|
-
manager.setupRoutes(app);
|
|
295
|
-
app.use(async (c, next) => {
|
|
296
|
-
if (!pluginsInitialized) try {
|
|
297
|
-
await manager.initialize();
|
|
298
|
-
pluginsInitialized = true;
|
|
299
|
-
} catch (error$1) {
|
|
300
|
-
console.error("Plugin initialization failed:", error$1);
|
|
301
|
-
return c.newResponse("Plugin initialization failed", { status: 500 });
|
|
302
|
-
}
|
|
303
|
-
return await next();
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
280
|
for (const [name, value] of Object.entries(input.providers)) {
|
|
307
281
|
const route = new Router();
|
|
308
282
|
route.use(async (c, next) => {
|
|
@@ -590,10 +564,6 @@ const issuer = (input) => {
|
|
|
590
564
|
redirectURI: redirect_uri,
|
|
591
565
|
audience
|
|
592
566
|
}, c.request)) throw new UnauthorizedClientError(client_id, redirect_uri);
|
|
593
|
-
if (manager) {
|
|
594
|
-
const scopes = scope ? scope.split(" ") : void 0;
|
|
595
|
-
await manager.executeAuthorizeHooks(client_id, provider, scopes);
|
|
596
|
-
}
|
|
597
567
|
await auth.set(c, "authorization", 900, authorization);
|
|
598
568
|
if (provider) return c.redirect(`${provider}/authorize`);
|
|
599
569
|
const availableProviders = Object.keys(input.providers);
|
|
@@ -601,12 +571,6 @@ const issuer = (input) => {
|
|
|
601
571
|
return auth.forward(c, await select()(Object.fromEntries(Object.entries(input.providers).map(([key, value]) => [key, value.type])), c.request));
|
|
602
572
|
});
|
|
603
573
|
app.onError(async (err, c) => {
|
|
604
|
-
if (manager) try {
|
|
605
|
-
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
606
|
-
await manager.executeErrorHooks(errorObj);
|
|
607
|
-
} catch (hookError) {
|
|
608
|
-
console.error("Plugin error hook failed:", hookError);
|
|
609
|
-
}
|
|
610
574
|
if (err instanceof UnknownStateError) return auth.forward(c, await error(err, c.request));
|
|
611
575
|
try {
|
|
612
576
|
const authorization = await getAuthorization(c);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { OAuthStrategy } from "./providers/strategy.mjs";
|
|
2
|
+
import { AuthStorage } from "./storage.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/toolkit/client.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for a single OAuth provider.
|
|
8
|
+
*/
|
|
9
|
+
interface ProviderConfig<TStrategy extends OAuthStrategy> {
|
|
10
|
+
/** OAuth strategy defining endpoints and defaults */
|
|
11
|
+
readonly strategy: TStrategy;
|
|
12
|
+
/** OAuth client ID from provider */
|
|
13
|
+
readonly clientId: string;
|
|
14
|
+
/** OAuth client secret from provider */
|
|
15
|
+
readonly clientSecret: string;
|
|
16
|
+
/** Redirect URI registered with provider */
|
|
17
|
+
readonly redirectUri: string;
|
|
18
|
+
/** Optional default scopes for this provider */
|
|
19
|
+
readonly scopes?: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Options for initiating OAuth authorization flow.
|
|
23
|
+
*/
|
|
24
|
+
interface AuthorizeOptions {
|
|
25
|
+
/** Optional scopes to request (overrides provider defaults) */
|
|
26
|
+
readonly scopes?: string[];
|
|
27
|
+
/** Optional additional parameters to include in authorization URL */
|
|
28
|
+
readonly params?: Record<string, string>;
|
|
29
|
+
/** Optional nonce for additional security */
|
|
30
|
+
readonly nonce?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Result of successful OAuth callback handling.
|
|
34
|
+
*/
|
|
35
|
+
interface CallbackResult {
|
|
36
|
+
/** OAuth provider that was used */
|
|
37
|
+
readonly provider: string;
|
|
38
|
+
/** Access token from provider */
|
|
39
|
+
readonly accessToken: string;
|
|
40
|
+
/** Optional refresh token from provider */
|
|
41
|
+
readonly refreshToken?: string;
|
|
42
|
+
/** Token expiration time in seconds */
|
|
43
|
+
readonly expiresIn?: number;
|
|
44
|
+
/** Token type (usually "Bearer") */
|
|
45
|
+
readonly tokenType?: string;
|
|
46
|
+
/** Optional ID token (OpenID Connect) */
|
|
47
|
+
readonly idToken?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* OAuth 2.0 client configuration.
|
|
51
|
+
*/
|
|
52
|
+
interface OAuthClientConfig<TProviders extends Record<string, ProviderConfig<OAuthStrategy>>> {
|
|
53
|
+
/** Provider configurations keyed by provider name */
|
|
54
|
+
readonly providers: TProviders;
|
|
55
|
+
/** Storage adapter for PKCE state (defaults to sessionStorage in browser) */
|
|
56
|
+
readonly storage?: AuthStorage;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* OAuth 2.0 client for managing authentication flows.
|
|
60
|
+
*/
|
|
61
|
+
interface OAuthClient<TProviders extends Record<string, ProviderConfig<OAuthStrategy>>> {
|
|
62
|
+
/**
|
|
63
|
+
* Initiate OAuth authorization flow.
|
|
64
|
+
*
|
|
65
|
+
* @param provider - Provider name (key from providers config)
|
|
66
|
+
* @param options - Authorization options
|
|
67
|
+
* @returns Authorization URL to redirect user to
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* // Basic usage
|
|
72
|
+
* const { url } = await client.authorize('github')
|
|
73
|
+
* window.location.href = url
|
|
74
|
+
*
|
|
75
|
+
* // With custom scopes
|
|
76
|
+
* const { url } = await client.authorize('google', {
|
|
77
|
+
* scopes: ['openid', 'email', 'profile']
|
|
78
|
+
* })
|
|
79
|
+
*
|
|
80
|
+
* // With additional params
|
|
81
|
+
* const { url } = await client.authorize('github', {
|
|
82
|
+
* params: { prompt: 'consent' }
|
|
83
|
+
* })
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
authorize(provider: (string & {}) | keyof TProviders, options?: AuthorizeOptions): Promise<{
|
|
87
|
+
url: string;
|
|
88
|
+
state: string;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Handle OAuth callback and exchange code for tokens.
|
|
92
|
+
*
|
|
93
|
+
* @param callbackUrl - Full callback URL with query parameters
|
|
94
|
+
* @returns Token exchange result
|
|
95
|
+
*
|
|
96
|
+
* @throws {Error} If callback URL is invalid, state mismatch, or token exchange fails
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* // Client-side
|
|
101
|
+
* const result = await client.handleCallback(window.location.href)
|
|
102
|
+
* console.log(result.accessToken, result.provider)
|
|
103
|
+
*
|
|
104
|
+
* // Server-side (Next.js)
|
|
105
|
+
* export async function GET(req: Request) {
|
|
106
|
+
* const result = await client.handleCallback(req.url)
|
|
107
|
+
* // Store tokens, create session, etc.
|
|
108
|
+
* return Response.redirect('/')
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
handleCallback(callbackUrl: string): Promise<CallbackResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Get user info from OAuth provider using access token.
|
|
115
|
+
*
|
|
116
|
+
* @param provider - Provider name
|
|
117
|
+
* @param accessToken - Access token from provider
|
|
118
|
+
* @returns User info from provider
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const userInfo = await client.getUserInfo('github', accessToken)
|
|
123
|
+
* console.log(userInfo.email, userInfo.name)
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
getUserInfo(provider: (string & {}) | keyof TProviders, accessToken: string): Promise<Record<string, unknown>>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates an OAuth 2.0 client for managing authentication flows.
|
|
130
|
+
*
|
|
131
|
+
* Supports PKCE (Proof Key for Code Exchange) for enhanced security.
|
|
132
|
+
* Works in both client-side (browser) and server-side (Node.js) environments.
|
|
133
|
+
*
|
|
134
|
+
* @param config - OAuth client configuration
|
|
135
|
+
* @returns OAuth client instance
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* import { createOAuthClient } from '@draftlab/auth/toolkit/client'
|
|
140
|
+
* import { GitHubStrategy, GoogleStrategy } from '@draftlab/auth/toolkit/providers'
|
|
141
|
+
*
|
|
142
|
+
* const client = createOAuthClient({
|
|
143
|
+
* providers: {
|
|
144
|
+
* github: {
|
|
145
|
+
* strategy: GitHubStrategy,
|
|
146
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
147
|
+
* clientSecret: 'YOUR_CLIENT_SECRET',
|
|
148
|
+
* redirectUri: 'http://localhost:3000/auth/callback'
|
|
149
|
+
* },
|
|
150
|
+
* google: {
|
|
151
|
+
* strategy: GoogleStrategy,
|
|
152
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
153
|
+
* clientSecret: 'YOUR_CLIENT_SECRET',
|
|
154
|
+
* redirectUri: 'http://localhost:3000/auth/callback',
|
|
155
|
+
* scopes: ['openid', 'email', 'profile']
|
|
156
|
+
* }
|
|
157
|
+
* }
|
|
158
|
+
* })
|
|
159
|
+
*
|
|
160
|
+
* // Initiate login
|
|
161
|
+
* const { url } = await client.authorize('github')
|
|
162
|
+
*
|
|
163
|
+
* // Handle callback
|
|
164
|
+
* const result = await client.handleCallback(callbackUrl)
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
declare const createOAuthClient: <TProviders extends Record<string, ProviderConfig<OAuthStrategy>>>(config: OAuthClientConfig<TProviders>) => OAuthClient<TProviders>;
|
|
168
|
+
//#endregion
|
|
169
|
+
export { AuthorizeOptions, CallbackResult, OAuthClient, OAuthClientConfig, ProviderConfig, createOAuthClient };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { generatePKCE } from "../pkce.mjs";
|
|
2
|
+
import { createSessionStorage } from "./storage.mjs";
|
|
3
|
+
import { generateSecureRandom } from "./utils.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/toolkit/client.ts
|
|
6
|
+
/**
|
|
7
|
+
* Lightweight OAuth 2.0 client toolkit for DraftAuth.
|
|
8
|
+
*
|
|
9
|
+
* Provides a simple, framework-agnostic way to implement OAuth 2.0 authentication
|
|
10
|
+
* with PKCE support. Works in both client-side (SPA) and server-side (Next.js, Remix) environments.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Client-side SPA (React, Vue, Solid, etc.)
|
|
15
|
+
* import { createOAuthClient } from '@draftlab/auth/toolkit/client'
|
|
16
|
+
* import { GitHubStrategy, GoogleStrategy } from '@draftlab/auth/toolkit/providers'
|
|
17
|
+
* import { createSessionStorage } from '@draftlab/auth/toolkit/storage'
|
|
18
|
+
*
|
|
19
|
+
* const client = createOAuthClient({
|
|
20
|
+
* providers: {
|
|
21
|
+
* github: {
|
|
22
|
+
* strategy: GitHubStrategy,
|
|
23
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
24
|
+
* clientSecret: 'YOUR_CLIENT_SECRET',
|
|
25
|
+
* redirectUri: 'http://localhost:3000/auth/callback'
|
|
26
|
+
* },
|
|
27
|
+
* google: {
|
|
28
|
+
* strategy: GoogleStrategy,
|
|
29
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
30
|
+
* clientSecret: 'YOUR_CLIENT_SECRET',
|
|
31
|
+
* redirectUri: 'http://localhost:3000/auth/callback'
|
|
32
|
+
* }
|
|
33
|
+
* },
|
|
34
|
+
* storage: createSessionStorage()
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* // Initiate login
|
|
38
|
+
* const { url } = await client.authorize('github', { scopes: ['user:email'] })
|
|
39
|
+
* window.location.href = url
|
|
40
|
+
*
|
|
41
|
+
* // Handle callback
|
|
42
|
+
* const result = await client.handleCallback(window.location.href)
|
|
43
|
+
* console.log(result.accessToken, result.provider)
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* // Server-side (Next.js App Router)
|
|
49
|
+
* import { createOAuthClient } from '@draftlab/auth/toolkit/client'
|
|
50
|
+
* import { GitHubStrategy } from '@draftlab/auth/toolkit/providers'
|
|
51
|
+
* import { createCookieStorage } from '@draftlab/auth/toolkit/storage'
|
|
52
|
+
* import { cookies } from 'next/headers'
|
|
53
|
+
*
|
|
54
|
+
* export async function GET(req: Request) {
|
|
55
|
+
* const client = createOAuthClient({
|
|
56
|
+
* providers: {
|
|
57
|
+
* github: {
|
|
58
|
+
* strategy: GitHubStrategy,
|
|
59
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
60
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
61
|
+
* redirectUri: 'https://myapp.com/auth/callback'
|
|
62
|
+
* }
|
|
63
|
+
* },
|
|
64
|
+
* storage: createCookieStorage({
|
|
65
|
+
* getCookie: (name) => cookies().get(name)?.value ?? null,
|
|
66
|
+
* setCookie: (name, value, opts) => cookies().set(name, value, opts),
|
|
67
|
+
* deleteCookie: (name) => cookies().delete(name)
|
|
68
|
+
* })
|
|
69
|
+
* })
|
|
70
|
+
*
|
|
71
|
+
* const { url } = await client.authorize('github')
|
|
72
|
+
* return Response.redirect(url)
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Creates an OAuth 2.0 client for managing authentication flows.
|
|
78
|
+
*
|
|
79
|
+
* Supports PKCE (Proof Key for Code Exchange) for enhanced security.
|
|
80
|
+
* Works in both client-side (browser) and server-side (Node.js) environments.
|
|
81
|
+
*
|
|
82
|
+
* @param config - OAuth client configuration
|
|
83
|
+
* @returns OAuth client instance
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { createOAuthClient } from '@draftlab/auth/toolkit/client'
|
|
88
|
+
* import { GitHubStrategy, GoogleStrategy } from '@draftlab/auth/toolkit/providers'
|
|
89
|
+
*
|
|
90
|
+
* const client = createOAuthClient({
|
|
91
|
+
* providers: {
|
|
92
|
+
* github: {
|
|
93
|
+
* strategy: GitHubStrategy,
|
|
94
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
95
|
+
* clientSecret: 'YOUR_CLIENT_SECRET',
|
|
96
|
+
* redirectUri: 'http://localhost:3000/auth/callback'
|
|
97
|
+
* },
|
|
98
|
+
* google: {
|
|
99
|
+
* strategy: GoogleStrategy,
|
|
100
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
101
|
+
* clientSecret: 'YOUR_CLIENT_SECRET',
|
|
102
|
+
* redirectUri: 'http://localhost:3000/auth/callback',
|
|
103
|
+
* scopes: ['openid', 'email', 'profile']
|
|
104
|
+
* }
|
|
105
|
+
* }
|
|
106
|
+
* })
|
|
107
|
+
*
|
|
108
|
+
* // Initiate login
|
|
109
|
+
* const { url } = await client.authorize('github')
|
|
110
|
+
*
|
|
111
|
+
* // Handle callback
|
|
112
|
+
* const result = await client.handleCallback(callbackUrl)
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
const createOAuthClient = (config) => {
|
|
116
|
+
const storage = config.storage || (typeof sessionStorage !== "undefined" ? createSessionStorage() : null);
|
|
117
|
+
if (!storage) throw new Error("No storage adapter provided. Please provide a storage adapter for server-side environments.");
|
|
118
|
+
return {
|
|
119
|
+
async authorize(provider, options) {
|
|
120
|
+
const providerConfig = config.providers[provider];
|
|
121
|
+
if (!providerConfig) throw new Error(`Provider '${String(provider)}' not configured`);
|
|
122
|
+
const pkce = await generatePKCE();
|
|
123
|
+
const state = generateSecureRandom(16);
|
|
124
|
+
await storage.set({
|
|
125
|
+
state,
|
|
126
|
+
verifier: pkce.verifier,
|
|
127
|
+
provider: String(provider),
|
|
128
|
+
nonce: options?.nonce
|
|
129
|
+
});
|
|
130
|
+
const scopes = options?.scopes || providerConfig.scopes || providerConfig.strategy.scopes;
|
|
131
|
+
const params = new URLSearchParams({
|
|
132
|
+
client_id: providerConfig.clientId,
|
|
133
|
+
redirect_uri: providerConfig.redirectUri,
|
|
134
|
+
response_type: "code",
|
|
135
|
+
scope: Array.isArray(scopes) ? scopes.join(" ") : scopes,
|
|
136
|
+
state,
|
|
137
|
+
code_challenge: pkce.challenge,
|
|
138
|
+
code_challenge_method: pkce.method,
|
|
139
|
+
...options?.params
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
url: `${providerConfig.strategy.authorizationEndpoint}?${params.toString()}`,
|
|
143
|
+
state
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
async handleCallback(callbackUrl) {
|
|
147
|
+
const url = new URL(callbackUrl);
|
|
148
|
+
const code = url.searchParams.get("code");
|
|
149
|
+
const state = url.searchParams.get("state");
|
|
150
|
+
const error = url.searchParams.get("error");
|
|
151
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
152
|
+
if (error) throw new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`);
|
|
153
|
+
if (!code || !state) throw new Error("Invalid callback URL: missing code or state parameter");
|
|
154
|
+
const storedState = await storage.get();
|
|
155
|
+
await storage.clear();
|
|
156
|
+
if (!storedState) throw new Error("No stored PKCE state found. OAuth flow may have expired or been tampered with.");
|
|
157
|
+
if (state !== storedState.state) throw new Error("State mismatch. Possible CSRF attack detected.");
|
|
158
|
+
const providerConfig = config.providers[storedState.provider];
|
|
159
|
+
if (!providerConfig) throw new Error(`Provider '${storedState.provider}' from callback not configured`);
|
|
160
|
+
const tokenParams = new URLSearchParams({
|
|
161
|
+
grant_type: "authorization_code",
|
|
162
|
+
code,
|
|
163
|
+
redirect_uri: providerConfig.redirectUri,
|
|
164
|
+
client_id: providerConfig.clientId,
|
|
165
|
+
client_secret: providerConfig.clientSecret,
|
|
166
|
+
code_verifier: storedState.verifier
|
|
167
|
+
});
|
|
168
|
+
const tokenResponse = await fetch(providerConfig.strategy.tokenEndpoint, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
172
|
+
Accept: "application/json"
|
|
173
|
+
},
|
|
174
|
+
body: tokenParams
|
|
175
|
+
});
|
|
176
|
+
if (!tokenResponse.ok) {
|
|
177
|
+
const errorText = await tokenResponse.text();
|
|
178
|
+
throw new Error(`Token exchange failed (${tokenResponse.status}): ${errorText}`);
|
|
179
|
+
}
|
|
180
|
+
const tokenData = await tokenResponse.json();
|
|
181
|
+
if (!tokenData.access_token) throw new Error("No access token in provider response");
|
|
182
|
+
return {
|
|
183
|
+
provider: storedState.provider,
|
|
184
|
+
accessToken: tokenData.access_token,
|
|
185
|
+
refreshToken: tokenData.refresh_token,
|
|
186
|
+
expiresIn: tokenData.expires_in,
|
|
187
|
+
tokenType: tokenData.token_type,
|
|
188
|
+
idToken: tokenData.id_token
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
async getUserInfo(provider, accessToken) {
|
|
192
|
+
const providerConfig = config.providers[provider];
|
|
193
|
+
if (!providerConfig) throw new Error(`Provider '${String(provider)}' not configured`);
|
|
194
|
+
if (!providerConfig.strategy.userInfoEndpoint) throw new Error(`Provider '${String(provider)}' does not support user info endpoint`);
|
|
195
|
+
const response = await fetch(providerConfig.strategy.userInfoEndpoint, { headers: {
|
|
196
|
+
Authorization: `Bearer ${accessToken}`,
|
|
197
|
+
Accept: "application/json"
|
|
198
|
+
} });
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const errorText = await response.text();
|
|
201
|
+
throw new Error(`Failed to fetch user info (${response.status}): ${errorText}`);
|
|
202
|
+
}
|
|
203
|
+
return response.json();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
//#endregion
|
|
209
|
+
export { createOAuthClient };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { generatePKCE } from "../pkce.mjs";
|
|
2
|
+
import { OAuth2TokenResponse, OAuthStrategy } from "./providers/strategy.mjs";
|
|
3
|
+
import { AuthStorage, PKCEState, createCookieStorage, createLocalStorage, createMemoryStorage, createSessionStorage } from "./storage.mjs";
|
|
4
|
+
import { AuthorizeOptions, CallbackResult, OAuthClient, OAuthClientConfig, ProviderConfig, createOAuthClient } from "./client.mjs";
|
|
5
|
+
import { FacebookStrategy } from "./providers/facebook.mjs";
|
|
6
|
+
import { GitHubStrategy } from "./providers/github.mjs";
|
|
7
|
+
import { GoogleStrategy } from "./providers/google.mjs";
|
|
8
|
+
import { generateSecureRandom } from "./utils.mjs";
|
|
9
|
+
export { type AuthStorage, type AuthorizeOptions, type CallbackResult, FacebookStrategy, GitHubStrategy, GoogleStrategy, type OAuth2TokenResponse, type OAuthClient, type OAuthClientConfig, type OAuthStrategy, type PKCEState, type ProviderConfig, createCookieStorage, createLocalStorage, createMemoryStorage, createOAuthClient, createSessionStorage, generatePKCE, generateSecureRandom };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { generatePKCE } from "../pkce.mjs";
|
|
2
|
+
import { createCookieStorage, createLocalStorage, createMemoryStorage, createSessionStorage } from "./storage.mjs";
|
|
3
|
+
import { generateSecureRandom } from "./utils.mjs";
|
|
4
|
+
import { createOAuthClient } from "./client.mjs";
|
|
5
|
+
import { FacebookStrategy } from "./providers/facebook.mjs";
|
|
6
|
+
import { GitHubStrategy } from "./providers/github.mjs";
|
|
7
|
+
import { GoogleStrategy } from "./providers/google.mjs";
|
|
8
|
+
|
|
9
|
+
export { FacebookStrategy, GitHubStrategy, GoogleStrategy, createCookieStorage, createLocalStorage, createMemoryStorage, createOAuthClient, createSessionStorage, generatePKCE, generateSecureRandom };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OAuthStrategy } from "./strategy.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/toolkit/providers/facebook.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Facebook OAuth 2.0 strategy.
|
|
7
|
+
*
|
|
8
|
+
* @see https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow
|
|
9
|
+
*/
|
|
10
|
+
declare const FacebookStrategy: OAuthStrategy;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { FacebookStrategy };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/toolkit/providers/facebook.ts
|
|
2
|
+
/**
|
|
3
|
+
* Facebook OAuth 2.0 strategy.
|
|
4
|
+
*
|
|
5
|
+
* @see https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow
|
|
6
|
+
*/
|
|
7
|
+
const FacebookStrategy = {
|
|
8
|
+
name: "facebook",
|
|
9
|
+
authorizationEndpoint: "https://www.facebook.com/v23.0/dialog/oauth",
|
|
10
|
+
tokenEndpoint: "https://graph.facebook.com/v23.0/oauth/access_token",
|
|
11
|
+
userInfoEndpoint: "https://graph.facebook.com/me?fields=id,name,email",
|
|
12
|
+
scopes: ["public_profile", "email"]
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { FacebookStrategy };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OAuthStrategy } from "./strategy.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/toolkit/providers/github.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GitHub OAuth 2.0 strategy.
|
|
7
|
+
*
|
|
8
|
+
* @see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
|
|
9
|
+
*/
|
|
10
|
+
declare const GitHubStrategy: OAuthStrategy;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { GitHubStrategy };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/toolkit/providers/github.ts
|
|
2
|
+
/**
|
|
3
|
+
* GitHub OAuth 2.0 strategy.
|
|
4
|
+
*
|
|
5
|
+
* @see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
|
|
6
|
+
*/
|
|
7
|
+
const GitHubStrategy = {
|
|
8
|
+
name: "github",
|
|
9
|
+
authorizationEndpoint: "https://github.com/login/oauth/authorize",
|
|
10
|
+
tokenEndpoint: "https://github.com/login/oauth/access_token",
|
|
11
|
+
userInfoEndpoint: "https://api.github.com/user",
|
|
12
|
+
scopes: ["read:user", "user:email"]
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { GitHubStrategy };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OAuthStrategy } from "./strategy.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/toolkit/providers/google.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Google OAuth 2.0 / OpenID Connect strategy.
|
|
7
|
+
*
|
|
8
|
+
* @see https://developers.google.com/identity/protocols/oauth2
|
|
9
|
+
*/
|
|
10
|
+
declare const GoogleStrategy: OAuthStrategy;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { GoogleStrategy };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/toolkit/providers/google.ts
|
|
2
|
+
/**
|
|
3
|
+
* Google OAuth 2.0 / OpenID Connect strategy.
|
|
4
|
+
*
|
|
5
|
+
* @see https://developers.google.com/identity/protocols/oauth2
|
|
6
|
+
*/
|
|
7
|
+
const GoogleStrategy = {
|
|
8
|
+
name: "google",
|
|
9
|
+
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
10
|
+
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
11
|
+
userInfoEndpoint: "https://www.googleapis.com/oauth2/v3/userinfo",
|
|
12
|
+
scopes: [
|
|
13
|
+
"openid",
|
|
14
|
+
"email",
|
|
15
|
+
"profile"
|
|
16
|
+
]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { GoogleStrategy };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/toolkit/providers/strategy.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* OAuth 2.0 strategy interface and types for provider configurations.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* OAuth 2.0 token response from provider's token endpoint.
|
|
7
|
+
* Based on RFC 6749 Section 5.1.
|
|
8
|
+
*/
|
|
9
|
+
interface OAuth2TokenResponse {
|
|
10
|
+
/** The access token issued by the authorization server */
|
|
11
|
+
access_token: string;
|
|
12
|
+
/** The type of token (usually "Bearer") */
|
|
13
|
+
token_type?: string;
|
|
14
|
+
/** The lifetime in seconds of the access token */
|
|
15
|
+
expires_in?: number;
|
|
16
|
+
/** The refresh token for obtaining new access tokens */
|
|
17
|
+
refresh_token?: string;
|
|
18
|
+
/** The scope of the access token (space-separated list) */
|
|
19
|
+
scope?: string;
|
|
20
|
+
/** OpenID Connect ID token (JWT) */
|
|
21
|
+
id_token?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* OAuth 2.0 provider strategy definition.
|
|
25
|
+
* Defines the endpoints and default configuration for an OAuth provider.
|
|
26
|
+
*/
|
|
27
|
+
interface OAuthStrategy {
|
|
28
|
+
/** Provider name (e.g., "github", "google") */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** OAuth authorization endpoint URL */
|
|
31
|
+
readonly authorizationEndpoint: string;
|
|
32
|
+
/** OAuth token exchange endpoint URL */
|
|
33
|
+
readonly tokenEndpoint: string;
|
|
34
|
+
/** Optional user info endpoint URL (for OpenID Connect) */
|
|
35
|
+
readonly userInfoEndpoint?: string;
|
|
36
|
+
/** Default scopes to request */
|
|
37
|
+
readonly scopes: string[];
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { OAuth2TokenResponse, OAuthStrategy };
|