@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.
- package/LICENSE +192 -0
- package/README.md +200 -0
- package/dist/guards/oauth-state.guard.d.ts +15 -0
- package/dist/guards/oauth-state.guard.d.ts.map +1 -0
- package/dist/guards/oauth-state.guard.js +43 -0
- package/dist/guards/oauth-state.guard.test.d.ts +2 -0
- package/dist/guards/oauth-state.guard.test.d.ts.map +1 -0
- package/dist/guards/oauth-state.guard.test.js +60 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/oauth.controller.d.ts +29 -0
- package/dist/oauth.controller.d.ts.map +1 -0
- package/dist/oauth.controller.js +137 -0
- package/dist/oauth.module.d.ts +5 -0
- package/dist/oauth.module.d.ts.map +1 -0
- package/dist/oauth.module.js +27 -0
- package/dist/oauth.module.test.d.ts +2 -0
- package/dist/oauth.module.test.d.ts.map +1 -0
- package/dist/oauth.module.test.js +40 -0
- package/dist/oauth.service.d.ts +47 -0
- package/dist/oauth.service.d.ts.map +1 -0
- package/dist/oauth.service.js +256 -0
- package/dist/oauth.service.test.d.ts +2 -0
- package/dist/oauth.service.test.d.ts.map +1 -0
- package/dist/oauth.service.test.js +248 -0
- package/dist/providers/facebook.provider.d.ts +11 -0
- package/dist/providers/facebook.provider.d.ts.map +1 -0
- package/dist/providers/facebook.provider.js +63 -0
- package/dist/providers/facebook.provider.test.d.ts +2 -0
- package/dist/providers/facebook.provider.test.d.ts.map +1 -0
- package/dist/providers/facebook.provider.test.js +66 -0
- package/dist/providers/github.provider.d.ts +11 -0
- package/dist/providers/github.provider.d.ts.map +1 -0
- package/dist/providers/github.provider.js +79 -0
- package/dist/providers/github.provider.test.d.ts +2 -0
- package/dist/providers/github.provider.test.d.ts.map +1 -0
- package/dist/providers/github.provider.test.js +73 -0
- package/dist/providers/google.provider.d.ts +11 -0
- package/dist/providers/google.provider.d.ts.map +1 -0
- package/dist/providers/google.provider.js +61 -0
- package/dist/providers/google.provider.test.d.ts +2 -0
- package/dist/providers/google.provider.test.d.ts.map +1 -0
- package/dist/providers/google.provider.test.js +69 -0
- package/dist/providers/index.d.ts +7 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +22 -0
- package/dist/providers/microsoft.provider.d.ts +11 -0
- package/dist/providers/microsoft.provider.d.ts.map +1 -0
- package/dist/providers/microsoft.provider.js +62 -0
- package/dist/providers/microsoft.provider.test.d.ts +2 -0
- package/dist/providers/microsoft.provider.test.d.ts.map +1 -0
- package/dist/providers/microsoft.provider.test.js +62 -0
- package/dist/providers/provider.types.d.ts +94 -0
- package/dist/providers/provider.types.d.ts.map +1 -0
- package/dist/providers/provider.types.js +5 -0
- package/dist/providers/provider.types.test.d.ts +2 -0
- package/dist/providers/provider.types.test.d.ts.map +1 -0
- package/dist/providers/provider.types.test.js +57 -0
- package/dist/providers/twitter.provider.d.ts +11 -0
- package/dist/providers/twitter.provider.d.ts.map +1 -0
- package/dist/providers/twitter.provider.js +67 -0
- package/dist/providers/twitter.provider.test.d.ts +2 -0
- package/dist/providers/twitter.provider.test.d.ts.map +1 -0
- package/dist/providers/twitter.provider.test.js +71 -0
- package/package.json +56 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const google_provider_1 = require("./google.provider");
|
|
4
|
+
jest.mock('arctic', () => ({
|
|
5
|
+
Google: jest.fn().mockImplementation(() => ({})),
|
|
6
|
+
}));
|
|
7
|
+
describe('Google Provider', () => {
|
|
8
|
+
const config = {
|
|
9
|
+
clientId: 'test-id',
|
|
10
|
+
clientSecret: 'test-secret',
|
|
11
|
+
redirectUri: 'https://app.com/callback',
|
|
12
|
+
};
|
|
13
|
+
describe('createGoogleProvider', () => {
|
|
14
|
+
it('should create provider instance', () => {
|
|
15
|
+
const provider = (0, google_provider_1.createGoogleProvider)(config);
|
|
16
|
+
expect(provider).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('getGoogleDefaultScopes', () => {
|
|
20
|
+
it('should return default scopes', () => {
|
|
21
|
+
const scopes = (0, google_provider_1.getGoogleDefaultScopes)();
|
|
22
|
+
expect(scopes).toContain('openid');
|
|
23
|
+
expect(scopes).toContain('profile');
|
|
24
|
+
expect(scopes).toContain('email');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe('fetchGoogleUser', () => {
|
|
28
|
+
const originalFetch = global.fetch;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
global.fetch = jest.fn();
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
global.fetch = originalFetch;
|
|
34
|
+
});
|
|
35
|
+
it('should fetch and map user data', async () => {
|
|
36
|
+
global.fetch.mockResolvedValue({
|
|
37
|
+
ok: true,
|
|
38
|
+
json: () => Promise.resolve({
|
|
39
|
+
sub: 'google-123',
|
|
40
|
+
email: 'user@gmail.com',
|
|
41
|
+
name: 'Google User',
|
|
42
|
+
picture: 'https://google.com/photo.jpg',
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
const user = await (0, google_provider_1.fetchGoogleUser)('access-token');
|
|
46
|
+
expect(user.id).toBe('google-123');
|
|
47
|
+
expect(user.email).toBe('user@gmail.com');
|
|
48
|
+
expect(user.name).toBe('Google User');
|
|
49
|
+
expect(user.picture).toBe('https://google.com/photo.jpg');
|
|
50
|
+
});
|
|
51
|
+
it('should handle missing optional fields', async () => {
|
|
52
|
+
global.fetch.mockResolvedValue({
|
|
53
|
+
ok: true,
|
|
54
|
+
json: () => Promise.resolve({
|
|
55
|
+
sub: 'google-456',
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
const user = await (0, google_provider_1.fetchGoogleUser)('access-token');
|
|
59
|
+
expect(user.id).toBe('google-456');
|
|
60
|
+
expect(user.email).toBe('');
|
|
61
|
+
expect(user.name).toBeNull();
|
|
62
|
+
expect(user.picture).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
it('should throw on failed request', async () => {
|
|
65
|
+
global.fetch.mockResolvedValue({ ok: false });
|
|
66
|
+
await expect((0, google_provider_1.fetchGoogleUser)('bad-token')).rejects.toThrow('Failed to fetch Google user');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./provider.types"), exports);
|
|
18
|
+
__exportStar(require("./google.provider"), exports);
|
|
19
|
+
__exportStar(require("./microsoft.provider"), exports);
|
|
20
|
+
__exportStar(require("./github.provider"), exports);
|
|
21
|
+
__exportStar(require("./facebook.provider"), exports);
|
|
22
|
+
__exportStar(require("./twitter.provider"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as arctic from 'arctic';
|
|
2
|
+
import type { MicrosoftProviderConfig } from './provider.types';
|
|
3
|
+
export declare function createMicrosoftProvider(config: MicrosoftProviderConfig): arctic.MicrosoftEntraId;
|
|
4
|
+
export declare function getMicrosoftDefaultScopes(): string[];
|
|
5
|
+
export declare function fetchMicrosoftUser(accessToken: string): Promise<{
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
picture?: string | null;
|
|
10
|
+
}>;
|
|
11
|
+
//# sourceMappingURL=microsoft.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft.provider.d.ts","sourceRoot":"","sources":["../../src/providers/microsoft.provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAIhE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,uBAAuB,GAAG,MAAM,CAAC,gBAAgB,CAQhG;AAED,wBAAgB,yBAAyB,IAAI,MAAM,EAAE,CAEpD;AAED,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC,CAmBD"}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createMicrosoftProvider = createMicrosoftProvider;
|
|
37
|
+
exports.getMicrosoftDefaultScopes = getMicrosoftDefaultScopes;
|
|
38
|
+
exports.fetchMicrosoftUser = fetchMicrosoftUser;
|
|
39
|
+
const arctic = __importStar(require("arctic"));
|
|
40
|
+
const DEFAULT_SCOPES = ['openid', 'profile', 'email'];
|
|
41
|
+
function createMicrosoftProvider(config) {
|
|
42
|
+
const tenant = config.tenant || 'common';
|
|
43
|
+
return new arctic.MicrosoftEntraId(tenant, config.clientId, config.clientSecret, config.redirectUri);
|
|
44
|
+
}
|
|
45
|
+
function getMicrosoftDefaultScopes() {
|
|
46
|
+
return [...DEFAULT_SCOPES];
|
|
47
|
+
}
|
|
48
|
+
async function fetchMicrosoftUser(accessToken) {
|
|
49
|
+
const res = await fetch('https://graph.microsoft.com/oidc/userinfo', {
|
|
50
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new Error(`Failed to fetch Microsoft user: ${res.status}`);
|
|
54
|
+
}
|
|
55
|
+
const data = (await res.json());
|
|
56
|
+
return {
|
|
57
|
+
id: data.sub,
|
|
58
|
+
email: data.email || '',
|
|
59
|
+
name: data.name || null,
|
|
60
|
+
picture: data.picture ?? null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft.provider.test.d.ts","sourceRoot":"","sources":["../../src/providers/microsoft.provider.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const microsoft_provider_1 = require("./microsoft.provider");
|
|
4
|
+
jest.mock('arctic', () => ({
|
|
5
|
+
MicrosoftEntraId: jest.fn().mockImplementation(() => ({})),
|
|
6
|
+
}));
|
|
7
|
+
describe('Microsoft Provider', () => {
|
|
8
|
+
const config = {
|
|
9
|
+
clientId: 'test-id',
|
|
10
|
+
clientSecret: 'test-secret',
|
|
11
|
+
redirectUri: 'https://app.com/callback',
|
|
12
|
+
};
|
|
13
|
+
describe('createMicrosoftProvider', () => {
|
|
14
|
+
it('should create provider with common tenant by default', () => {
|
|
15
|
+
const provider = (0, microsoft_provider_1.createMicrosoftProvider)(config);
|
|
16
|
+
expect(provider).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
it('should create provider with custom tenant', () => {
|
|
19
|
+
const provider = (0, microsoft_provider_1.createMicrosoftProvider)({
|
|
20
|
+
...config,
|
|
21
|
+
tenant: 'my-tenant-id',
|
|
22
|
+
});
|
|
23
|
+
expect(provider).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('getMicrosoftDefaultScopes', () => {
|
|
27
|
+
it('should return default scopes', () => {
|
|
28
|
+
const scopes = (0, microsoft_provider_1.getMicrosoftDefaultScopes)();
|
|
29
|
+
expect(scopes).toContain('openid');
|
|
30
|
+
expect(scopes).toContain('profile');
|
|
31
|
+
expect(scopes).toContain('email');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('fetchMicrosoftUser', () => {
|
|
35
|
+
const originalFetch = global.fetch;
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
global.fetch = jest.fn();
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
global.fetch = originalFetch;
|
|
41
|
+
});
|
|
42
|
+
it('should fetch and map user data', async () => {
|
|
43
|
+
global.fetch.mockResolvedValue({
|
|
44
|
+
ok: true,
|
|
45
|
+
json: () => Promise.resolve({
|
|
46
|
+
sub: 'ms-123',
|
|
47
|
+
email: 'user@outlook.com',
|
|
48
|
+
name: 'MS User',
|
|
49
|
+
picture: 'https://graph.microsoft.com/photo',
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
const user = await (0, microsoft_provider_1.fetchMicrosoftUser)('access-token');
|
|
53
|
+
expect(user.id).toBe('ms-123');
|
|
54
|
+
expect(user.email).toBe('user@outlook.com');
|
|
55
|
+
expect(user.name).toBe('MS User');
|
|
56
|
+
});
|
|
57
|
+
it('should throw on failed request', async () => {
|
|
58
|
+
global.fetch.mockResolvedValue({ ok: false });
|
|
59
|
+
await expect((0, microsoft_provider_1.fetchMicrosoftUser)('bad-token')).rejects.toThrow('Failed to fetch Microsoft user');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth provider configuration types
|
|
3
|
+
*/
|
|
4
|
+
export interface BaseProviderConfig {
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
}
|
|
9
|
+
export type GoogleProviderConfig = BaseProviderConfig;
|
|
10
|
+
export interface MicrosoftProviderConfig extends BaseProviderConfig {
|
|
11
|
+
/** Azure AD tenant ID. Use 'common' for multi-tenant. Default: 'common' */
|
|
12
|
+
tenant?: string;
|
|
13
|
+
}
|
|
14
|
+
export type GitHubProviderConfig = BaseProviderConfig;
|
|
15
|
+
export type FacebookProviderConfig = BaseProviderConfig;
|
|
16
|
+
export interface TwitterProviderConfig extends Omit<BaseProviderConfig, 'clientSecret'> {
|
|
17
|
+
/** Optional for public clients (PKCE-only) */
|
|
18
|
+
clientSecret?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export type OAuthProviderConfig = {
|
|
21
|
+
google: GoogleProviderConfig;
|
|
22
|
+
} | {
|
|
23
|
+
microsoft: MicrosoftProviderConfig;
|
|
24
|
+
} | {
|
|
25
|
+
github: GitHubProviderConfig;
|
|
26
|
+
} | {
|
|
27
|
+
facebook: FacebookProviderConfig;
|
|
28
|
+
} | {
|
|
29
|
+
twitter: TwitterProviderConfig;
|
|
30
|
+
};
|
|
31
|
+
export interface OAuthProvidersConfig {
|
|
32
|
+
google?: GoogleProviderConfig;
|
|
33
|
+
microsoft?: MicrosoftProviderConfig;
|
|
34
|
+
github?: GitHubProviderConfig;
|
|
35
|
+
facebook?: FacebookProviderConfig;
|
|
36
|
+
twitter?: TwitterProviderConfig;
|
|
37
|
+
}
|
|
38
|
+
export type SupportedProvider = 'google' | 'microsoft' | 'github' | 'facebook' | 'twitter';
|
|
39
|
+
export interface OAuthUser {
|
|
40
|
+
id: string;
|
|
41
|
+
email: string;
|
|
42
|
+
name: string | null;
|
|
43
|
+
picture?: string | null;
|
|
44
|
+
}
|
|
45
|
+
export interface OAuthCallbackResult {
|
|
46
|
+
accessToken: string;
|
|
47
|
+
refreshToken?: string;
|
|
48
|
+
expiresAt?: Date;
|
|
49
|
+
user: OAuthUser;
|
|
50
|
+
}
|
|
51
|
+
export interface OAuthAuthorizationResult {
|
|
52
|
+
url: string;
|
|
53
|
+
state: string;
|
|
54
|
+
/** Required for PKCE providers (Google, Microsoft). Store and pass to handleCallback. */
|
|
55
|
+
codeVerifier?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Implement this interface and pass the class to OAuthModule.forRoot({ callbackHandler })
|
|
59
|
+
* to hook into the OAuth callback before the response is sent.
|
|
60
|
+
*
|
|
61
|
+
* The handler is resolved from the DI container, so it can inject any service
|
|
62
|
+
* (JwtService, a UsersRepository, etc.) through its constructor.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* @Injectable()
|
|
67
|
+
* export class MyOAuthHandler implements OAuthCallbackHandler {
|
|
68
|
+
* constructor(
|
|
69
|
+
* private readonly jwtService: JwtService,
|
|
70
|
+
* private readonly usersRepo: UsersRepository,
|
|
71
|
+
* ) {}
|
|
72
|
+
*
|
|
73
|
+
* async handle(result: OAuthCallbackResult, provider: SupportedProvider) {
|
|
74
|
+
* const user = await this.usersRepo.findOrCreate(result.user);
|
|
75
|
+
* return { token: this.jwtService.sign({ sub: user.id, role: user.role, tenantId: user.orgId }) };
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export interface OAuthCallbackHandler {
|
|
81
|
+
handle(result: OAuthCallbackResult, provider: SupportedProvider): Promise<unknown> | unknown;
|
|
82
|
+
}
|
|
83
|
+
export interface OAuthModuleOptions {
|
|
84
|
+
providers: OAuthProvidersConfig;
|
|
85
|
+
/** Default scopes per provider. Override via getAuthorizationUrl scopes param. */
|
|
86
|
+
defaultScopes?: Partial<Record<SupportedProvider, string[]>>;
|
|
87
|
+
/**
|
|
88
|
+
* Optional class (must be registered in the DI container) whose `handle()` method is
|
|
89
|
+
* called after a successful OAuth callback. Use it to look up / create the user in
|
|
90
|
+
* your database and return a JWT. When omitted the raw OAuthCallbackResult is returned.
|
|
91
|
+
*/
|
|
92
|
+
callbackHandler?: new (...args: any[]) => OAuthCallbackHandler;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=provider.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.types.d.ts","sourceRoot":"","sources":["../../src/providers/provider.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;AAEtD,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB;IACjE,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;AAEtD,MAAM,MAAM,sBAAsB,GAAG,kBAAkB,CAAC;AAExD,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,kBAAkB,EAAE,cAAc,CAAC;IACrF,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAChC;IAAE,SAAS,EAAE,uBAAuB,CAAA;CAAE,GACtC;IAAE,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAChC;IAAE,QAAQ,EAAE,sBAAsB,CAAA;CAAE,GACpC;IAAE,OAAO,EAAE,qBAAqB,CAAA;CAAE,CAAC;AAEvC,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,SAAS,CAAC,EAAE,uBAAuB,CAAC;IACpC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAE3F,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,yFAAyF;IACzF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CAC9F;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,oBAAoB,CAAC;IAChC,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAC7D;;;;OAIG;IAEH,eAAe,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,oBAAoB,CAAC;CAChE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.types.test.d.ts","sourceRoot":"","sources":["../../src/providers/provider.types.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
describe('Provider Types', () => {
|
|
4
|
+
it('should allow valid BaseProviderConfig', () => {
|
|
5
|
+
const config = {
|
|
6
|
+
clientId: 'id',
|
|
7
|
+
clientSecret: 'secret',
|
|
8
|
+
redirectUri: 'https://app.com/cb',
|
|
9
|
+
};
|
|
10
|
+
expect(config.clientId).toBe('id');
|
|
11
|
+
});
|
|
12
|
+
it('should allow GoogleProviderConfig', () => {
|
|
13
|
+
const config = {
|
|
14
|
+
clientId: 'id',
|
|
15
|
+
clientSecret: 'secret',
|
|
16
|
+
redirectUri: 'https://app.com/cb',
|
|
17
|
+
};
|
|
18
|
+
expect(config).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
it('should allow MicrosoftProviderConfig with optional tenant', () => {
|
|
21
|
+
const config = {
|
|
22
|
+
clientId: 'id',
|
|
23
|
+
clientSecret: 'secret',
|
|
24
|
+
redirectUri: 'https://app.com/cb',
|
|
25
|
+
tenant: 'common',
|
|
26
|
+
};
|
|
27
|
+
expect(config.tenant).toBe('common');
|
|
28
|
+
});
|
|
29
|
+
it('should define OAuthUser shape', () => {
|
|
30
|
+
const user = {
|
|
31
|
+
id: '123',
|
|
32
|
+
email: 'a@b.com',
|
|
33
|
+
name: 'User',
|
|
34
|
+
picture: 'https://pic.com',
|
|
35
|
+
};
|
|
36
|
+
expect(user.id).toBe('123');
|
|
37
|
+
});
|
|
38
|
+
it('should define OAuthCallbackResult shape', () => {
|
|
39
|
+
const result = {
|
|
40
|
+
accessToken: 'token',
|
|
41
|
+
user: { id: '1', email: 'e@e.com', name: 'N' },
|
|
42
|
+
};
|
|
43
|
+
expect(result.accessToken).toBe('token');
|
|
44
|
+
});
|
|
45
|
+
it('should define OAuthAuthorizationResult shape', () => {
|
|
46
|
+
const result = {
|
|
47
|
+
url: 'https://provider.com',
|
|
48
|
+
state: 'state',
|
|
49
|
+
codeVerifier: 'verifier',
|
|
50
|
+
};
|
|
51
|
+
expect(result.codeVerifier).toBe('verifier');
|
|
52
|
+
});
|
|
53
|
+
it('should include all SupportedProvider values', () => {
|
|
54
|
+
const providers = ['google', 'microsoft', 'github', 'facebook', 'twitter'];
|
|
55
|
+
expect(providers).toHaveLength(5);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as arctic from 'arctic';
|
|
2
|
+
import type { TwitterProviderConfig } from './provider.types';
|
|
3
|
+
export declare function createTwitterProvider(config: TwitterProviderConfig): arctic.Twitter;
|
|
4
|
+
export declare function getTwitterDefaultScopes(): string[];
|
|
5
|
+
export declare function fetchTwitterUser(accessToken: string): Promise<{
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
picture?: string | null;
|
|
10
|
+
}>;
|
|
11
|
+
//# sourceMappingURL=twitter.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twitter.provider.d.ts","sourceRoot":"","sources":["../../src/providers/twitter.provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAI9D,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAAC,OAAO,CAEnF;AAED,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,CAElD;AAED,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IACnE,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC,CA2BD"}
|
|
@@ -0,0 +1,67 @@
|
|
|
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 __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createTwitterProvider = createTwitterProvider;
|
|
37
|
+
exports.getTwitterDefaultScopes = getTwitterDefaultScopes;
|
|
38
|
+
exports.fetchTwitterUser = fetchTwitterUser;
|
|
39
|
+
const arctic = __importStar(require("arctic"));
|
|
40
|
+
const DEFAULT_SCOPES = ['users.read', 'tweet.read'];
|
|
41
|
+
function createTwitterProvider(config) {
|
|
42
|
+
return new arctic.Twitter(config.clientId, config.clientSecret ?? null, config.redirectUri);
|
|
43
|
+
}
|
|
44
|
+
function getTwitterDefaultScopes() {
|
|
45
|
+
return [...DEFAULT_SCOPES];
|
|
46
|
+
}
|
|
47
|
+
async function fetchTwitterUser(accessToken) {
|
|
48
|
+
const res = await fetch('https://api.twitter.com/2/users/me', {
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: `Bearer ${accessToken}`,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new Error(`Failed to fetch Twitter user: ${res.status}`);
|
|
55
|
+
}
|
|
56
|
+
const data = (await res.json());
|
|
57
|
+
const user = data.data;
|
|
58
|
+
if (!user) {
|
|
59
|
+
throw new Error('Twitter API returned no user data');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
id: user.id,
|
|
63
|
+
email: '', // Twitter API v2 does not provide email
|
|
64
|
+
name: user.name || user.username || null,
|
|
65
|
+
picture: user.profile_image_url ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twitter.provider.test.d.ts","sourceRoot":"","sources":["../../src/providers/twitter.provider.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const twitter_provider_1 = require("./twitter.provider");
|
|
4
|
+
jest.mock('arctic', () => ({
|
|
5
|
+
Twitter: jest.fn().mockImplementation(() => ({})),
|
|
6
|
+
}));
|
|
7
|
+
describe('Twitter Provider', () => {
|
|
8
|
+
const config = {
|
|
9
|
+
clientId: 'test-id',
|
|
10
|
+
clientSecret: 'test-secret',
|
|
11
|
+
redirectUri: 'https://app.com/callback',
|
|
12
|
+
};
|
|
13
|
+
describe('createTwitterProvider', () => {
|
|
14
|
+
it('should create provider with client secret', () => {
|
|
15
|
+
const provider = (0, twitter_provider_1.createTwitterProvider)(config);
|
|
16
|
+
expect(provider).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
it('should create provider without client secret', () => {
|
|
19
|
+
const provider = (0, twitter_provider_1.createTwitterProvider)({
|
|
20
|
+
clientId: config.clientId,
|
|
21
|
+
redirectUri: config.redirectUri,
|
|
22
|
+
});
|
|
23
|
+
expect(provider).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('getTwitterDefaultScopes', () => {
|
|
27
|
+
it('should return default scopes', () => {
|
|
28
|
+
const scopes = (0, twitter_provider_1.getTwitterDefaultScopes)();
|
|
29
|
+
expect(scopes).toContain('users.read');
|
|
30
|
+
expect(scopes).toContain('tweet.read');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('fetchTwitterUser', () => {
|
|
34
|
+
const originalFetch = global.fetch;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
global.fetch = jest.fn();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
global.fetch = originalFetch;
|
|
40
|
+
});
|
|
41
|
+
it('should fetch and map user data', async () => {
|
|
42
|
+
global.fetch.mockResolvedValue({
|
|
43
|
+
ok: true,
|
|
44
|
+
json: () => Promise.resolve({
|
|
45
|
+
data: {
|
|
46
|
+
id: 'tw-999',
|
|
47
|
+
name: 'Twitter User',
|
|
48
|
+
username: 'twuser',
|
|
49
|
+
profile_image_url: 'https://twitter.com/pic.png',
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
const user = await (0, twitter_provider_1.fetchTwitterUser)('access-token');
|
|
54
|
+
expect(user.id).toBe('tw-999');
|
|
55
|
+
expect(user.name).toBe('Twitter User');
|
|
56
|
+
expect(user.email).toBe('');
|
|
57
|
+
expect(user.picture).toBe('https://twitter.com/pic.png');
|
|
58
|
+
});
|
|
59
|
+
it('should throw when no user data', async () => {
|
|
60
|
+
global.fetch.mockResolvedValue({
|
|
61
|
+
ok: true,
|
|
62
|
+
json: () => Promise.resolve({}),
|
|
63
|
+
});
|
|
64
|
+
await expect((0, twitter_provider_1.fetchTwitterUser)('bad-token')).rejects.toThrow('Twitter API returned no user data');
|
|
65
|
+
});
|
|
66
|
+
it('should throw on failed request', async () => {
|
|
67
|
+
global.fetch.mockResolvedValue({ ok: false });
|
|
68
|
+
await expect((0, twitter_provider_1.fetchTwitterUser)('bad-token')).rejects.toThrow('Failed to fetch Twitter user');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hazeljs/oauth",
|
|
3
|
+
"version": "0.2.0-alpha.1",
|
|
4
|
+
"description": "OAuth 2.0 social login module for HazelJS - Microsoft, Google, GitHub and more",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "jest --coverage --passWithNoTests",
|
|
13
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
14
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"arctic": "^3.7.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.17.50",
|
|
22
|
+
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
|
23
|
+
"@typescript-eslint/parser": "^8.18.2",
|
|
24
|
+
"eslint": "^8.56.0",
|
|
25
|
+
"jest": "^29.7.0",
|
|
26
|
+
"ts-jest": "^29.1.2",
|
|
27
|
+
"typescript": "^5.3.3"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/hazel-js/hazeljs.git",
|
|
35
|
+
"directory": "packages/oauth"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"hazeljs",
|
|
39
|
+
"oauth",
|
|
40
|
+
"authentication",
|
|
41
|
+
"google",
|
|
42
|
+
"microsoft",
|
|
43
|
+
"github",
|
|
44
|
+
"social-login"
|
|
45
|
+
],
|
|
46
|
+
"author": "Muhammad Arslan <muhammad.arslan@hazeljs.com>",
|
|
47
|
+
"license": "Apache-2.0",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/hazeljs/hazel-js/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://hazeljs.com",
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@hazeljs/core": ">=0.2.0-beta.0"
|
|
54
|
+
},
|
|
55
|
+
"gitHead": "cbc5ee2c12ced28fd0576faf13c5f078c1e8421e"
|
|
56
|
+
}
|