@hazeljs/oauth 0.2.0-alpha.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.
Files changed (66) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +200 -0
  3. package/dist/guards/oauth-state.guard.d.ts +15 -0
  4. package/dist/guards/oauth-state.guard.d.ts.map +1 -0
  5. package/dist/guards/oauth-state.guard.js +43 -0
  6. package/dist/guards/oauth-state.guard.test.d.ts +2 -0
  7. package/dist/guards/oauth-state.guard.test.d.ts.map +1 -0
  8. package/dist/guards/oauth-state.guard.test.js +60 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +17 -0
  12. package/dist/oauth.controller.d.ts +29 -0
  13. package/dist/oauth.controller.d.ts.map +1 -0
  14. package/dist/oauth.controller.js +137 -0
  15. package/dist/oauth.module.d.ts +5 -0
  16. package/dist/oauth.module.d.ts.map +1 -0
  17. package/dist/oauth.module.js +27 -0
  18. package/dist/oauth.module.test.d.ts +2 -0
  19. package/dist/oauth.module.test.d.ts.map +1 -0
  20. package/dist/oauth.module.test.js +40 -0
  21. package/dist/oauth.service.d.ts +47 -0
  22. package/dist/oauth.service.d.ts.map +1 -0
  23. package/dist/oauth.service.js +256 -0
  24. package/dist/oauth.service.test.d.ts +2 -0
  25. package/dist/oauth.service.test.d.ts.map +1 -0
  26. package/dist/oauth.service.test.js +248 -0
  27. package/dist/providers/facebook.provider.d.ts +11 -0
  28. package/dist/providers/facebook.provider.d.ts.map +1 -0
  29. package/dist/providers/facebook.provider.js +63 -0
  30. package/dist/providers/facebook.provider.test.d.ts +2 -0
  31. package/dist/providers/facebook.provider.test.d.ts.map +1 -0
  32. package/dist/providers/facebook.provider.test.js +66 -0
  33. package/dist/providers/github.provider.d.ts +11 -0
  34. package/dist/providers/github.provider.d.ts.map +1 -0
  35. package/dist/providers/github.provider.js +79 -0
  36. package/dist/providers/github.provider.test.d.ts +2 -0
  37. package/dist/providers/github.provider.test.d.ts.map +1 -0
  38. package/dist/providers/github.provider.test.js +73 -0
  39. package/dist/providers/google.provider.d.ts +11 -0
  40. package/dist/providers/google.provider.d.ts.map +1 -0
  41. package/dist/providers/google.provider.js +61 -0
  42. package/dist/providers/google.provider.test.d.ts +2 -0
  43. package/dist/providers/google.provider.test.d.ts.map +1 -0
  44. package/dist/providers/google.provider.test.js +69 -0
  45. package/dist/providers/index.d.ts +7 -0
  46. package/dist/providers/index.d.ts.map +1 -0
  47. package/dist/providers/index.js +22 -0
  48. package/dist/providers/microsoft.provider.d.ts +11 -0
  49. package/dist/providers/microsoft.provider.d.ts.map +1 -0
  50. package/dist/providers/microsoft.provider.js +62 -0
  51. package/dist/providers/microsoft.provider.test.d.ts +2 -0
  52. package/dist/providers/microsoft.provider.test.d.ts.map +1 -0
  53. package/dist/providers/microsoft.provider.test.js +62 -0
  54. package/dist/providers/provider.types.d.ts +94 -0
  55. package/dist/providers/provider.types.d.ts.map +1 -0
  56. package/dist/providers/provider.types.js +5 -0
  57. package/dist/providers/provider.types.test.d.ts +2 -0
  58. package/dist/providers/provider.types.test.d.ts.map +1 -0
  59. package/dist/providers/provider.types.test.js +57 -0
  60. package/dist/providers/twitter.provider.d.ts +11 -0
  61. package/dist/providers/twitter.provider.d.ts.map +1 -0
  62. package/dist/providers/twitter.provider.js +67 -0
  63. package/dist/providers/twitter.provider.test.d.ts +2 -0
  64. package/dist/providers/twitter.provider.test.d.ts.map +1 -0
  65. package/dist/providers/twitter.provider.test.js +71 -0
  66. package/package.json +56 -0
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.OAuthController = void 0;
16
+ const core_1 = require("@hazeljs/core");
17
+ const oauth_service_1 = require("./oauth.service");
18
+ const STATE_COOKIE = 'oauth_state';
19
+ const CODE_VERIFIER_COOKIE = 'oauth_code_verifier';
20
+ const COOKIE_MAX_AGE = 60 * 10; // 10 minutes
21
+ const COOKIE_OPTS = `Path=/; HttpOnly; SameSite=Lax; Max-Age=${COOKIE_MAX_AGE}`;
22
+ function getCookie(req, name) {
23
+ const h = req?.headers?.['cookie'];
24
+ const cookieHeader = Array.isArray(h) ? h[0] : h;
25
+ if (typeof cookieHeader !== 'string')
26
+ return undefined;
27
+ const part = cookieHeader.split(';').find((c) => c.trim().startsWith(`${name}=`));
28
+ return part?.split('=')[1]?.trim();
29
+ }
30
+ /**
31
+ * Optional controller that provides OAuth routes.
32
+ * Register in your app if you want ready-made /auth/:provider and /auth/:provider/callback routes.
33
+ */
34
+ let OAuthController = class OAuthController {
35
+ constructor(oauthService) {
36
+ this.oauthService = oauthService;
37
+ }
38
+ /**
39
+ * GET /auth/:provider - Redirects user to OAuth provider.
40
+ * Sets state and codeVerifier (for PKCE) in cookies.
41
+ */
42
+ async login(provider, res) {
43
+ const p = provider.toLowerCase();
44
+ if (!['google', 'microsoft', 'github', 'facebook', 'twitter'].includes(p)) {
45
+ res.status(400).json({ error: 'Invalid provider' });
46
+ return;
47
+ }
48
+ const { url, state, codeVerifier } = this.oauthService.getAuthorizationUrl(p);
49
+ const cookies = [`${STATE_COOKIE}=${state}; ${COOKIE_OPTS}`];
50
+ if (codeVerifier) {
51
+ cookies.push(`${CODE_VERIFIER_COOKIE}=${codeVerifier}; ${COOKIE_OPTS}`);
52
+ }
53
+ res.setHeader('Set-Cookie', cookies);
54
+ res.status(302);
55
+ res.setHeader('Location', url);
56
+ res.end();
57
+ }
58
+ /**
59
+ * GET /auth/:provider/callback - Handles OAuth callback.
60
+ * Returns JSON with accessToken, user. Use successRedirect/errorRedirect query params for redirects.
61
+ */
62
+ async callback(provider, query, req, res) {
63
+ const p = provider.toLowerCase();
64
+ if (!['google', 'microsoft', 'github', 'facebook', 'twitter'].includes(p)) {
65
+ res.status(400).json({ error: 'Invalid provider' });
66
+ return;
67
+ }
68
+ const code = query?.code;
69
+ if (!code) {
70
+ this.redirectOrJson(res, 400, query.errorRedirect, { error: 'Missing authorization code' });
71
+ return;
72
+ }
73
+ const storedState = getCookie(req, STATE_COOKIE);
74
+ const codeVerifier = getCookie(req, CODE_VERIFIER_COOKIE);
75
+ res.setHeader('Set-Cookie', [
76
+ `${STATE_COOKIE}=; Path=/; HttpOnly; Max-Age=0`,
77
+ `${CODE_VERIFIER_COOKIE}=; Path=/; HttpOnly; Max-Age=0`,
78
+ ]);
79
+ if (!storedState || query.state !== storedState) {
80
+ this.redirectOrJson(res, 400, query.errorRedirect, { error: 'Invalid state' });
81
+ return;
82
+ }
83
+ try {
84
+ const result = await this.oauthService.handleCallback(p, code, storedState, codeVerifier);
85
+ // Pass through callbackHandler (if configured) so the app can issue a JWT,
86
+ // look up the user in its own DB, etc. before responding.
87
+ const response = await this.oauthService.executeCallback(p, result);
88
+ if (query.successRedirect) {
89
+ res.status(302);
90
+ res.setHeader('Location', query.successRedirect);
91
+ res.end();
92
+ return;
93
+ }
94
+ res.status(200).json(response);
95
+ }
96
+ catch (err) {
97
+ const message = err instanceof Error ? err.message : 'OAuth callback failed';
98
+ this.redirectOrJson(res, 401, query.errorRedirect, { error: message });
99
+ }
100
+ }
101
+ redirectOrJson(res, status, errorRedirect, json) {
102
+ if (errorRedirect) {
103
+ const url = new URL(errorRedirect);
104
+ if (json?.error)
105
+ url.searchParams.set('error', json.error);
106
+ res.status(302);
107
+ res.setHeader('Location', url.toString());
108
+ res.end();
109
+ }
110
+ else if (json) {
111
+ res.status(status).json(json);
112
+ }
113
+ }
114
+ };
115
+ exports.OAuthController = OAuthController;
116
+ __decorate([
117
+ (0, core_1.Get)('/:provider'),
118
+ __param(0, (0, core_1.Param)('provider')),
119
+ __param(1, (0, core_1.Res)()),
120
+ __metadata("design:type", Function),
121
+ __metadata("design:paramtypes", [String, Object]),
122
+ __metadata("design:returntype", Promise)
123
+ ], OAuthController.prototype, "login", null);
124
+ __decorate([
125
+ (0, core_1.Get)('/:provider/callback'),
126
+ __param(0, (0, core_1.Param)('provider')),
127
+ __param(1, (0, core_1.Query)()),
128
+ __param(2, (0, core_1.Req)()),
129
+ __param(3, (0, core_1.Res)()),
130
+ __metadata("design:type", Function),
131
+ __metadata("design:paramtypes", [String, Object, Object, Object]),
132
+ __metadata("design:returntype", Promise)
133
+ ], OAuthController.prototype, "callback", null);
134
+ exports.OAuthController = OAuthController = __decorate([
135
+ (0, core_1.Controller)('/auth'),
136
+ __metadata("design:paramtypes", [oauth_service_1.OAuthService])
137
+ ], OAuthController);
@@ -0,0 +1,5 @@
1
+ import type { OAuthModuleOptions } from './providers/provider.types';
2
+ export declare class OAuthModule {
3
+ static forRoot(options: OAuthModuleOptions): typeof OAuthModule;
4
+ }
5
+ //# sourceMappingURL=oauth.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.module.d.ts","sourceRoot":"","sources":["../src/oauth.module.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,qBAKa,WAAW;IACtB,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,WAAW;CAIhE"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var OAuthModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.OAuthModule = void 0;
11
+ const core_1 = require("@hazeljs/core");
12
+ const oauth_controller_1 = require("./oauth.controller");
13
+ const oauth_service_1 = require("./oauth.service");
14
+ let OAuthModule = OAuthModule_1 = class OAuthModule {
15
+ static forRoot(options) {
16
+ oauth_service_1.OAuthService.configure(options);
17
+ return OAuthModule_1;
18
+ }
19
+ };
20
+ exports.OAuthModule = OAuthModule;
21
+ exports.OAuthModule = OAuthModule = OAuthModule_1 = __decorate([
22
+ (0, core_1.HazelModule)({
23
+ controllers: [oauth_controller_1.OAuthController],
24
+ providers: [oauth_service_1.OAuthService],
25
+ exports: [oauth_service_1.OAuthService],
26
+ })
27
+ ], OAuthModule);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=oauth.module.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.module.test.d.ts","sourceRoot":"","sources":["../src/oauth.module.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const oauth_module_1 = require("./oauth.module");
4
+ const oauth_service_1 = require("./oauth.service");
5
+ jest.mock('arctic', () => ({
6
+ generateState: jest.fn(() => 'state'),
7
+ generateCodeVerifier: jest.fn(() => 'verifier'),
8
+ Google: jest.fn().mockImplementation(() => ({
9
+ createAuthorizationURL: () => new URL('https://accounts.google.com/auth'),
10
+ validateAuthorizationCode: jest.fn(),
11
+ })),
12
+ MicrosoftEntraId: jest.fn(),
13
+ Twitter: jest.fn(),
14
+ GitHub: jest.fn(),
15
+ Facebook: jest.fn(),
16
+ }));
17
+ describe('OAuthModule', () => {
18
+ const testOptions = {
19
+ providers: {
20
+ google: {
21
+ clientId: 'test-id',
22
+ clientSecret: 'test-secret',
23
+ redirectUri: 'https://app.com/callback',
24
+ },
25
+ },
26
+ };
27
+ beforeEach(() => {
28
+ oauth_module_1.OAuthModule.forRoot(testOptions);
29
+ });
30
+ it('should return OAuthModule from forRoot', () => {
31
+ const result = oauth_module_1.OAuthModule.forRoot(testOptions);
32
+ expect(result).toBe(oauth_module_1.OAuthModule);
33
+ });
34
+ it('should configure OAuthService when forRoot is called', () => {
35
+ const service = new oauth_service_1.OAuthService();
36
+ expect(service).toBeInstanceOf(oauth_service_1.OAuthService);
37
+ const url = service.getAuthorizationUrl('google');
38
+ expect(url.url).toBeDefined();
39
+ });
40
+ });
@@ -0,0 +1,47 @@
1
+ import type { OAuthModuleOptions, OAuthCallbackResult, OAuthAuthorizationResult, SupportedProvider } from './providers/provider.types';
2
+ export declare class OAuthService {
3
+ private options;
4
+ private googleClient;
5
+ private microsoftClient;
6
+ private githubClient;
7
+ private facebookClient;
8
+ private twitterClient;
9
+ constructor();
10
+ private static options;
11
+ static configure(options: OAuthModuleOptions): void;
12
+ private static getOptions;
13
+ private initClients;
14
+ private getClient;
15
+ private getDefaultScopes;
16
+ /**
17
+ * Get the authorization URL to redirect the user to the OAuth provider.
18
+ * For PKCE providers (Google, Microsoft), store codeVerifier and pass it to handleCallback.
19
+ */
20
+ getAuthorizationUrl(provider: SupportedProvider, state?: string, scopes?: string[]): OAuthAuthorizationResult;
21
+ /**
22
+ * Exchange the authorization code for tokens and fetch user profile.
23
+ * For PKCE providers (Google, Microsoft), codeVerifier is required.
24
+ */
25
+ handleCallback(provider: SupportedProvider, code: string, state: string, codeVerifier?: string): Promise<OAuthCallbackResult>;
26
+ /**
27
+ * Called by OAuthController after a successful token exchange and user-profile fetch.
28
+ *
29
+ * If the module was configured with a `callbackHandler` class, the handler is
30
+ * resolved from the global DI container and its `handle()` method is invoked.
31
+ * This is where you look up / create your application user and issue a JWT.
32
+ *
33
+ * When no handler is configured the raw OAuthCallbackResult is returned unchanged,
34
+ * which preserves backwards-compatible behaviour.
35
+ */
36
+ executeCallback(provider: SupportedProvider, result: OAuthCallbackResult): Promise<unknown>;
37
+ /**
38
+ * Validate that the state matches (CSRF protection).
39
+ * Use this when handling the callback to ensure the request originated from your app.
40
+ */
41
+ validateState(receivedState: string, storedState: string): boolean;
42
+ /**
43
+ * Generate a cryptographically secure state value for OAuth.
44
+ */
45
+ generateState(): string;
46
+ }
47
+ //# sourceMappingURL=oauth.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.service.d.ts","sourceRoot":"","sources":["../src/oauth.service.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,kBAAkB,EAElB,mBAAmB,EACnB,wBAAwB,EACxB,iBAAiB,EAElB,MAAM,4BAA4B,CAAC;AAIpC,qBACa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,YAAY,CAAwD;IAC5E,OAAO,CAAC,eAAe,CAA2D;IAClF,OAAO,CAAC,YAAY,CAAwD;IAC5E,OAAO,CAAC,cAAc,CAA0D;IAChF,OAAO,CAAC,aAAa,CAAyD;;IAO9E,OAAO,CAAC,MAAM,CAAC,OAAO,CAAmC;IAEzD,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAInD,OAAO,CAAC,MAAM,CAAC,UAAU;IASzB,OAAO,CAAC,WAAW;IAkBnB,OAAO,CAAC,SAAS;IA8BjB,OAAO,CAAC,gBAAgB;IAmBxB;;;OAGG;IACH,mBAAmB,CACjB,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EAAE,GAChB,wBAAwB;IA2B3B;;;OAGG;IACG,cAAc,CAClB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,CAAC;IAuF/B;;;;;;;;;OASG;IACG,eAAe,CACnB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,OAAO,CAAC;IAWnB;;;OAGG;IACH,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO;IAIlE;;OAEG;IACH,aAAa,IAAI,MAAM;CAGxB"}
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var OAuthService_1;
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.OAuthService = void 0;
47
+ const core_1 = require("@hazeljs/core");
48
+ const arctic = __importStar(require("arctic"));
49
+ const providers_1 = require("./providers");
50
+ const PKCE_PROVIDERS = ['google', 'microsoft', 'twitter'];
51
+ let OAuthService = OAuthService_1 = class OAuthService {
52
+ constructor() {
53
+ this.googleClient = null;
54
+ this.microsoftClient = null;
55
+ this.githubClient = null;
56
+ this.facebookClient = null;
57
+ this.twitterClient = null;
58
+ this.options = OAuthService_1.getOptions();
59
+ this.initClients();
60
+ }
61
+ static configure(options) {
62
+ OAuthService_1.options = options;
63
+ }
64
+ static getOptions() {
65
+ if (!OAuthService_1.options) {
66
+ throw new Error('OAuthModule not configured. Call OAuthModule.forRoot({ providers: {...} }) in your app module.');
67
+ }
68
+ return OAuthService_1.options;
69
+ }
70
+ initClients() {
71
+ if (this.options.providers.google) {
72
+ this.googleClient = (0, providers_1.createGoogleProvider)(this.options.providers.google);
73
+ }
74
+ if (this.options.providers.microsoft) {
75
+ this.microsoftClient = (0, providers_1.createMicrosoftProvider)(this.options.providers.microsoft);
76
+ }
77
+ if (this.options.providers.github) {
78
+ this.githubClient = (0, providers_1.createGitHubProvider)(this.options.providers.github);
79
+ }
80
+ if (this.options.providers.facebook) {
81
+ this.facebookClient = (0, providers_1.createFacebookProvider)(this.options.providers.facebook);
82
+ }
83
+ if (this.options.providers.twitter) {
84
+ this.twitterClient = (0, providers_1.createTwitterProvider)(this.options.providers.twitter);
85
+ }
86
+ }
87
+ getClient(provider) {
88
+ switch (provider) {
89
+ case 'google':
90
+ if (!this.googleClient)
91
+ throw new Error('Google OAuth is not configured');
92
+ return this.googleClient;
93
+ case 'microsoft':
94
+ if (!this.microsoftClient)
95
+ throw new Error('Microsoft OAuth is not configured');
96
+ return this.microsoftClient;
97
+ case 'github':
98
+ if (!this.githubClient)
99
+ throw new Error('GitHub OAuth is not configured');
100
+ return this.githubClient;
101
+ case 'facebook':
102
+ if (!this.facebookClient)
103
+ throw new Error('Facebook OAuth is not configured');
104
+ return this.facebookClient;
105
+ case 'twitter':
106
+ if (!this.twitterClient)
107
+ throw new Error('Twitter OAuth is not configured');
108
+ return this.twitterClient;
109
+ default:
110
+ throw new Error(`Unsupported provider: ${provider}`);
111
+ }
112
+ }
113
+ getDefaultScopes(provider) {
114
+ const defaults = this.options.defaultScopes?.[provider];
115
+ if (defaults)
116
+ return defaults;
117
+ switch (provider) {
118
+ case 'google':
119
+ return (0, providers_1.getGoogleDefaultScopes)();
120
+ case 'microsoft':
121
+ return (0, providers_1.getMicrosoftDefaultScopes)();
122
+ case 'github':
123
+ return (0, providers_1.getGitHubDefaultScopes)();
124
+ case 'facebook':
125
+ return (0, providers_1.getFacebookDefaultScopes)();
126
+ case 'twitter':
127
+ return (0, providers_1.getTwitterDefaultScopes)();
128
+ default:
129
+ return [];
130
+ }
131
+ }
132
+ /**
133
+ * Get the authorization URL to redirect the user to the OAuth provider.
134
+ * For PKCE providers (Google, Microsoft), store codeVerifier and pass it to handleCallback.
135
+ */
136
+ getAuthorizationUrl(provider, state, scopes) {
137
+ const client = this.getClient(provider);
138
+ const resolvedState = state || arctic.generateState();
139
+ const resolvedScopes = scopes ?? this.getDefaultScopes(provider);
140
+ if (PKCE_PROVIDERS.includes(provider)) {
141
+ const codeVerifier = arctic.generateCodeVerifier();
142
+ const url = client.createAuthorizationURL(resolvedState, codeVerifier, resolvedScopes);
143
+ return {
144
+ url: url.toString(),
145
+ state: resolvedState,
146
+ codeVerifier,
147
+ };
148
+ }
149
+ // GitHub, Facebook - no PKCE
150
+ const url = client.createAuthorizationURL(resolvedState, resolvedScopes);
151
+ return {
152
+ url: url.toString(),
153
+ state: resolvedState,
154
+ };
155
+ }
156
+ /**
157
+ * Exchange the authorization code for tokens and fetch user profile.
158
+ * For PKCE providers (Google, Microsoft), codeVerifier is required.
159
+ */
160
+ async handleCallback(provider, code, state, codeVerifier) {
161
+ const client = this.getClient(provider);
162
+ const needsPkce = PKCE_PROVIDERS.includes(provider);
163
+ if (needsPkce && !codeVerifier) {
164
+ throw new Error(`Provider ${provider} requires codeVerifier (PKCE). Pass the codeVerifier from getAuthorizationUrl.`);
165
+ }
166
+ let accessToken;
167
+ let refreshToken;
168
+ let expiresAt;
169
+ try {
170
+ if (needsPkce && codeVerifier) {
171
+ const tokens = await client.validateAuthorizationCode(code, codeVerifier);
172
+ accessToken = tokens.accessToken();
173
+ refreshToken = tokens.hasRefreshToken() ? tokens.refreshToken() : undefined;
174
+ expiresAt = tokens.accessTokenExpiresAt();
175
+ }
176
+ else {
177
+ const tokens = await client.validateAuthorizationCode(code);
178
+ accessToken = tokens.accessToken();
179
+ refreshToken = tokens.hasRefreshToken() ? tokens.refreshToken() : undefined;
180
+ expiresAt = tokens.accessTokenExpiresAt();
181
+ }
182
+ }
183
+ catch (e) {
184
+ if (e instanceof arctic.OAuth2RequestError) {
185
+ throw new Error(`OAuth token exchange failed: ${e.code} - ${e.description || e.message}`);
186
+ }
187
+ if (e instanceof arctic.ArcticFetchError) {
188
+ const cause = e.cause;
189
+ throw new Error(`OAuth request failed: ${cause?.message || e.message}`);
190
+ }
191
+ throw e;
192
+ }
193
+ let user;
194
+ switch (provider) {
195
+ case 'google':
196
+ user = await (0, providers_1.fetchGoogleUser)(accessToken);
197
+ break;
198
+ case 'microsoft':
199
+ user = await (0, providers_1.fetchMicrosoftUser)(accessToken);
200
+ break;
201
+ case 'github':
202
+ user = await (0, providers_1.fetchGitHubUser)(accessToken);
203
+ break;
204
+ case 'facebook':
205
+ user = await (0, providers_1.fetchFacebookUser)(accessToken);
206
+ break;
207
+ case 'twitter':
208
+ user = await (0, providers_1.fetchTwitterUser)(accessToken);
209
+ break;
210
+ default:
211
+ throw new Error(`Unsupported provider: ${provider}`);
212
+ }
213
+ return {
214
+ accessToken,
215
+ refreshToken,
216
+ expiresAt,
217
+ user,
218
+ };
219
+ }
220
+ /**
221
+ * Called by OAuthController after a successful token exchange and user-profile fetch.
222
+ *
223
+ * If the module was configured with a `callbackHandler` class, the handler is
224
+ * resolved from the global DI container and its `handle()` method is invoked.
225
+ * This is where you look up / create your application user and issue a JWT.
226
+ *
227
+ * When no handler is configured the raw OAuthCallbackResult is returned unchanged,
228
+ * which preserves backwards-compatible behaviour.
229
+ */
230
+ async executeCallback(provider, result) {
231
+ const handlerClass = OAuthService_1.options?.callbackHandler;
232
+ if (!handlerClass)
233
+ return result;
234
+ const handler = core_1.Container.getInstance().resolve(handlerClass);
235
+ return handler.handle(result, provider);
236
+ }
237
+ /**
238
+ * Validate that the state matches (CSRF protection).
239
+ * Use this when handling the callback to ensure the request originated from your app.
240
+ */
241
+ validateState(receivedState, storedState) {
242
+ return receivedState === storedState && receivedState.length > 0;
243
+ }
244
+ /**
245
+ * Generate a cryptographically secure state value for OAuth.
246
+ */
247
+ generateState() {
248
+ return arctic.generateState();
249
+ }
250
+ };
251
+ exports.OAuthService = OAuthService;
252
+ OAuthService.options = null;
253
+ exports.OAuthService = OAuthService = OAuthService_1 = __decorate([
254
+ (0, core_1.Service)(),
255
+ __metadata("design:paramtypes", [])
256
+ ], OAuthService);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=oauth.service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.service.test.d.ts","sourceRoot":"","sources":["../src/oauth.service.test.ts"],"names":[],"mappings":""}