@zshn-dev/auth-server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -0
- package/dist/__tests__/crypto.test.d.ts +2 -0
- package/dist/__tests__/crypto.test.d.ts.map +1 -0
- package/dist/__tests__/crypto.test.js +16 -0
- package/dist/__tests__/crypto.test.js.map +1 -0
- package/dist/__tests__/jwt.test.d.ts +2 -0
- package/dist/__tests__/jwt.test.d.ts.map +1 -0
- package/dist/__tests__/jwt.test.js +41 -0
- package/dist/__tests__/jwt.test.js.map +1 -0
- package/dist/__tests__/sanitize.test.d.ts +2 -0
- package/dist/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/__tests__/sanitize.test.js +31 -0
- package/dist/__tests__/sanitize.test.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/verify-jwt.d.ts +12 -0
- package/dist/middleware/verify-jwt.d.ts.map +1 -0
- package/dist/middleware/verify-jwt.js +21 -0
- package/dist/middleware/verify-jwt.js.map +1 -0
- package/dist/providers/github.d.ts +5 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +47 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +42 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/utils/jwt.d.ts +8 -0
- package/dist/utils/jwt.d.ts.map +1 -0
- package/dist/utils/jwt.js +15 -0
- package/dist/utils/jwt.js.map +1 -0
- package/dist/utils/sanitize.d.ts +4 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +20 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @your-scope/auth-server
|
|
2
|
+
|
|
3
|
+
> GitHub OAuth + JWT authentication middleware for Express.
|
|
4
|
+
|
|
5
|
+
> **Note:** `@your-scope` is a placeholder — replace it with your actual npm scope.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @your-scope/auth-server
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import express from 'express';
|
|
21
|
+
import { createAuthRouter, verifyJwt } from '@your-scope/auth-server';
|
|
22
|
+
|
|
23
|
+
const config = {
|
|
24
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
25
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
26
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
27
|
+
callbackUrl: 'http://localhost:3000/auth/github/callback',
|
|
28
|
+
afterLoginUrl: 'http://localhost:4200/auth/callback',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const app = express();
|
|
32
|
+
|
|
33
|
+
// Mount the auth router at /auth
|
|
34
|
+
app.use('/auth', createAuthRouter(config));
|
|
35
|
+
|
|
36
|
+
// Protect routes with JWT middleware
|
|
37
|
+
app.get('/api/me', verifyJwt(config), (req, res) => {
|
|
38
|
+
res.json({ user: req.user });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After a successful login, GitHub redirects to `callbackUrl`, which exchanges the OAuth code for a JWT and then redirects the user to:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
<afterLoginUrl>?token=<jwt>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## API Reference
|
|
53
|
+
|
|
54
|
+
### `createAuthRouter(config: AuthServerConfig): Router`
|
|
55
|
+
|
|
56
|
+
Returns an Express `Router`. Mount it at `/auth`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
app.use('/auth', createAuthRouter(config));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Routes:**
|
|
63
|
+
|
|
64
|
+
| Method | Path | Description |
|
|
65
|
+
| ------ | ----------------- | ------------------------------------------------------------------------ |
|
|
66
|
+
| GET | `/github` | Redirects the user to the GitHub OAuth authorization page |
|
|
67
|
+
| GET | `/github/callback`| Exchanges the OAuth code for a JWT; redirects to `afterLoginUrl?token=…` |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### `AuthServerConfig`
|
|
72
|
+
|
|
73
|
+
| Property | Type | Required | Default | Description |
|
|
74
|
+
| ----------------- | ----------------------------------------------- | -------- | -------------- | -------------------------------------------------------------- |
|
|
75
|
+
| `clientId` | `string` | ✅ | — | GitHub OAuth App client ID |
|
|
76
|
+
| `clientSecret` | `string` | ✅ | — | GitHub OAuth App client secret |
|
|
77
|
+
| `jwtSecret` | `string` | ✅ | — | Secret used to sign/verify JWTs — **must be 32+ characters** |
|
|
78
|
+
| `callbackUrl` | `string` | ✅ | — | Full URL GitHub redirects to after authorization |
|
|
79
|
+
| `afterLoginUrl` | `string` | ✅ | — | URL of your frontend app; receives `?token=<jwt>` on success |
|
|
80
|
+
| `transformUser` | `(profile: GitHubProfile) => Partial<User>` | ❌ | — | Optional hook to customize the JWT payload from the GitHub profile |
|
|
81
|
+
| `stateCookieName` | `string` | ❌ | `oauth_state` | Name of the cookie used to store the OAuth CSRF state value |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### `verifyJwt(config: AuthServerConfig): RequestHandler`
|
|
86
|
+
|
|
87
|
+
Returns an Express middleware that validates a JWT on every request.
|
|
88
|
+
|
|
89
|
+
- Reads the `Authorization: Bearer <token>` header
|
|
90
|
+
- Verifies the token against `config.jwtSecret`
|
|
91
|
+
- Sets `req.user` (typed as `JwtPayload`) on success
|
|
92
|
+
- Returns `401 Unauthorized` if the token is missing, invalid, or expired
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
app.get('/api/profile', verifyJwt(config), (req, res) => {
|
|
96
|
+
res.json(req.user);
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## `transformUser` Hook
|
|
103
|
+
|
|
104
|
+
Use `transformUser` to control which fields from the GitHub profile are included in the JWT payload:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { createAuthRouter, GitHubProfile } from '@your-scope/auth-server';
|
|
108
|
+
|
|
109
|
+
app.use('/auth', createAuthRouter({
|
|
110
|
+
...config,
|
|
111
|
+
transformUser: (profile: GitHubProfile) => ({
|
|
112
|
+
id: profile.id,
|
|
113
|
+
login: profile.login,
|
|
114
|
+
name: profile.name,
|
|
115
|
+
avatarUrl: profile.avatar_url,
|
|
116
|
+
email: profile.email,
|
|
117
|
+
}),
|
|
118
|
+
}));
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The returned object is merged into the JWT payload and later available on `req.user`.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Environment Variables
|
|
126
|
+
|
|
127
|
+
Never hardcode secrets. Use a `.env` file (via [dotenv](https://github.com/motdotla/dotenv)) or your deployment platform's secret management:
|
|
128
|
+
|
|
129
|
+
```env
|
|
130
|
+
GITHUB_CLIENT_ID=your_github_client_id
|
|
131
|
+
GITHUB_CLIENT_SECRET=your_github_client_secret
|
|
132
|
+
# Must be at least 32 characters
|
|
133
|
+
JWT_SECRET=a_very_long_random_secret_at_least_32_chars
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import 'dotenv/config';
|
|
138
|
+
|
|
139
|
+
const config = {
|
|
140
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
141
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
142
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
143
|
+
callbackUrl: process.env.CALLBACK_URL ?? 'http://localhost:3000/auth/github/callback',
|
|
144
|
+
afterLoginUrl: process.env.AFTER_LOGIN_URL ?? 'http://localhost:4200/auth/callback',
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Add `.env` to your `.gitignore` to avoid committing secrets:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
.env
|
|
152
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/crypto.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const crypto_js_1 = require("../utils/crypto.js");
|
|
5
|
+
(0, vitest_1.describe)('generateState', () => {
|
|
6
|
+
(0, vitest_1.it)('returns a 64-character lowercase hex string', () => {
|
|
7
|
+
const state = (0, crypto_js_1.generateState)();
|
|
8
|
+
(0, vitest_1.expect)(state).toMatch(/^[0-9a-f]{64}$/);
|
|
9
|
+
});
|
|
10
|
+
(0, vitest_1.it)('returns a different value on each call', () => {
|
|
11
|
+
const a = (0, crypto_js_1.generateState)();
|
|
12
|
+
const b = (0, crypto_js_1.generateState)();
|
|
13
|
+
(0, vitest_1.expect)(a).not.toBe(b);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=crypto.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.test.js","sourceRoot":"","sources":["../../src/__tests__/crypto.test.ts"],"names":[],"mappings":";;AAAA,mCAA8C;AAC9C,kDAAmD;AAEnD,IAAA,iBAAQ,EAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,IAAA,WAAE,EAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,KAAK,GAAG,IAAA,yBAAa,GAAE,CAAC;QAC9B,IAAA,eAAM,EAAC,KAAK,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,GAAG,IAAA,yBAAa,GAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAA,yBAAa,GAAE,CAAC;QAC1B,IAAA,eAAM,EAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/jwt.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const jwt_js_1 = require("../utils/jwt.js");
|
|
5
|
+
const SECRET = 'a-test-secret-that-is-at-least-32-characters-long';
|
|
6
|
+
const USER = {
|
|
7
|
+
id: '42',
|
|
8
|
+
email: 'user@example.com',
|
|
9
|
+
name: 'Test User',
|
|
10
|
+
avatar_url: 'https://example.com/avatar.png',
|
|
11
|
+
};
|
|
12
|
+
(0, vitest_1.describe)('signJwt', () => {
|
|
13
|
+
(0, vitest_1.it)('returns a three-part JWT string', () => {
|
|
14
|
+
const token = (0, jwt_js_1.signJwt)(USER, SECRET);
|
|
15
|
+
const parts = token.split('.');
|
|
16
|
+
(0, vitest_1.expect)(parts).toHaveLength(3);
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.it)('embeds the user payload in the token', () => {
|
|
19
|
+
const token = (0, jwt_js_1.signJwt)(USER, SECRET);
|
|
20
|
+
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
21
|
+
(0, vitest_1.expect)(payload.id).toBe('42');
|
|
22
|
+
(0, vitest_1.expect)(payload.email).toBe('user@example.com');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.describe)('verifyJwtToken', () => {
|
|
26
|
+
(0, vitest_1.it)('round-trips: verify returns the original user payload', () => {
|
|
27
|
+
const token = (0, jwt_js_1.signJwt)(USER, SECRET);
|
|
28
|
+
const payload = (0, jwt_js_1.verifyJwtToken)(token, SECRET);
|
|
29
|
+
(0, vitest_1.expect)(payload.id).toBe(USER.id);
|
|
30
|
+
(0, vitest_1.expect)(payload.email).toBe(USER.email);
|
|
31
|
+
(0, vitest_1.expect)(payload.name).toBe(USER.name);
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)('throws when the secret is wrong', () => {
|
|
34
|
+
const token = (0, jwt_js_1.signJwt)(USER, SECRET);
|
|
35
|
+
(0, vitest_1.expect)(() => (0, jwt_js_1.verifyJwtToken)(token, 'wrong-secret')).toThrow();
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)('throws for a malformed token', () => {
|
|
38
|
+
(0, vitest_1.expect)(() => (0, jwt_js_1.verifyJwtToken)('not.a.token', SECRET)).toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=jwt.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.test.js","sourceRoot":"","sources":["../../src/__tests__/jwt.test.ts"],"names":[],"mappings":";;AAAA,mCAA8C;AAC9C,4CAA0D;AAE1D,MAAM,MAAM,GAAG,mDAAmD,CAAC;AACnE,MAAM,IAAI,GAAG;IACX,EAAE,EAAE,IAAI;IACR,KAAK,EAAE,kBAAkB;IACzB,IAAI,EAAE,WAAW;IACjB,UAAU,EAAE,gCAAgC;CAC7C,CAAC;AAEF,IAAA,iBAAQ,EAAC,SAAS,EAAE,GAAG,EAAE;IACvB,IAAA,WAAE,EAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,KAAK,GAAG,IAAA,gBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAA,eAAM,EAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,KAAK,GAAG,IAAA,gBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClF,IAAA,eAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAA,eAAM,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,IAAA,WAAE,EAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,KAAK,GAAG,IAAA,gBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,IAAA,uBAAc,EAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC9C,IAAA,eAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,IAAA,eAAM,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,KAAK,GAAG,IAAA,gBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,IAAA,eAAM,EAAC,GAAG,EAAE,CAAC,IAAA,uBAAc,EAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,IAAA,eAAM,EAAC,GAAG,EAAE,CAAC,IAAA,uBAAc,EAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sanitize.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
5
|
+
(0, vitest_1.describe)('isSafeUrl', () => {
|
|
6
|
+
(0, vitest_1.it)('accepts http URLs', () => {
|
|
7
|
+
(0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('http://localhost:3000')).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
(0, vitest_1.it)('accepts https URLs', () => {
|
|
10
|
+
(0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('https://example.com/callback')).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
(0, vitest_1.it)('rejects javascript: protocol', () => {
|
|
13
|
+
(0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('javascript:alert(1)')).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)('rejects ftp: protocol', () => {
|
|
16
|
+
(0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('ftp://example.com')).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.it)('rejects plain strings that are not URLs', () => {
|
|
19
|
+
(0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('not-a-url')).toBe(false);
|
|
20
|
+
(0, vitest_1.expect)((0, sanitize_js_1.isSafeUrl)('')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
(0, vitest_1.describe)('assertConfig', () => {
|
|
24
|
+
(0, vitest_1.it)('does not throw when condition is true', () => {
|
|
25
|
+
(0, vitest_1.expect)(() => (0, sanitize_js_1.assertConfig)(true, 'should not throw')).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
(0, vitest_1.it)('throws an Error with the given message when condition is false', () => {
|
|
28
|
+
(0, vitest_1.expect)(() => (0, sanitize_js_1.assertConfig)(false, 'clientId is required')).toThrowError('[auth-server] Invalid config: clientId is required');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=sanitize.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.test.js","sourceRoot":"","sources":["../../src/__tests__/sanitize.test.ts"],"names":[],"mappings":";;AAAA,mCAA8C;AAC9C,sDAA+D;AAE/D,IAAA,iBAAQ,EAAC,WAAW,EAAE,GAAG,EAAE;IACzB,IAAA,WAAE,EAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,8BAA8B,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAA,eAAM,EAAC,IAAA,uBAAS,EAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,IAAA,WAAE,EAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,IAAA,eAAM,EAAC,GAAG,EAAE,CAAC,IAAA,0BAAY,EAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,IAAA,eAAM,EAAC,GAAG,EAAE,CAAC,IAAA,0BAAY,EAAC,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,YAAY,CACpE,oDAAoD,CACrD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Router, RequestHandler } from 'express';
|
|
2
|
+
import { AuthServerConfig } from './types.js';
|
|
3
|
+
import { JwtPayload } from './middleware/verify-jwt.js';
|
|
4
|
+
export type { JwtPayload };
|
|
5
|
+
declare global {
|
|
6
|
+
namespace Express {
|
|
7
|
+
interface Request {
|
|
8
|
+
user?: JwtPayload;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates an Express Router that handles the GitHub OAuth flow.
|
|
14
|
+
*
|
|
15
|
+
* Mount it at a path like `/auth`:
|
|
16
|
+
* ```ts
|
|
17
|
+
* app.use('/auth', createAuthRouter({ ... }));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Routes provided:
|
|
21
|
+
* - `GET /github` — redirects to GitHub's authorization page
|
|
22
|
+
* - `GET /github/callback` — handles the OAuth callback, signs a JWT, and redirects to the Angular app
|
|
23
|
+
*/
|
|
24
|
+
export declare function createAuthRouter(config: AuthServerConfig): Router;
|
|
25
|
+
/**
|
|
26
|
+
* Express middleware that validates a Bearer JWT and attaches the decoded payload to `req.user`.
|
|
27
|
+
* Returns 401 if the token is missing, malformed, or expired.
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyJwt(config: Pick<AuthServerConfig, 'jwtSecret'>): RequestHandler;
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAQ,MAAM,YAAY,CAAC;AAKpD,OAAO,EAA6B,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAInF,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,UAAU,CAAC;SACnB;KACF;CACF;AAUD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAoDjE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,GAAG,cAAc,CAErF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createAuthRouter = createAuthRouter;
|
|
7
|
+
exports.verifyJwt = verifyJwt;
|
|
8
|
+
const express_1 = require("express");
|
|
9
|
+
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
|
10
|
+
const crypto_js_1 = require("./utils/crypto.js");
|
|
11
|
+
const jwt_js_1 = require("./utils/jwt.js");
|
|
12
|
+
const sanitize_js_1 = require("./utils/sanitize.js");
|
|
13
|
+
const github_js_1 = require("./providers/github.js");
|
|
14
|
+
const verify_jwt_js_1 = require("./middleware/verify-jwt.js");
|
|
15
|
+
function validateConfig(config) {
|
|
16
|
+
(0, sanitize_js_1.assertConfig)(!!config.clientId, 'clientId is required');
|
|
17
|
+
(0, sanitize_js_1.assertConfig)(!!config.clientSecret, 'clientSecret is required');
|
|
18
|
+
(0, sanitize_js_1.assertConfig)(!!config.jwtSecret && config.jwtSecret.length >= 32, 'jwtSecret must be at least 32 characters');
|
|
19
|
+
(0, sanitize_js_1.assertConfig)((0, sanitize_js_1.isSafeUrl)(config.callbackUrl), 'callbackUrl must be a valid http/https URL');
|
|
20
|
+
(0, sanitize_js_1.assertConfig)((0, sanitize_js_1.isSafeUrl)(config.afterLoginUrl), 'afterLoginUrl must be a valid http/https URL');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates an Express Router that handles the GitHub OAuth flow.
|
|
24
|
+
*
|
|
25
|
+
* Mount it at a path like `/auth`:
|
|
26
|
+
* ```ts
|
|
27
|
+
* app.use('/auth', createAuthRouter({ ... }));
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Routes provided:
|
|
31
|
+
* - `GET /github` — redirects to GitHub's authorization page
|
|
32
|
+
* - `GET /github/callback` — handles the OAuth callback, signs a JWT, and redirects to the Angular app
|
|
33
|
+
*/
|
|
34
|
+
function createAuthRouter(config) {
|
|
35
|
+
validateConfig(config);
|
|
36
|
+
const stateCookieName = config.stateCookieName ?? 'oauth_state';
|
|
37
|
+
const router = (0, express_1.Router)();
|
|
38
|
+
router.use((0, cookie_parser_1.default)());
|
|
39
|
+
// Step 1: redirect to GitHub
|
|
40
|
+
router.get('/github', (_req, res) => {
|
|
41
|
+
const state = (0, crypto_js_1.generateState)();
|
|
42
|
+
res.cookie(stateCookieName, state, { httpOnly: true, sameSite: 'lax', maxAge: 10 * 60 * 1000 });
|
|
43
|
+
res.redirect((0, github_js_1.buildAuthUrl)(config.clientId, state, config.callbackUrl));
|
|
44
|
+
});
|
|
45
|
+
// Step 2: handle callback
|
|
46
|
+
router.get('/github/callback', async (req, res) => {
|
|
47
|
+
const { code, state } = req.query;
|
|
48
|
+
const savedState = req.cookies[stateCookieName];
|
|
49
|
+
if (!code || !state || state !== savedState) {
|
|
50
|
+
res.status(400).send('Invalid OAuth state');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
res.clearCookie(stateCookieName);
|
|
54
|
+
try {
|
|
55
|
+
const accessToken = await (0, github_js_1.exchangeCode)(config.clientId, config.clientSecret, code);
|
|
56
|
+
const profile = await (0, github_js_1.fetchGitHubUser)(accessToken);
|
|
57
|
+
const baseUser = {
|
|
58
|
+
id: String(profile.id),
|
|
59
|
+
email: profile.email ?? '',
|
|
60
|
+
name: profile.name ?? profile.login,
|
|
61
|
+
avatar_url: profile.avatar_url,
|
|
62
|
+
};
|
|
63
|
+
const user = config.transformUser
|
|
64
|
+
? { ...baseUser, ...config.transformUser(profile) }
|
|
65
|
+
: baseUser;
|
|
66
|
+
const token = (0, jwt_js_1.signJwt)(user, config.jwtSecret);
|
|
67
|
+
const redirectUrl = new URL(config.afterLoginUrl);
|
|
68
|
+
redirectUrl.searchParams.set('token', token);
|
|
69
|
+
res.redirect(redirectUrl.toString());
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error('[auth-server] OAuth callback error:', err);
|
|
73
|
+
res.status(500).send('Authentication failed');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return router;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Express middleware that validates a Bearer JWT and attaches the decoded payload to `req.user`.
|
|
80
|
+
* Returns 401 if the token is missing, malformed, or expired.
|
|
81
|
+
*/
|
|
82
|
+
function verifyJwt(config) {
|
|
83
|
+
return (0, verify_jwt_js_1.createVerifyJwtMiddleware)(config.jwtSecret);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RequestHandler } from 'express';
|
|
2
|
+
import { JwtPayload } from '../utils/jwt.js';
|
|
3
|
+
export type { JwtPayload };
|
|
4
|
+
declare global {
|
|
5
|
+
namespace Express {
|
|
6
|
+
interface Request {
|
|
7
|
+
user?: JwtPayload;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export declare function createVerifyJwtMiddleware(jwtSecret: string): RequestHandler;
|
|
12
|
+
//# sourceMappingURL=verify-jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-jwt.d.ts","sourceRoot":"","sources":["../../src/middleware/verify-jwt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAkB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,UAAU,CAAC;SACnB;KACF;CACF;AAED,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAc3E"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createVerifyJwtMiddleware = createVerifyJwtMiddleware;
|
|
4
|
+
const jwt_js_1 = require("../utils/jwt.js");
|
|
5
|
+
function createVerifyJwtMiddleware(jwtSecret) {
|
|
6
|
+
return (req, res, next) => {
|
|
7
|
+
const authHeader = req.headers['authorization'];
|
|
8
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
9
|
+
res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
req.user = (0, jwt_js_1.verifyJwtToken)(authHeader.slice(7), jwtSecret);
|
|
14
|
+
next();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
res.status(401).json({ error: 'Invalid or expired token' });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=verify-jwt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-jwt.js","sourceRoot":"","sources":["../../src/middleware/verify-jwt.ts"],"names":[],"mappings":";;AAcA,8DAcC;AA3BD,4CAA6D;AAa7D,SAAgB,yBAAyB,CAAC,SAAiB;IACzD,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACvC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,GAAG,IAAA,uBAAc,EAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;YAC1D,IAAI,EAAE,CAAC;QACT,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { GitHubProfile } from '../types.js';
|
|
2
|
+
export declare function buildAuthUrl(clientId: string, state: string, callbackUrl: string): string;
|
|
3
|
+
export declare function exchangeCode(clientId: string, clientSecret: string, code: string): Promise<string>;
|
|
4
|
+
export declare function fetchGitHubUser(accessToken: string): Promise<GitHubProfile>;
|
|
5
|
+
//# sourceMappingURL=github.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/providers/github.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAK5C,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAQzF;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAyBjF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAuthUrl = buildAuthUrl;
|
|
4
|
+
exports.exchangeCode = exchangeCode;
|
|
5
|
+
exports.fetchGitHubUser = fetchGitHubUser;
|
|
6
|
+
const GITHUB_API = 'https://api.github.com';
|
|
7
|
+
const GITHUB_OAUTH = 'https://github.com';
|
|
8
|
+
function buildAuthUrl(clientId, state, callbackUrl) {
|
|
9
|
+
const params = new URLSearchParams({
|
|
10
|
+
client_id: clientId,
|
|
11
|
+
redirect_uri: callbackUrl,
|
|
12
|
+
scope: 'read:user user:email',
|
|
13
|
+
state,
|
|
14
|
+
});
|
|
15
|
+
return `${GITHUB_OAUTH}/login/oauth/authorize?${params}`;
|
|
16
|
+
}
|
|
17
|
+
async function exchangeCode(clientId, clientSecret, code) {
|
|
18
|
+
const res = await fetch(`${GITHUB_OAUTH}/login/oauth/access_token`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code }),
|
|
22
|
+
});
|
|
23
|
+
const data = (await res.json());
|
|
24
|
+
if (!data.access_token) {
|
|
25
|
+
throw new Error(`GitHub token exchange failed: ${data.error ?? 'unknown error'}`);
|
|
26
|
+
}
|
|
27
|
+
return data.access_token;
|
|
28
|
+
}
|
|
29
|
+
async function fetchGitHubUser(accessToken) {
|
|
30
|
+
const headers = {
|
|
31
|
+
Authorization: `Bearer ${accessToken}`,
|
|
32
|
+
Accept: 'application/vnd.github+json',
|
|
33
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
34
|
+
};
|
|
35
|
+
const [profileRes, emailsRes] = await Promise.all([
|
|
36
|
+
fetch(`${GITHUB_API}/user`, { headers }),
|
|
37
|
+
fetch(`${GITHUB_API}/user/emails`, { headers }),
|
|
38
|
+
]);
|
|
39
|
+
const profile = (await profileRes.json());
|
|
40
|
+
if (!profile.email) {
|
|
41
|
+
const emails = (await emailsRes.json());
|
|
42
|
+
const primary = emails.find((e) => e.primary && e.verified);
|
|
43
|
+
profile.email = primary?.email ?? null;
|
|
44
|
+
}
|
|
45
|
+
return profile;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/providers/github.ts"],"names":[],"mappings":";;AAKA,oCAQC;AAED,oCAeC;AAED,0CAyBC;AAvDD,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAC5C,MAAM,YAAY,GAAG,oBAAoB,CAAC;AAE1C,SAAgB,YAAY,CAAC,QAAgB,EAAE,KAAa,EAAE,WAAmB;IAC/E,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,QAAQ;QACnB,YAAY,EAAE,WAAW;QACzB,KAAK,EAAE,sBAAsB;QAC7B,KAAK;KACN,CAAC,CAAC;IACH,OAAO,GAAG,YAAY,0BAA0B,MAAM,EAAE,CAAC;AAC3D,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,QAAgB,EAChB,YAAoB,EACpB,IAAY;IAEZ,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,YAAY,2BAA2B,EAAE;QAClE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;KACjF,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA8C,CAAC;IAC7E,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,KAAK,IAAI,eAAe,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,IAAI,CAAC,YAAY,CAAC;AAC3B,CAAC;AAEM,KAAK,UAAU,eAAe,CAAC,WAAmB;IACvD,MAAM,OAAO,GAAG;QACd,aAAa,EAAE,UAAU,WAAW,EAAE;QACtC,MAAM,EAAE,6BAA6B;QACrC,sBAAsB,EAAE,YAAY;KACrC,CAAC;IAEF,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAChD,KAAK,CAAC,GAAG,UAAU,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC;QACxC,KAAK,CAAC,GAAG,UAAU,cAAc,EAAE,EAAE,OAAO,EAAE,CAAC;KAChD,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,CAAC,MAAM,UAAU,CAAC,IAAI,EAAE,CAAkB,CAAC;IAE3D,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,CAIpC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC5D,OAAO,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;IACzC,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface AuthServerConfig {
|
|
2
|
+
/** GitHub OAuth client ID */
|
|
3
|
+
clientId: string;
|
|
4
|
+
/** GitHub OAuth client secret */
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
/** JWT signing secret */
|
|
7
|
+
jwtSecret: string;
|
|
8
|
+
/** URL to redirect to after successful login (e.g. http://localhost:4200/auth/callback) */
|
|
9
|
+
callbackUrl: string;
|
|
10
|
+
/** URL the Angular app sends users back to after the OAuth callback completes */
|
|
11
|
+
afterLoginUrl: string;
|
|
12
|
+
/** Optional: transform the raw GitHub profile before it is signed into the JWT */
|
|
13
|
+
transformUser?: (profile: GitHubProfile) => Partial<User>;
|
|
14
|
+
/** Cookie name used for the CSRF state param (default: "oauth_state") */
|
|
15
|
+
stateCookieName?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface GitHubProfile {
|
|
18
|
+
id: number;
|
|
19
|
+
login: string;
|
|
20
|
+
name: string | null;
|
|
21
|
+
email: string | null;
|
|
22
|
+
avatar_url: string;
|
|
23
|
+
}
|
|
24
|
+
/** Normalised user stored in the JWT */
|
|
25
|
+
export interface User {
|
|
26
|
+
id: string;
|
|
27
|
+
email: string;
|
|
28
|
+
name: string;
|
|
29
|
+
avatar_url: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wCAAwC;AACxC,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":"AAEA,2EAA2E;AAC3E,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
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.generateState = generateState;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
/** Generate a cryptographically-random state string for CSRF protection */
|
|
39
|
+
function generateState() {
|
|
40
|
+
return crypto.randomBytes(32).toString('hex');
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,sCAEC;AALD,+CAAiC;AAEjC,2EAA2E;AAC3E,SAAgB,aAAa;IAC3B,OAAO,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { User } from '../types.js';
|
|
2
|
+
export interface JwtPayload extends User {
|
|
3
|
+
iat?: number;
|
|
4
|
+
exp?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function signJwt(user: User, secret: string, expiresIn?: string): string;
|
|
7
|
+
export declare function verifyJwtToken(token: string, secret: string): JwtPayload;
|
|
8
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/utils/jwt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,MAAM,WAAW,UAAW,SAAQ,IAAI;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,MAAM,CAE5E;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAExE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.signJwt = signJwt;
|
|
7
|
+
exports.verifyJwtToken = verifyJwtToken;
|
|
8
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
|
+
function signJwt(user, secret, expiresIn = '7d') {
|
|
10
|
+
return jsonwebtoken_1.default.sign(user, secret, { expiresIn });
|
|
11
|
+
}
|
|
12
|
+
function verifyJwtToken(token, secret) {
|
|
13
|
+
return jsonwebtoken_1.default.verify(token, secret);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=jwt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../../src/utils/jwt.ts"],"names":[],"mappings":";;;;;AAQA,0BAEC;AAED,wCAEC;AAdD,gEAA+B;AAQ/B,SAAgB,OAAO,CAAC,IAAU,EAAE,MAAc,EAAE,SAAS,GAAG,IAAI;IAClE,OAAO,sBAAG,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,EAAqB,CAAC,CAAC;AAClE,CAAC;AAED,SAAgB,cAAc,CAAC,KAAa,EAAE,MAAc;IAC1D,OAAO,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAe,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOhD;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAItE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSafeUrl = isSafeUrl;
|
|
4
|
+
exports.assertConfig = assertConfig;
|
|
5
|
+
/** Validate that a string is a safe URL (http/https only, no javascript: etc.) */
|
|
6
|
+
function isSafeUrl(value) {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(value);
|
|
9
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function assertConfig(condition, message) {
|
|
16
|
+
if (!condition) {
|
|
17
|
+
throw new Error(`[auth-server] Invalid config: ${message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":";;AACA,8BAOC;AAED,oCAIC;AAdD,kFAAkF;AAClF,SAAgB,SAAS,CAAC,KAAa;IACrC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAgB,YAAY,CAAC,SAAkB,EAAE,OAAe;IAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,iCAAiC,OAAO,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zshn-dev/auth-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Angular-first auth server SDK — Express router for GitHub OAuth",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"github",
|
|
7
|
+
"oauth",
|
|
8
|
+
"jwt",
|
|
9
|
+
"express",
|
|
10
|
+
"angular",
|
|
11
|
+
"auth"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/zshn-dev/ngx-gh-auth.git",
|
|
17
|
+
"directory": "packages/auth-server"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/zshn-dev/ngx-gh-auth#readme",
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"cookie-parser": "^1.4.7",
|
|
32
|
+
"express": "^4.21.2",
|
|
33
|
+
"jsonwebtoken": "^9.0.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/cookie-parser": "^1.4.8",
|
|
37
|
+
"@types/express": "^5.0.1",
|
|
38
|
+
"@types/jsonwebtoken": "^9.0.9",
|
|
39
|
+
"@types/node": "^22.15.0",
|
|
40
|
+
"typescript": "~5.9.2",
|
|
41
|
+
"vitest": "^4.0.8"
|
|
42
|
+
}
|
|
43
|
+
}
|