@moriajs/auth 0.3.5 → 0.4.1

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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @moriajs/auth@0.3.5 build C:\Codes\node\2026\github\moriajs\packages\auth
2
+ > @moriajs/auth@0.4.1 build C:\Codes\node\2026\github\moriajs\packages\auth
3
3
  > tsc
4
4
 
@@ -1,4 +1,4 @@
1
1
 
2
- > @moriajs/auth@0.0.1 typecheck C:\Codes\node\2026\github\moriajs\packages\auth
2
+ > @moriajs/auth@0.4.0 typecheck C:\Codes\node\2026\github\moriajs\packages\auth
3
3
  > tsc --noEmit
4
4
 
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
- * Architecture: Provider-based for future OAuth, sessions, etc.
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
  */
@@ -71,14 +91,16 @@ export declare function createAuthPlugin(config: AuthConfig): {
71
91
  * Route-level authentication guard.
72
92
  * Use as a Fastify preHandler hook.
73
93
  *
94
+ * Supports both direct and factory calls:
95
+ * - `preHandler: [requireAuth]`
96
+ * - `preHandler: [requireAuth({ role: 'admin' })]`
97
+ *
74
98
  * @example
75
99
  * ```ts
76
- * server.get('/protected', { preHandler: [requireAuth()] }, async (req) => {
100
+ * server.get('/protected', { preHandler: [requireAuth] }, async (req) => {
77
101
  * return { user: req.user };
78
102
  * });
79
103
  * ```
80
104
  */
81
- export declare function requireAuth(options?: {
82
- role?: string;
83
- }): (request: FastifyRequest, reply: FastifyReply) => Promise<undefined>;
105
+ export declare function requireAuth(arg1?: any, arg2?: any): any;
84
106
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAE7E;;;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;CACxC;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;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU;;yBAIhB;QAAE,MAAM,EAAE,GAAG,CAAA;KAAE;EAoCjD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,IACrC,SAAS,cAAc,EAAE,OAAO,YAAY,wBAe7D"}
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;AAyGD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAUvD"}
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
- * Architecture: Provider-based for future OAuth, sessions, etc.
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,36 +57,124 @@ 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
+ }
137
+ /**
138
+ * Internal auth verification logic.
139
+ */
140
+ async function performAuth(request, reply, options) {
141
+ try {
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ await request.jwtVerify();
144
+ if (options?.role) {
145
+ const user = request.user;
146
+ if (user.role !== options.role) {
147
+ return reply.status(403).send({ error: 'Forbidden' });
148
+ }
149
+ }
150
+ }
151
+ catch {
152
+ return reply.status(401).send({ error: 'Unauthorized' });
153
+ }
154
+ }
50
155
  /**
51
156
  * Route-level authentication guard.
52
157
  * Use as a Fastify preHandler hook.
53
158
  *
159
+ * Supports both direct and factory calls:
160
+ * - `preHandler: [requireAuth]`
161
+ * - `preHandler: [requireAuth({ role: 'admin' })]`
162
+ *
54
163
  * @example
55
164
  * ```ts
56
- * server.get('/protected', { preHandler: [requireAuth()] }, async (req) => {
165
+ * server.get('/protected', { preHandler: [requireAuth] }, async (req) => {
57
166
  * return { user: req.user };
58
167
  * });
59
168
  * ```
60
169
  */
61
- export function requireAuth(options) {
170
+ export function requireAuth(arg1, arg2) {
171
+ // If called with (request, reply), it's a direct call
172
+ if (arg1 && typeof arg1 === 'object' && 'raw' in arg1) {
173
+ return performAuth(arg1, arg2);
174
+ }
175
+ // Otherwise, it's a factory call: requireAuth(options)
62
176
  return async (request, reply) => {
63
- try {
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
- await request.jwtVerify();
66
- if (options?.role) {
67
- const user = request.user;
68
- if (user.role !== options.role) {
69
- return reply.status(403).send({ error: 'Forbidden' });
70
- }
71
- }
72
- }
73
- catch {
74
- return reply.status(401).send({ error: 'Unauthorized' });
75
- }
177
+ return performAuth(request, reply, arg1);
76
178
  };
77
179
  }
78
180
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA+CH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAkB;IAC/C,OAAO;QACH,IAAI,EAAE,eAAe;QACrB,8DAA8D;QAC9D,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAmB;YACtC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;YAEzC,MAAO,MAA0B,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE;gBACpD,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,MAAM,EAAE;oBACJ,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,aAAa;oBAC9C,MAAM,EAAE,KAAK;iBAChB;aACJ,CAAC,CAAC;YAEH,wCAAwC;YACxC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAc,EAAE,KAAmB,EAAE,EAAE;gBACpE,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CACzB,EAAE,GAAG,IAAI,EAAE,EACX,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,IAAI,EAAE,CAC1C,CAAC;gBAEF,KAAK,CAAC,MAAM,CAAC,YAAY,EACrB,GAAG,MAAM,CAAC,UAAU,IAAI,aAAa,IAAI,KAAK,oBAAoB,MAAM,CAAC,UAAU,IAAI,GAAG,cAAc,MAAM,CAAC,QAAQ,IAAI,KAAK,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAClN,EAAE,CACL,CAAC;gBAEF,OAAO,KAAK,CAAC;YACjB,CAAC,CAAC,CAAC;YAEH,wCAAwC;YACxC,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,EAAE,QAAwB,EAAE,KAAmB,EAAE,EAAE;gBAC/E,KAAK,CAAC,MAAM,CAAC,YAAY,EACrB,GAAG,MAAM,CAAC,UAAU,IAAI,aAAa,qBAAqB,MAAM,CAAC,UAAU,IAAI,GAAG,aAAa,CAClG,CAAC;YACN,CAAC,CAAC,CAAC;YAEF,MAA0B,CAAC,GAAG,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAC;QACtF,CAAC;KACJ,CAAC;AACN,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,OAA2B;IACnD,OAAO,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QAC1D,IAAI,CAAC;YACD,8DAA8D;YAC9D,MAAO,OAAe,CAAC,SAAS,EAAE,CAAC;YAEnC,IAAI,OAAO,EAAE,IAAI,EAAE,CAAC;gBAChB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAgB,CAAC;gBACtC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC;oBAC7B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;gBAC1D,CAAC;YACL,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAC7D,CAAC;IACL,CAAC,CAAC;AACN,CAAC"}
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,7 @@
1
+ /**
2
+ * OAuth Provider Types
3
+ *
4
+ * Shared types for all OAuth providers in MoriaJS.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -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.5",
3
+ "version": "0.4.1",
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.3.5"
25
+ "@moriajs/core": "0.4.1"
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
- * Architecture: Provider-based for future OAuth, sessions, etc.
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,36 +126,147 @@ 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
+
228
+ /**
229
+ * Internal auth verification logic.
230
+ */
231
+ async function performAuth(request: FastifyRequest, reply: FastifyReply, options?: { role?: string }) {
232
+ try {
233
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
234
+ await (request as any).jwtVerify();
235
+
236
+ if (options?.role) {
237
+ const user = request.user as AuthUser;
238
+ if (user.role !== options.role) {
239
+ return reply.status(403).send({ error: 'Forbidden' });
240
+ }
241
+ }
242
+ } catch {
243
+ return reply.status(401).send({ error: 'Unauthorized' });
244
+ }
245
+ }
246
+
111
247
  /**
112
248
  * Route-level authentication guard.
113
249
  * Use as a Fastify preHandler hook.
114
250
  *
251
+ * Supports both direct and factory calls:
252
+ * - `preHandler: [requireAuth]`
253
+ * - `preHandler: [requireAuth({ role: 'admin' })]`
254
+ *
115
255
  * @example
116
256
  * ```ts
117
- * server.get('/protected', { preHandler: [requireAuth()] }, async (req) => {
257
+ * server.get('/protected', { preHandler: [requireAuth] }, async (req) => {
118
258
  * return { user: req.user };
119
259
  * });
120
260
  * ```
121
261
  */
122
- export function requireAuth(options?: { role?: string }) {
262
+ export function requireAuth(arg1?: any, arg2?: any): any {
263
+ // If called with (request, reply), it's a direct call
264
+ if (arg1 && typeof arg1 === 'object' && 'raw' in arg1) {
265
+ return performAuth(arg1, arg2);
266
+ }
267
+
268
+ // Otherwise, it's a factory call: requireAuth(options)
123
269
  return async (request: FastifyRequest, reply: FastifyReply) => {
124
- try {
125
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
- await (request as any).jwtVerify();
127
-
128
- if (options?.role) {
129
- const user = request.user as AuthUser;
130
- if (user.role !== options.role) {
131
- return reply.status(403).send({ error: 'Forbidden' });
132
- }
133
- }
134
- } catch {
135
- return reply.status(401).send({ error: 'Unauthorized' });
136
- }
270
+ return performAuth(request, reply, arg1);
137
271
  };
138
272
  }
@@ -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
+ }