@moriajs/auth 0.3.5 → 0.4.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/.turbo/turbo-build.log +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -2
- package/dist/index.js.map +1 -1
- package/dist/providers/github.d.ts +27 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +115 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/google.d.ts +27 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +86 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/types.d.ts +43 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +7 -0
- package/dist/providers/types.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +119 -2
- package/src/providers/github.ts +139 -0
- package/src/providers/google.ts +105 -0
- package/src/providers/types.ts +45 -0
package/.turbo/turbo-build.log
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pluggable authentication system for MoriaJS.
|
|
5
5
|
* Default: JWT + httpOnly cookies.
|
|
6
|
-
*
|
|
6
|
+
* Providers: Google OAuth, GitHub OAuth (built-in).
|
|
7
7
|
*/
|
|
8
8
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
9
|
+
import type { OAuthProvider } from './providers/types.js';
|
|
10
|
+
export { googleProvider } from './providers/google.js';
|
|
11
|
+
export { githubProvider } from './providers/github.js';
|
|
12
|
+
export type { OAuthProviderConfig, OAuthProvider } from './providers/types.js';
|
|
9
13
|
/**
|
|
10
14
|
* User payload stored in JWT token.
|
|
11
15
|
* Extend this via TypeScript module augmentation in your app.
|
|
@@ -32,6 +36,12 @@ export interface AuthConfig {
|
|
|
32
36
|
cookiePath?: string;
|
|
33
37
|
/** SameSite cookie attribute (default: 'lax') */
|
|
34
38
|
sameSite?: 'strict' | 'lax' | 'none';
|
|
39
|
+
/** OAuth providers (Google, GitHub, etc.) */
|
|
40
|
+
providers?: OAuthProvider[];
|
|
41
|
+
/** Default redirect after successful OAuth (default: '/') */
|
|
42
|
+
successRedirect?: string;
|
|
43
|
+
/** Default redirect after failed OAuth (default: '/') */
|
|
44
|
+
failureRedirect?: string;
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
37
47
|
* Auth provider interface for pluggable authentication strategies.
|
|
@@ -52,12 +62,22 @@ export interface AuthProvider {
|
|
|
52
62
|
* @example
|
|
53
63
|
* ```ts
|
|
54
64
|
* import { createApp } from '@moriajs/core';
|
|
55
|
-
* import { createAuthPlugin } from '@moriajs/auth';
|
|
65
|
+
* import { createAuthPlugin, googleProvider, githubProvider } from '@moriajs/auth';
|
|
56
66
|
*
|
|
57
67
|
* const app = await createApp();
|
|
58
68
|
* await app.use(createAuthPlugin({
|
|
59
69
|
* secret: process.env.JWT_SECRET!,
|
|
60
70
|
* expiresIn: '24h',
|
|
71
|
+
* providers: [
|
|
72
|
+
* googleProvider({
|
|
73
|
+
* clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
74
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
75
|
+
* }),
|
|
76
|
+
* githubProvider({
|
|
77
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
78
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
79
|
+
* }),
|
|
80
|
+
* ],
|
|
61
81
|
* }));
|
|
62
82
|
* ```
|
|
63
83
|
*/
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAmB,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAmB,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE/E;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACrB,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACvB,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACrC,6CAA6C;IAC7C,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,MAAM,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAC9D,wCAAwC;IACxC,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,iCAAiC;IACjC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU;;yBAIhB;QAAE,MAAM,EAAE,GAAG,CAAA;KAAE;EA8CjD;AAsFD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,IACrC,SAAS,cAAc,EAAE,OAAO,YAAY,wBAe7D"}
|
package/dist/index.js
CHANGED
|
@@ -3,20 +3,34 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pluggable authentication system for MoriaJS.
|
|
5
5
|
* Default: JWT + httpOnly cookies.
|
|
6
|
-
*
|
|
6
|
+
* Providers: Google OAuth, GitHub OAuth (built-in).
|
|
7
7
|
*/
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
// ─── Re-exports ──────────────────────────────────────
|
|
10
|
+
export { googleProvider } from './providers/google.js';
|
|
11
|
+
export { githubProvider } from './providers/github.js';
|
|
8
12
|
/**
|
|
9
13
|
* Create the JWT auth plugin for MoriaJS.
|
|
10
14
|
*
|
|
11
15
|
* @example
|
|
12
16
|
* ```ts
|
|
13
17
|
* import { createApp } from '@moriajs/core';
|
|
14
|
-
* import { createAuthPlugin } from '@moriajs/auth';
|
|
18
|
+
* import { createAuthPlugin, googleProvider, githubProvider } from '@moriajs/auth';
|
|
15
19
|
*
|
|
16
20
|
* const app = await createApp();
|
|
17
21
|
* await app.use(createAuthPlugin({
|
|
18
22
|
* secret: process.env.JWT_SECRET!,
|
|
19
23
|
* expiresIn: '24h',
|
|
24
|
+
* providers: [
|
|
25
|
+
* googleProvider({
|
|
26
|
+
* clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
27
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
28
|
+
* }),
|
|
29
|
+
* githubProvider({
|
|
30
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
31
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
32
|
+
* }),
|
|
33
|
+
* ],
|
|
20
34
|
* }));
|
|
21
35
|
* ```
|
|
22
36
|
*/
|
|
@@ -43,10 +57,83 @@ export function createAuthPlugin(config) {
|
|
|
43
57
|
server.decorate('signOut', async (_request, reply) => {
|
|
44
58
|
reply.header('Set-Cookie', `${config.cookieName ?? 'moria_token'}=; HttpOnly; Path=${config.cookiePath ?? '/'}; Max-Age=0`);
|
|
45
59
|
});
|
|
60
|
+
// ─── Register OAuth providers ────────────────────
|
|
61
|
+
if (config.providers && config.providers.length > 0) {
|
|
62
|
+
registerOAuthRoutes(server, config);
|
|
63
|
+
}
|
|
46
64
|
server.log.info('@moriajs/auth: JWT auth plugin registered');
|
|
65
|
+
if (config.providers?.length) {
|
|
66
|
+
const names = config.providers.map((p) => p.name).join(', ');
|
|
67
|
+
server.log.info(`@moriajs/auth: OAuth providers registered: ${names}`);
|
|
68
|
+
}
|
|
47
69
|
},
|
|
48
70
|
};
|
|
49
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Register OAuth redirect + callback routes for each provider.
|
|
74
|
+
*/
|
|
75
|
+
function registerOAuthRoutes(server, config) {
|
|
76
|
+
for (const provider of config.providers ?? []) {
|
|
77
|
+
const authPath = `/auth/${provider.name}`;
|
|
78
|
+
const callbackPath = provider.callbackPath;
|
|
79
|
+
// GET /auth/:provider → Redirect to OAuth consent screen
|
|
80
|
+
server.get(authPath, async (request, reply) => {
|
|
81
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
82
|
+
// Store state in a short-lived cookie for CSRF protection
|
|
83
|
+
reply.header('Set-Cookie', `moria_oauth_state=${state}; HttpOnly; Path=/; Max-Age=600; SameSite=Lax`);
|
|
84
|
+
// Build the full callback URL from the request
|
|
85
|
+
const protocol = request.protocol ?? 'http';
|
|
86
|
+
const host = request.hostname;
|
|
87
|
+
const fullCallbackUrl = `${protocol}://${host}${callbackPath}`;
|
|
88
|
+
// Get auth URL and inject the full callback URL
|
|
89
|
+
let authUrl = provider.getAuthUrl(state);
|
|
90
|
+
authUrl = authUrl.replace('redirect_uri=', `redirect_uri=${encodeURIComponent(fullCallbackUrl)}`);
|
|
91
|
+
return reply.redirect(authUrl);
|
|
92
|
+
});
|
|
93
|
+
// GET /auth/:provider/callback → Exchange code, issue JWT
|
|
94
|
+
server.get(callbackPath, async (request, reply) => {
|
|
95
|
+
const query = request.query;
|
|
96
|
+
const failureUrl = provider.failureRedirect ?? config.failureRedirect ?? '/';
|
|
97
|
+
const successUrl = provider.successRedirect ?? config.successRedirect ?? '/';
|
|
98
|
+
// Check for OAuth errors
|
|
99
|
+
if (query.error || !query.code) {
|
|
100
|
+
request.log.warn(`OAuth ${provider.name} error: ${query.error ?? 'no code'}`);
|
|
101
|
+
return reply.redirect(failureUrl);
|
|
102
|
+
}
|
|
103
|
+
// Validate state (CSRF protection)
|
|
104
|
+
const cookieHeader = request.headers.cookie ?? '';
|
|
105
|
+
const stateCookie = cookieHeader
|
|
106
|
+
.split(';')
|
|
107
|
+
.map((c) => c.trim())
|
|
108
|
+
.find((c) => c.startsWith('moria_oauth_state='));
|
|
109
|
+
const savedState = stateCookie?.split('=')[1];
|
|
110
|
+
if (!savedState || savedState !== query.state) {
|
|
111
|
+
request.log.warn(`OAuth ${provider.name}: state mismatch`);
|
|
112
|
+
return reply.redirect(failureUrl);
|
|
113
|
+
}
|
|
114
|
+
// Clear state cookie
|
|
115
|
+
reply.header('Set-Cookie', 'moria_oauth_state=; HttpOnly; Path=/; Max-Age=0');
|
|
116
|
+
try {
|
|
117
|
+
// Build full callback URL
|
|
118
|
+
const protocol = request.protocol ?? 'http';
|
|
119
|
+
const host = request.hostname;
|
|
120
|
+
const fullCallbackUrl = `${protocol}://${host}${callbackPath}`;
|
|
121
|
+
// Exchange code for access token
|
|
122
|
+
const accessToken = await provider.exchangeCode(query.code, fullCallbackUrl);
|
|
123
|
+
// Fetch user profile
|
|
124
|
+
const user = await provider.fetchProfile(accessToken);
|
|
125
|
+
// Sign JWT and set cookie
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
+
await server.signIn(user, reply);
|
|
128
|
+
return reply.redirect(successUrl);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
request.log.error(err, `OAuth ${provider.name} callback failed`);
|
|
132
|
+
return reply.redirect(failureUrl);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
50
137
|
/**
|
|
51
138
|
* Route-level authentication guard.
|
|
52
139
|
* Use as a Fastify preHandler hook.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth2 Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements the Authorization Code flow for GitHub.
|
|
5
|
+
* Uses Node.js built-in fetch — zero additional dependencies.
|
|
6
|
+
*/
|
|
7
|
+
import type { OAuthProviderConfig, OAuthProvider } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create a GitHub OAuth provider.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createAuthPlugin, githubProvider } from '@moriajs/auth';
|
|
14
|
+
*
|
|
15
|
+
* await app.use(createAuthPlugin({
|
|
16
|
+
* secret: process.env.JWT_SECRET!,
|
|
17
|
+
* providers: [
|
|
18
|
+
* githubProvider({
|
|
19
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
20
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
21
|
+
* }),
|
|
22
|
+
* ],
|
|
23
|
+
* }));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function githubProvider(config: OAuthProviderConfig): OAuthProvider;
|
|
27
|
+
//# sourceMappingURL=github.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/providers/github.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AASrE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAuGzE"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth2 Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements the Authorization Code flow for GitHub.
|
|
5
|
+
* Uses Node.js built-in fetch — zero additional dependencies.
|
|
6
|
+
*/
|
|
7
|
+
const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize';
|
|
8
|
+
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
9
|
+
const GITHUB_PROFILE_URL = 'https://api.github.com/user';
|
|
10
|
+
const GITHUB_EMAILS_URL = 'https://api.github.com/user/emails';
|
|
11
|
+
const DEFAULT_SCOPES = ['read:user', 'user:email'];
|
|
12
|
+
/**
|
|
13
|
+
* Create a GitHub OAuth provider.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { createAuthPlugin, githubProvider } from '@moriajs/auth';
|
|
18
|
+
*
|
|
19
|
+
* await app.use(createAuthPlugin({
|
|
20
|
+
* secret: process.env.JWT_SECRET!,
|
|
21
|
+
* providers: [
|
|
22
|
+
* githubProvider({
|
|
23
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
24
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
25
|
+
* }),
|
|
26
|
+
* ],
|
|
27
|
+
* }));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function githubProvider(config) {
|
|
31
|
+
const scopes = config.scopes ?? DEFAULT_SCOPES;
|
|
32
|
+
const callbackPath = config.callbackUrl ?? '/auth/github/callback';
|
|
33
|
+
return {
|
|
34
|
+
name: 'github',
|
|
35
|
+
callbackPath,
|
|
36
|
+
successRedirect: config.successRedirect ?? '/',
|
|
37
|
+
failureRedirect: config.failureRedirect ?? '/',
|
|
38
|
+
getAuthUrl(state) {
|
|
39
|
+
const params = new URLSearchParams({
|
|
40
|
+
client_id: config.clientId,
|
|
41
|
+
redirect_uri: '', // Will be set at runtime with full URL
|
|
42
|
+
scope: scopes.join(' '),
|
|
43
|
+
state,
|
|
44
|
+
});
|
|
45
|
+
return `${GITHUB_AUTH_URL}?${params.toString()}`;
|
|
46
|
+
},
|
|
47
|
+
async exchangeCode(code, callbackUrl) {
|
|
48
|
+
const res = await fetch(GITHUB_TOKEN_URL, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
Accept: 'application/json',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
client_id: config.clientId,
|
|
56
|
+
client_secret: config.clientSecret,
|
|
57
|
+
code,
|
|
58
|
+
redirect_uri: callbackUrl,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const error = await res.text();
|
|
63
|
+
throw new Error(`GitHub token exchange failed: ${error}`);
|
|
64
|
+
}
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
if (data.error) {
|
|
67
|
+
throw new Error(`GitHub OAuth error: ${data.error}`);
|
|
68
|
+
}
|
|
69
|
+
return data.access_token;
|
|
70
|
+
},
|
|
71
|
+
async fetchProfile(accessToken) {
|
|
72
|
+
// Fetch profile
|
|
73
|
+
const profileRes = await fetch(GITHUB_PROFILE_URL, {
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${accessToken}`,
|
|
76
|
+
Accept: 'application/vnd.github+json',
|
|
77
|
+
'User-Agent': 'MoriaJS',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (!profileRes.ok) {
|
|
81
|
+
throw new Error('Failed to fetch GitHub profile');
|
|
82
|
+
}
|
|
83
|
+
const profile = await profileRes.json();
|
|
84
|
+
// If email is not public, fetch from /user/emails
|
|
85
|
+
let email = profile.email;
|
|
86
|
+
if (!email) {
|
|
87
|
+
try {
|
|
88
|
+
const emailsRes = await fetch(GITHUB_EMAILS_URL, {
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${accessToken}`,
|
|
91
|
+
Accept: 'application/vnd.github+json',
|
|
92
|
+
'User-Agent': 'MoriaJS',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
if (emailsRes.ok) {
|
|
96
|
+
const emails = await emailsRes.json();
|
|
97
|
+
const primary = emails.find((e) => e.primary && e.verified);
|
|
98
|
+
email = primary?.email ?? emails[0]?.email ?? null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Email fetch failed, continue without
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
id: profile.id,
|
|
107
|
+
email: email ?? undefined,
|
|
108
|
+
name: profile.name ?? profile.login,
|
|
109
|
+
avatar: profile.avatar_url,
|
|
110
|
+
provider: 'github',
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/providers/github.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements the Authorization Code flow for Google.
|
|
5
|
+
* Uses Node.js built-in fetch — zero additional dependencies.
|
|
6
|
+
*/
|
|
7
|
+
import type { OAuthProviderConfig, OAuthProvider } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create a Google OAuth provider.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createAuthPlugin, googleProvider } from '@moriajs/auth';
|
|
14
|
+
*
|
|
15
|
+
* await app.use(createAuthPlugin({
|
|
16
|
+
* secret: process.env.JWT_SECRET!,
|
|
17
|
+
* providers: [
|
|
18
|
+
* googleProvider({
|
|
19
|
+
* clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
20
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
21
|
+
* }),
|
|
22
|
+
* ],
|
|
23
|
+
* }));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function googleProvider(config: OAuthProviderConfig): OAuthProvider;
|
|
27
|
+
//# sourceMappingURL=google.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../src/providers/google.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAQrE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAsEzE"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements the Authorization Code flow for Google.
|
|
5
|
+
* Uses Node.js built-in fetch — zero additional dependencies.
|
|
6
|
+
*/
|
|
7
|
+
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
8
|
+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
9
|
+
const GOOGLE_PROFILE_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
|
|
10
|
+
const DEFAULT_SCOPES = ['openid', 'email', 'profile'];
|
|
11
|
+
/**
|
|
12
|
+
* Create a Google OAuth provider.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { createAuthPlugin, googleProvider } from '@moriajs/auth';
|
|
17
|
+
*
|
|
18
|
+
* await app.use(createAuthPlugin({
|
|
19
|
+
* secret: process.env.JWT_SECRET!,
|
|
20
|
+
* providers: [
|
|
21
|
+
* googleProvider({
|
|
22
|
+
* clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
23
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
24
|
+
* }),
|
|
25
|
+
* ],
|
|
26
|
+
* }));
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function googleProvider(config) {
|
|
30
|
+
const scopes = config.scopes ?? DEFAULT_SCOPES;
|
|
31
|
+
const callbackPath = config.callbackUrl ?? '/auth/google/callback';
|
|
32
|
+
return {
|
|
33
|
+
name: 'google',
|
|
34
|
+
callbackPath,
|
|
35
|
+
successRedirect: config.successRedirect ?? '/',
|
|
36
|
+
failureRedirect: config.failureRedirect ?? '/',
|
|
37
|
+
getAuthUrl(state) {
|
|
38
|
+
const params = new URLSearchParams({
|
|
39
|
+
client_id: config.clientId,
|
|
40
|
+
redirect_uri: '', // Will be set at runtime with full URL
|
|
41
|
+
response_type: 'code',
|
|
42
|
+
scope: scopes.join(' '),
|
|
43
|
+
state,
|
|
44
|
+
access_type: 'offline',
|
|
45
|
+
prompt: 'consent',
|
|
46
|
+
});
|
|
47
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
48
|
+
},
|
|
49
|
+
async exchangeCode(code, callbackUrl) {
|
|
50
|
+
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
53
|
+
body: new URLSearchParams({
|
|
54
|
+
code,
|
|
55
|
+
client_id: config.clientId,
|
|
56
|
+
client_secret: config.clientSecret,
|
|
57
|
+
redirect_uri: callbackUrl,
|
|
58
|
+
grant_type: 'authorization_code',
|
|
59
|
+
}).toString(),
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const error = await res.text();
|
|
63
|
+
throw new Error(`Google token exchange failed: ${error}`);
|
|
64
|
+
}
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
return data.access_token;
|
|
67
|
+
},
|
|
68
|
+
async fetchProfile(accessToken) {
|
|
69
|
+
const res = await fetch(GOOGLE_PROFILE_URL, {
|
|
70
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
throw new Error('Failed to fetch Google profile');
|
|
74
|
+
}
|
|
75
|
+
const profile = await res.json();
|
|
76
|
+
return {
|
|
77
|
+
id: profile.id,
|
|
78
|
+
email: profile.email,
|
|
79
|
+
name: profile.name,
|
|
80
|
+
avatar: profile.picture,
|
|
81
|
+
provider: 'google',
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=google.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google.js","sourceRoot":"","sources":["../../src/providers/google.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,eAAe,GAAG,8CAA8C,CAAC;AACvE,MAAM,gBAAgB,GAAG,qCAAqC,CAAC;AAC/D,MAAM,kBAAkB,GAAG,+CAA+C,CAAC;AAE3E,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAAC,MAA2B;IACtD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,cAAc,CAAC;IAC/C,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,IAAI,uBAAuB,CAAC;IAEnE,OAAO;QACH,IAAI,EAAE,QAAQ;QACd,YAAY;QACZ,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,GAAG;QAC9C,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,GAAG;QAE9C,UAAU,CAAC,KAAa;YACpB,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;gBAC/B,SAAS,EAAE,MAAM,CAAC,QAAQ;gBAC1B,YAAY,EAAE,EAAE,EAAE,uCAAuC;gBACzD,aAAa,EAAE,MAAM;gBACrB,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;gBACvB,KAAK;gBACL,WAAW,EAAE,SAAS;gBACtB,MAAM,EAAE,SAAS;aACpB,CAAC,CAAC;YACH,OAAO,GAAG,eAAe,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACrD,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,IAAY,EAAE,WAAmB;YAChD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,gBAAgB,EAAE;gBACtC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;gBAChE,IAAI,EAAE,IAAI,eAAe,CAAC;oBACtB,IAAI;oBACJ,SAAS,EAAE,MAAM,CAAC,QAAQ;oBAC1B,aAAa,EAAE,MAAM,CAAC,YAAY;oBAClC,YAAY,EAAE,WAAW;oBACzB,UAAU,EAAE,oBAAoB;iBACnC,CAAC,CAAC,QAAQ,EAAE;aAChB,CAAC,CAAC;YAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACV,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;gBAC/B,MAAM,IAAI,KAAK,CAAC,iCAAiC,KAAK,EAAE,CAAC,CAAC;YAC9D,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAA8B,CAAC;YAC1D,OAAO,IAAI,CAAC,YAAY,CAAC;QAC7B,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,WAAmB;YAClC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,kBAAkB,EAAE;gBACxC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE;aACtD,CAAC,CAAC;YAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;YACtD,CAAC;YAED,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,EAK7B,CAAC;YAEF,OAAO;gBACH,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,MAAM,EAAE,OAAO,CAAC,OAAO;gBACvB,QAAQ,EAAE,QAAQ;aACrB,CAAC;QACN,CAAC;KACJ,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Provider Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for all OAuth providers in MoriaJS.
|
|
5
|
+
*/
|
|
6
|
+
import type { AuthUser } from '../index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for an OAuth provider.
|
|
9
|
+
*/
|
|
10
|
+
export interface OAuthProviderConfig {
|
|
11
|
+
/** OAuth2 client ID */
|
|
12
|
+
clientId: string;
|
|
13
|
+
/** OAuth2 client secret */
|
|
14
|
+
clientSecret: string;
|
|
15
|
+
/** Callback URL path (e.g., '/auth/google/callback') */
|
|
16
|
+
callbackUrl?: string;
|
|
17
|
+
/** OAuth2 scopes to request */
|
|
18
|
+
scopes?: string[];
|
|
19
|
+
/** URL to redirect to after successful auth (default: '/') */
|
|
20
|
+
successRedirect?: string;
|
|
21
|
+
/** URL to redirect to after failed auth (default: '/') */
|
|
22
|
+
failureRedirect?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* An OAuth provider registers redirect + callback routes
|
|
26
|
+
* and maps external profiles to MoriaJS AuthUser.
|
|
27
|
+
*/
|
|
28
|
+
export interface OAuthProvider {
|
|
29
|
+
/** Provider name (e.g., 'google', 'github') */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Build the authorization redirect URL */
|
|
32
|
+
getAuthUrl(state: string): string;
|
|
33
|
+
/** Exchange auth code for access token */
|
|
34
|
+
exchangeCode(code: string, callbackUrl: string): Promise<string>;
|
|
35
|
+
/** Fetch user profile using access token */
|
|
36
|
+
fetchProfile(accessToken: string): Promise<AuthUser>;
|
|
37
|
+
/** The configured callback URL path */
|
|
38
|
+
callbackPath: string;
|
|
39
|
+
/** Redirect paths */
|
|
40
|
+
successRedirect: string;
|
|
41
|
+
failureRedirect: string;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/providers/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAClC,0CAA0C;IAC1C,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjE,4CAA4C;IAC5C,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrD,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CAC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/providers/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moriajs/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MoriaJS auth — JWT + httpOnly cookies, pluggable auth system",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"fastify": "^5.2.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@moriajs/core": "0.
|
|
25
|
+
"@moriajs/core": "0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"author": "Guntur-D <guntur.d.npm@gmail.com>",
|
package/src/index.ts
CHANGED
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pluggable authentication system for MoriaJS.
|
|
5
5
|
* Default: JWT + httpOnly cookies.
|
|
6
|
-
*
|
|
6
|
+
* Providers: Google OAuth, GitHub OAuth (built-in).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
10
|
+
import type { OAuthProvider } from './providers/types.js';
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
// ─── Re-exports ──────────────────────────────────────
|
|
14
|
+
export { googleProvider } from './providers/google.js';
|
|
15
|
+
export { githubProvider } from './providers/github.js';
|
|
16
|
+
export type { OAuthProviderConfig, OAuthProvider } from './providers/types.js';
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
19
|
* User payload stored in JWT token.
|
|
@@ -35,6 +42,12 @@ export interface AuthConfig {
|
|
|
35
42
|
cookiePath?: string;
|
|
36
43
|
/** SameSite cookie attribute (default: 'lax') */
|
|
37
44
|
sameSite?: 'strict' | 'lax' | 'none';
|
|
45
|
+
/** OAuth providers (Google, GitHub, etc.) */
|
|
46
|
+
providers?: OAuthProvider[];
|
|
47
|
+
/** Default redirect after successful OAuth (default: '/') */
|
|
48
|
+
successRedirect?: string;
|
|
49
|
+
/** Default redirect after failed OAuth (default: '/') */
|
|
50
|
+
failureRedirect?: string;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
/**
|
|
@@ -57,12 +70,22 @@ export interface AuthProvider {
|
|
|
57
70
|
* @example
|
|
58
71
|
* ```ts
|
|
59
72
|
* import { createApp } from '@moriajs/core';
|
|
60
|
-
* import { createAuthPlugin } from '@moriajs/auth';
|
|
73
|
+
* import { createAuthPlugin, googleProvider, githubProvider } from '@moriajs/auth';
|
|
61
74
|
*
|
|
62
75
|
* const app = await createApp();
|
|
63
76
|
* await app.use(createAuthPlugin({
|
|
64
77
|
* secret: process.env.JWT_SECRET!,
|
|
65
78
|
* expiresIn: '24h',
|
|
79
|
+
* providers: [
|
|
80
|
+
* googleProvider({
|
|
81
|
+
* clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
82
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
83
|
+
* }),
|
|
84
|
+
* githubProvider({
|
|
85
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
86
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
87
|
+
* }),
|
|
88
|
+
* ],
|
|
66
89
|
* }));
|
|
67
90
|
* ```
|
|
68
91
|
*/
|
|
@@ -103,11 +126,105 @@ export function createAuthPlugin(config: AuthConfig) {
|
|
|
103
126
|
);
|
|
104
127
|
});
|
|
105
128
|
|
|
129
|
+
// ─── Register OAuth providers ────────────────────
|
|
130
|
+
if (config.providers && config.providers.length > 0) {
|
|
131
|
+
registerOAuthRoutes(server as FastifyInstance, config);
|
|
132
|
+
}
|
|
133
|
+
|
|
106
134
|
(server as FastifyInstance).log.info('@moriajs/auth: JWT auth plugin registered');
|
|
135
|
+
|
|
136
|
+
if (config.providers?.length) {
|
|
137
|
+
const names = config.providers.map((p) => p.name).join(', ');
|
|
138
|
+
(server as FastifyInstance).log.info(`@moriajs/auth: OAuth providers registered: ${names}`);
|
|
139
|
+
}
|
|
107
140
|
},
|
|
108
141
|
};
|
|
109
142
|
}
|
|
110
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Register OAuth redirect + callback routes for each provider.
|
|
146
|
+
*/
|
|
147
|
+
function registerOAuthRoutes(server: FastifyInstance, config: AuthConfig) {
|
|
148
|
+
for (const provider of config.providers ?? []) {
|
|
149
|
+
const authPath = `/auth/${provider.name}`;
|
|
150
|
+
const callbackPath = provider.callbackPath;
|
|
151
|
+
|
|
152
|
+
// GET /auth/:provider → Redirect to OAuth consent screen
|
|
153
|
+
server.get(authPath, async (request, reply) => {
|
|
154
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
155
|
+
|
|
156
|
+
// Store state in a short-lived cookie for CSRF protection
|
|
157
|
+
reply.header('Set-Cookie',
|
|
158
|
+
`moria_oauth_state=${state}; HttpOnly; Path=/; Max-Age=600; SameSite=Lax`
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Build the full callback URL from the request
|
|
162
|
+
const protocol = request.protocol ?? 'http';
|
|
163
|
+
const host = request.hostname;
|
|
164
|
+
const fullCallbackUrl = `${protocol}://${host}${callbackPath}`;
|
|
165
|
+
|
|
166
|
+
// Get auth URL and inject the full callback URL
|
|
167
|
+
let authUrl = provider.getAuthUrl(state);
|
|
168
|
+
authUrl = authUrl.replace('redirect_uri=', `redirect_uri=${encodeURIComponent(fullCallbackUrl)}`);
|
|
169
|
+
|
|
170
|
+
return reply.redirect(authUrl);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// GET /auth/:provider/callback → Exchange code, issue JWT
|
|
174
|
+
server.get(callbackPath, async (request, reply) => {
|
|
175
|
+
const query = request.query as { code?: string; state?: string; error?: string };
|
|
176
|
+
const failureUrl = provider.failureRedirect ?? config.failureRedirect ?? '/';
|
|
177
|
+
const successUrl = provider.successRedirect ?? config.successRedirect ?? '/';
|
|
178
|
+
|
|
179
|
+
// Check for OAuth errors
|
|
180
|
+
if (query.error || !query.code) {
|
|
181
|
+
request.log.warn(`OAuth ${provider.name} error: ${query.error ?? 'no code'}`);
|
|
182
|
+
return reply.redirect(failureUrl);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Validate state (CSRF protection)
|
|
186
|
+
const cookieHeader = request.headers.cookie ?? '';
|
|
187
|
+
const stateCookie = cookieHeader
|
|
188
|
+
.split(';')
|
|
189
|
+
.map((c) => c.trim())
|
|
190
|
+
.find((c) => c.startsWith('moria_oauth_state='));
|
|
191
|
+
const savedState = stateCookie?.split('=')[1];
|
|
192
|
+
|
|
193
|
+
if (!savedState || savedState !== query.state) {
|
|
194
|
+
request.log.warn(`OAuth ${provider.name}: state mismatch`);
|
|
195
|
+
return reply.redirect(failureUrl);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Clear state cookie
|
|
199
|
+
reply.header('Set-Cookie',
|
|
200
|
+
'moria_oauth_state=; HttpOnly; Path=/; Max-Age=0'
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Build full callback URL
|
|
205
|
+
const protocol = request.protocol ?? 'http';
|
|
206
|
+
const host = request.hostname;
|
|
207
|
+
const fullCallbackUrl = `${protocol}://${host}${callbackPath}`;
|
|
208
|
+
|
|
209
|
+
// Exchange code for access token
|
|
210
|
+
const accessToken = await provider.exchangeCode(query.code, fullCallbackUrl);
|
|
211
|
+
|
|
212
|
+
// Fetch user profile
|
|
213
|
+
const user = await provider.fetchProfile(accessToken);
|
|
214
|
+
|
|
215
|
+
// Sign JWT and set cookie
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
217
|
+
await (server as any).signIn(user, reply);
|
|
218
|
+
|
|
219
|
+
return reply.redirect(successUrl);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
request.log.error(err, `OAuth ${provider.name} callback failed`);
|
|
222
|
+
return reply.redirect(failureUrl);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
111
228
|
/**
|
|
112
229
|
* Route-level authentication guard.
|
|
113
230
|
* Use as a Fastify preHandler hook.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth2 Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements the Authorization Code flow for GitHub.
|
|
5
|
+
* Uses Node.js built-in fetch — zero additional dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AuthUser } from '../index.js';
|
|
9
|
+
import type { OAuthProviderConfig, OAuthProvider } from './types.js';
|
|
10
|
+
|
|
11
|
+
const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize';
|
|
12
|
+
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
13
|
+
const GITHUB_PROFILE_URL = 'https://api.github.com/user';
|
|
14
|
+
const GITHUB_EMAILS_URL = 'https://api.github.com/user/emails';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_SCOPES = ['read:user', 'user:email'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a GitHub OAuth provider.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { createAuthPlugin, githubProvider } from '@moriajs/auth';
|
|
24
|
+
*
|
|
25
|
+
* await app.use(createAuthPlugin({
|
|
26
|
+
* secret: process.env.JWT_SECRET!,
|
|
27
|
+
* providers: [
|
|
28
|
+
* githubProvider({
|
|
29
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
30
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
31
|
+
* }),
|
|
32
|
+
* ],
|
|
33
|
+
* }));
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function githubProvider(config: OAuthProviderConfig): OAuthProvider {
|
|
37
|
+
const scopes = config.scopes ?? DEFAULT_SCOPES;
|
|
38
|
+
const callbackPath = config.callbackUrl ?? '/auth/github/callback';
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
name: 'github',
|
|
42
|
+
callbackPath,
|
|
43
|
+
successRedirect: config.successRedirect ?? '/',
|
|
44
|
+
failureRedirect: config.failureRedirect ?? '/',
|
|
45
|
+
|
|
46
|
+
getAuthUrl(state: string): string {
|
|
47
|
+
const params = new URLSearchParams({
|
|
48
|
+
client_id: config.clientId,
|
|
49
|
+
redirect_uri: '', // Will be set at runtime with full URL
|
|
50
|
+
scope: scopes.join(' '),
|
|
51
|
+
state,
|
|
52
|
+
});
|
|
53
|
+
return `${GITHUB_AUTH_URL}?${params.toString()}`;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async exchangeCode(code: string, callbackUrl: string): Promise<string> {
|
|
57
|
+
const res = await fetch(GITHUB_TOKEN_URL, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
Accept: 'application/json',
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
client_id: config.clientId,
|
|
65
|
+
client_secret: config.clientSecret,
|
|
66
|
+
code,
|
|
67
|
+
redirect_uri: callbackUrl,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const error = await res.text();
|
|
73
|
+
throw new Error(`GitHub token exchange failed: ${error}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data = await res.json() as { access_token: string; error?: string };
|
|
77
|
+
if (data.error) {
|
|
78
|
+
throw new Error(`GitHub OAuth error: ${data.error}`);
|
|
79
|
+
}
|
|
80
|
+
return data.access_token;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async fetchProfile(accessToken: string): Promise<AuthUser> {
|
|
84
|
+
// Fetch profile
|
|
85
|
+
const profileRes = await fetch(GITHUB_PROFILE_URL, {
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${accessToken}`,
|
|
88
|
+
Accept: 'application/vnd.github+json',
|
|
89
|
+
'User-Agent': 'MoriaJS',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!profileRes.ok) {
|
|
94
|
+
throw new Error('Failed to fetch GitHub profile');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const profile = await profileRes.json() as {
|
|
98
|
+
id: number;
|
|
99
|
+
login: string;
|
|
100
|
+
name: string | null;
|
|
101
|
+
email: string | null;
|
|
102
|
+
avatar_url: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// If email is not public, fetch from /user/emails
|
|
106
|
+
let email = profile.email;
|
|
107
|
+
if (!email) {
|
|
108
|
+
try {
|
|
109
|
+
const emailsRes = await fetch(GITHUB_EMAILS_URL, {
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${accessToken}`,
|
|
112
|
+
Accept: 'application/vnd.github+json',
|
|
113
|
+
'User-Agent': 'MoriaJS',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
if (emailsRes.ok) {
|
|
117
|
+
const emails = await emailsRes.json() as Array<{
|
|
118
|
+
email: string;
|
|
119
|
+
primary: boolean;
|
|
120
|
+
verified: boolean;
|
|
121
|
+
}>;
|
|
122
|
+
const primary = emails.find((e) => e.primary && e.verified);
|
|
123
|
+
email = primary?.email ?? emails[0]?.email ?? null;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// Email fetch failed, continue without
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
id: profile.id,
|
|
132
|
+
email: email ?? undefined,
|
|
133
|
+
name: profile.name ?? profile.login,
|
|
134
|
+
avatar: profile.avatar_url,
|
|
135
|
+
provider: 'github',
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements the Authorization Code flow for Google.
|
|
5
|
+
* Uses Node.js built-in fetch — zero additional dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AuthUser } from '../index.js';
|
|
9
|
+
import type { OAuthProviderConfig, OAuthProvider } from './types.js';
|
|
10
|
+
|
|
11
|
+
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
12
|
+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
13
|
+
const GOOGLE_PROFILE_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_SCOPES = ['openid', 'email', 'profile'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a Google OAuth provider.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { createAuthPlugin, googleProvider } from '@moriajs/auth';
|
|
23
|
+
*
|
|
24
|
+
* await app.use(createAuthPlugin({
|
|
25
|
+
* secret: process.env.JWT_SECRET!,
|
|
26
|
+
* providers: [
|
|
27
|
+
* googleProvider({
|
|
28
|
+
* clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
29
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
30
|
+
* }),
|
|
31
|
+
* ],
|
|
32
|
+
* }));
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function googleProvider(config: OAuthProviderConfig): OAuthProvider {
|
|
36
|
+
const scopes = config.scopes ?? DEFAULT_SCOPES;
|
|
37
|
+
const callbackPath = config.callbackUrl ?? '/auth/google/callback';
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
name: 'google',
|
|
41
|
+
callbackPath,
|
|
42
|
+
successRedirect: config.successRedirect ?? '/',
|
|
43
|
+
failureRedirect: config.failureRedirect ?? '/',
|
|
44
|
+
|
|
45
|
+
getAuthUrl(state: string): string {
|
|
46
|
+
const params = new URLSearchParams({
|
|
47
|
+
client_id: config.clientId,
|
|
48
|
+
redirect_uri: '', // Will be set at runtime with full URL
|
|
49
|
+
response_type: 'code',
|
|
50
|
+
scope: scopes.join(' '),
|
|
51
|
+
state,
|
|
52
|
+
access_type: 'offline',
|
|
53
|
+
prompt: 'consent',
|
|
54
|
+
});
|
|
55
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async exchangeCode(code: string, callbackUrl: string): Promise<string> {
|
|
59
|
+
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
62
|
+
body: new URLSearchParams({
|
|
63
|
+
code,
|
|
64
|
+
client_id: config.clientId,
|
|
65
|
+
client_secret: config.clientSecret,
|
|
66
|
+
redirect_uri: callbackUrl,
|
|
67
|
+
grant_type: 'authorization_code',
|
|
68
|
+
}).toString(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const error = await res.text();
|
|
73
|
+
throw new Error(`Google token exchange failed: ${error}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data = await res.json() as { access_token: string };
|
|
77
|
+
return data.access_token;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async fetchProfile(accessToken: string): Promise<AuthUser> {
|
|
81
|
+
const res = await fetch(GOOGLE_PROFILE_URL, {
|
|
82
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
throw new Error('Failed to fetch Google profile');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const profile = await res.json() as {
|
|
90
|
+
id: string;
|
|
91
|
+
email: string;
|
|
92
|
+
name: string;
|
|
93
|
+
picture?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
id: profile.id,
|
|
98
|
+
email: profile.email,
|
|
99
|
+
name: profile.name,
|
|
100
|
+
avatar: profile.picture,
|
|
101
|
+
provider: 'google',
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Provider Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for all OAuth providers in MoriaJS.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AuthUser } from '../index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for an OAuth provider.
|
|
11
|
+
*/
|
|
12
|
+
export interface OAuthProviderConfig {
|
|
13
|
+
/** OAuth2 client ID */
|
|
14
|
+
clientId: string;
|
|
15
|
+
/** OAuth2 client secret */
|
|
16
|
+
clientSecret: string;
|
|
17
|
+
/** Callback URL path (e.g., '/auth/google/callback') */
|
|
18
|
+
callbackUrl?: string;
|
|
19
|
+
/** OAuth2 scopes to request */
|
|
20
|
+
scopes?: string[];
|
|
21
|
+
/** URL to redirect to after successful auth (default: '/') */
|
|
22
|
+
successRedirect?: string;
|
|
23
|
+
/** URL to redirect to after failed auth (default: '/') */
|
|
24
|
+
failureRedirect?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An OAuth provider registers redirect + callback routes
|
|
29
|
+
* and maps external profiles to MoriaJS AuthUser.
|
|
30
|
+
*/
|
|
31
|
+
export interface OAuthProvider {
|
|
32
|
+
/** Provider name (e.g., 'google', 'github') */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Build the authorization redirect URL */
|
|
35
|
+
getAuthUrl(state: string): string;
|
|
36
|
+
/** Exchange auth code for access token */
|
|
37
|
+
exchangeCode(code: string, callbackUrl: string): Promise<string>;
|
|
38
|
+
/** Fetch user profile using access token */
|
|
39
|
+
fetchProfile(accessToken: string): Promise<AuthUser>;
|
|
40
|
+
/** The configured callback URL path */
|
|
41
|
+
callbackPath: string;
|
|
42
|
+
/** Redirect paths */
|
|
43
|
+
successRedirect: string;
|
|
44
|
+
failureRedirect: string;
|
|
45
|
+
}
|