@authrim/server 0.1.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 +610 -0
- package/dist/adapters/express.cjs +3 -0
- package/dist/adapters/express.cjs.map +1 -0
- package/dist/adapters/express.d.cts +75 -0
- package/dist/adapters/express.d.ts +75 -0
- package/dist/adapters/express.js +3 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fastify.cjs +3 -0
- package/dist/adapters/fastify.cjs.map +1 -0
- package/dist/adapters/fastify.d.cts +101 -0
- package/dist/adapters/fastify.d.ts +101 -0
- package/dist/adapters/fastify.js +3 -0
- package/dist/adapters/fastify.js.map +1 -0
- package/dist/adapters/hono.cjs +2 -0
- package/dist/adapters/hono.cjs.map +1 -0
- package/dist/adapters/hono.d.cts +85 -0
- package/dist/adapters/hono.d.ts +85 -0
- package/dist/adapters/hono.js +2 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/koa.cjs +3 -0
- package/dist/adapters/koa.cjs.map +1 -0
- package/dist/adapters/koa.d.cts +75 -0
- package/dist/adapters/koa.d.ts +75 -0
- package/dist/adapters/koa.js +3 -0
- package/dist/adapters/koa.js.map +1 -0
- package/dist/adapters/nestjs.cjs +3 -0
- package/dist/adapters/nestjs.cjs.map +1 -0
- package/dist/adapters/nestjs.d.cts +126 -0
- package/dist/adapters/nestjs.d.ts +126 -0
- package/dist/adapters/nestjs.js +3 -0
- package/dist/adapters/nestjs.js.map +1 -0
- package/dist/chunk-7POGA5LZ.cjs +3 -0
- package/dist/chunk-7POGA5LZ.cjs.map +1 -0
- package/dist/chunk-N3ONRO35.js +2 -0
- package/dist/chunk-N3ONRO35.js.map +1 -0
- package/dist/chunk-O2ALCNXB.cjs +2 -0
- package/dist/chunk-O2ALCNXB.cjs.map +1 -0
- package/dist/chunk-OS567YCE.js +3 -0
- package/dist/chunk-OS567YCE.js.map +1 -0
- package/dist/chunk-TPROSFE7.cjs +2 -0
- package/dist/chunk-TPROSFE7.cjs.map +1 -0
- package/dist/chunk-XOFM2JHF.js +2 -0
- package/dist/chunk-XOFM2JHF.js.map +1 -0
- package/dist/config-I0GIVJA_.d.cts +364 -0
- package/dist/config-I0GIVJA_.d.ts +364 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +791 -0
- package/dist/index.d.ts +791 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.cjs +2 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +79 -0
- package/dist/providers/index.d.ts +79 -0
- package/dist/providers/index.js +2 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/types-CzpMdWFR.d.cts +435 -0
- package/dist/types-D7gjcvs9.d.ts +435 -0
- package/package.json +119 -0
package/README.md
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# @authrim/server
|
|
2
|
+
|
|
3
|
+
Official Authrim Server SDK for OAuth 2.0 / OpenID Connect resource server implementation.
|
|
4
|
+
|
|
5
|
+
This SDK is part of the [Authrim](https://github.com/authrim) identity platform, providing token validation, DPoP support, and framework middleware for server-side applications.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **JWT Access Token Validation** - Signature verification and claims validation (RFC 7519)
|
|
10
|
+
- **JWKS Management** - Automatic key fetching, caching, and rotation
|
|
11
|
+
- **DPoP Support** - RFC 9449 compliant proof validation
|
|
12
|
+
- **Token Introspection** - RFC 7662 support
|
|
13
|
+
- **Token Revocation** - RFC 7009 support
|
|
14
|
+
- **Back-Channel Logout** - OIDC Back-Channel Logout 1.0 support
|
|
15
|
+
- **Framework Middleware** - Express, Fastify, Hono, Koa, NestJS
|
|
16
|
+
- **SCIM 2.0** - User and Group provisioning (RFC 7643/7644)
|
|
17
|
+
- **Verifiable Credentials** - OpenID4VCI and OpenID4VP support
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @authrim/server
|
|
23
|
+
# or
|
|
24
|
+
pnpm add @authrim/server
|
|
25
|
+
# or
|
|
26
|
+
yarn add @authrim/server
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { AuthrimServer } from '@authrim/server';
|
|
33
|
+
|
|
34
|
+
const server = new AuthrimServer({
|
|
35
|
+
issuer: 'https://auth.example.com',
|
|
36
|
+
audience: 'https://api.example.com',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await server.init();
|
|
40
|
+
|
|
41
|
+
// Validate a token
|
|
42
|
+
const result = await server.validateToken(accessToken);
|
|
43
|
+
if (result.data) {
|
|
44
|
+
console.log('User:', result.data.claims.sub);
|
|
45
|
+
console.log('Token Type:', result.data.tokenType); // 'Bearer' or 'DPoP'
|
|
46
|
+
} else {
|
|
47
|
+
console.error('Error:', result.error.code, result.error.message);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Runtime Support
|
|
52
|
+
|
|
53
|
+
This SDK uses Web Standard APIs (fetch, crypto.subtle) and runs on:
|
|
54
|
+
|
|
55
|
+
- Node.js 18+
|
|
56
|
+
- Bun
|
|
57
|
+
- Deno
|
|
58
|
+
- Cloudflare Workers
|
|
59
|
+
- Vercel Edge Functions
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const server = new AuthrimServer({
|
|
65
|
+
// Required
|
|
66
|
+
issuer: 'https://auth.example.com', // Expected token issuer
|
|
67
|
+
audience: 'https://api.example.com', // Expected audience (string or string[])
|
|
68
|
+
|
|
69
|
+
// JWKS (one of these is required)
|
|
70
|
+
jwksUri: 'https://auth.example.com/.well-known/jwks.json', // Explicit JWKS URI
|
|
71
|
+
// or omit to use OpenID Discovery (issuer + /.well-known/openid-configuration)
|
|
72
|
+
|
|
73
|
+
// Optional: Token operations
|
|
74
|
+
introspectionEndpoint: 'https://auth.example.com/oauth/introspect',
|
|
75
|
+
revocationEndpoint: 'https://auth.example.com/oauth/revoke',
|
|
76
|
+
clientCredentials: {
|
|
77
|
+
clientId: 'resource-server',
|
|
78
|
+
clientSecret: 'secret',
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Optional: Timing
|
|
82
|
+
clockToleranceSeconds: 60, // Clock skew tolerance (default: 60)
|
|
83
|
+
jwksRefreshIntervalMs: 3600000, // JWKS cache TTL (default: 1 hour)
|
|
84
|
+
|
|
85
|
+
// Optional: Security
|
|
86
|
+
requireHttps: true, // Require HTTPS for all endpoints (default: true)
|
|
87
|
+
|
|
88
|
+
// Optional: Custom providers (for testing/customization)
|
|
89
|
+
http: customHttpProvider,
|
|
90
|
+
crypto: customCryptoProvider,
|
|
91
|
+
clock: customClockProvider,
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Framework Middleware
|
|
96
|
+
|
|
97
|
+
### Express
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import express from 'express';
|
|
101
|
+
import { AuthrimServer } from '@authrim/server';
|
|
102
|
+
import { authrimMiddleware } from '@authrim/server/adapters/express';
|
|
103
|
+
|
|
104
|
+
const app = express();
|
|
105
|
+
const server = new AuthrimServer({
|
|
106
|
+
issuer: 'https://auth.example.com',
|
|
107
|
+
audience: 'https://api.example.com',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await server.init();
|
|
111
|
+
|
|
112
|
+
app.use('/api', authrimMiddleware(server));
|
|
113
|
+
|
|
114
|
+
app.get('/api/protected', (req, res) => {
|
|
115
|
+
res.json({ user: req.auth.claims.sub });
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Fastify
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import Fastify from 'fastify';
|
|
123
|
+
import { AuthrimServer } from '@authrim/server';
|
|
124
|
+
import { authrimPreHandler } from '@authrim/server/adapters/fastify';
|
|
125
|
+
|
|
126
|
+
const fastify = Fastify();
|
|
127
|
+
const server = new AuthrimServer({
|
|
128
|
+
issuer: 'https://auth.example.com',
|
|
129
|
+
audience: 'https://api.example.com',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await server.init();
|
|
133
|
+
|
|
134
|
+
fastify.addHook('preHandler', authrimPreHandler(server));
|
|
135
|
+
|
|
136
|
+
fastify.get('/api/protected', async (request) => {
|
|
137
|
+
return { user: request.auth.claims.sub };
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Hono
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { Hono } from 'hono';
|
|
145
|
+
import { AuthrimServer } from '@authrim/server';
|
|
146
|
+
import { authrimMiddleware, getAuth } from '@authrim/server/adapters/hono';
|
|
147
|
+
|
|
148
|
+
const app = new Hono();
|
|
149
|
+
const server = new AuthrimServer({
|
|
150
|
+
issuer: 'https://auth.example.com',
|
|
151
|
+
audience: 'https://api.example.com',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await server.init();
|
|
155
|
+
|
|
156
|
+
app.use('/api/*', authrimMiddleware(server));
|
|
157
|
+
|
|
158
|
+
app.get('/api/protected', (c) => {
|
|
159
|
+
const auth = getAuth(c);
|
|
160
|
+
return c.json({ user: auth?.claims.sub });
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Koa
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import Koa from 'koa';
|
|
168
|
+
import { AuthrimServer } from '@authrim/server';
|
|
169
|
+
import { authrimMiddleware } from '@authrim/server/adapters/koa';
|
|
170
|
+
|
|
171
|
+
const app = new Koa();
|
|
172
|
+
const server = new AuthrimServer({
|
|
173
|
+
issuer: 'https://auth.example.com',
|
|
174
|
+
audience: 'https://api.example.com',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await server.init();
|
|
178
|
+
|
|
179
|
+
app.use(authrimMiddleware(server));
|
|
180
|
+
|
|
181
|
+
app.use((ctx) => {
|
|
182
|
+
ctx.body = { user: ctx.state.auth?.claims.sub };
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### NestJS
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
190
|
+
import { HttpException } from '@nestjs/common';
|
|
191
|
+
import { AuthrimServer } from '@authrim/server';
|
|
192
|
+
import { createAuthrimGuard, Auth } from '@authrim/server/adapters/nestjs';
|
|
193
|
+
import type { ValidatedToken } from '@authrim/server';
|
|
194
|
+
|
|
195
|
+
const server = new AuthrimServer({
|
|
196
|
+
issuer: 'https://auth.example.com',
|
|
197
|
+
audience: 'https://api.example.com',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const AuthrimGuard = createAuthrimGuard(server, HttpException);
|
|
201
|
+
|
|
202
|
+
@Controller('api')
|
|
203
|
+
export class AppController {
|
|
204
|
+
@Get('protected')
|
|
205
|
+
@UseGuards(AuthrimGuard)
|
|
206
|
+
getProtected(@Auth() auth: ValidatedToken) {
|
|
207
|
+
return { user: auth.claims.sub };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## DPoP Support
|
|
213
|
+
|
|
214
|
+
DPoP (Demonstrating Proof of Possession) binds access tokens to a specific client key pair.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { AuthrimServer, DPoPValidator } from '@authrim/server';
|
|
218
|
+
|
|
219
|
+
const server = new AuthrimServer({
|
|
220
|
+
issuer: 'https://auth.example.com',
|
|
221
|
+
audience: 'https://api.example.com',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await server.init();
|
|
225
|
+
|
|
226
|
+
// 1. Validate the access token
|
|
227
|
+
const tokenResult = await server.validateToken(accessToken);
|
|
228
|
+
if (tokenResult.error) {
|
|
229
|
+
// Handle error
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 2. Check if token is DPoP-bound
|
|
233
|
+
if (tokenResult.data.tokenType === 'DPoP') {
|
|
234
|
+
// 3. Validate DPoP proof
|
|
235
|
+
const dpopResult = await server.validateDPoP(dpopProof, {
|
|
236
|
+
method: 'GET',
|
|
237
|
+
uri: 'https://api.example.com/resource',
|
|
238
|
+
accessToken: accessToken,
|
|
239
|
+
expectedThumbprint: tokenResult.data.claims.cnf?.jkt,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!dpopResult.valid) {
|
|
243
|
+
// DPoP proof invalid
|
|
244
|
+
return { error: dpopResult.errorCode };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### DPoP with Nonce
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const dpopResult = await server.validateDPoP(dpopProof, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
uri: 'https://api.example.com/token',
|
|
255
|
+
expectedNonce: serverProvidedNonce,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (dpopResult.errorCode === 'dpop_nonce_required') {
|
|
259
|
+
// Client should retry with the nonce
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Token Introspection
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
const server = new AuthrimServer({
|
|
267
|
+
issuer: 'https://auth.example.com',
|
|
268
|
+
audience: 'https://api.example.com',
|
|
269
|
+
introspectionEndpoint: 'https://auth.example.com/oauth/introspect',
|
|
270
|
+
clientCredentials: {
|
|
271
|
+
clientId: 'resource-server',
|
|
272
|
+
clientSecret: 'secret',
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await server.init();
|
|
277
|
+
|
|
278
|
+
const result = await server.introspect(token);
|
|
279
|
+
if (result.active) {
|
|
280
|
+
console.log('Token is active');
|
|
281
|
+
console.log('Subject:', result.sub);
|
|
282
|
+
console.log('Scope:', result.scope);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Token Revocation
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const server = new AuthrimServer({
|
|
290
|
+
issuer: 'https://auth.example.com',
|
|
291
|
+
audience: 'https://api.example.com',
|
|
292
|
+
revocationEndpoint: 'https://auth.example.com/oauth/revoke',
|
|
293
|
+
clientCredentials: {
|
|
294
|
+
clientId: 'resource-server',
|
|
295
|
+
clientSecret: 'secret',
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await server.init();
|
|
300
|
+
|
|
301
|
+
// Revoke access token
|
|
302
|
+
await server.revoke(accessToken);
|
|
303
|
+
|
|
304
|
+
// Revoke refresh token
|
|
305
|
+
await server.revoke(refreshToken, 'refresh_token');
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Back-Channel Logout
|
|
309
|
+
|
|
310
|
+
Handle logout tokens sent by the Authorization Server via back-channel.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { BackChannelLogoutValidator } from '@authrim/server';
|
|
314
|
+
|
|
315
|
+
const validator = new BackChannelLogoutValidator();
|
|
316
|
+
|
|
317
|
+
// Endpoint to receive logout tokens
|
|
318
|
+
app.post('/backchannel-logout', async (req, res) => {
|
|
319
|
+
const logoutToken = req.body.logout_token;
|
|
320
|
+
|
|
321
|
+
// 1. Validate logout token claims (synchronous)
|
|
322
|
+
const result = validator.validate(logoutToken, {
|
|
323
|
+
issuer: 'https://auth.example.com',
|
|
324
|
+
audience: 'https://api.example.com',
|
|
325
|
+
clockToleranceSeconds: 60,
|
|
326
|
+
now: Math.floor(Date.now() / 1000),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (!result.valid) {
|
|
330
|
+
return res.status(400).json({ error: result.error.code });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 2. Verify signature using JWKS (application responsibility)
|
|
334
|
+
// 3. Check jti for replay (application responsibility)
|
|
335
|
+
|
|
336
|
+
// 4. Terminate sessions
|
|
337
|
+
const { sub, sid } = result.claims;
|
|
338
|
+
if (sid) {
|
|
339
|
+
await terminateSession(sid);
|
|
340
|
+
} else if (sub) {
|
|
341
|
+
await terminateAllUserSessions(sub);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
res.status(200).end();
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## SCIM 2.0 Provisioning
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { ScimClient } from '@authrim/server';
|
|
352
|
+
|
|
353
|
+
const scim = new ScimClient({
|
|
354
|
+
baseUrl: 'https://api.example.com/scim/v2',
|
|
355
|
+
accessToken: token,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Create user
|
|
359
|
+
const user = await scim.createUser({
|
|
360
|
+
userName: 'john@example.com',
|
|
361
|
+
name: { givenName: 'John', familyName: 'Doe' },
|
|
362
|
+
emails: [{ value: 'john@example.com', primary: true }],
|
|
363
|
+
active: true,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Get user
|
|
367
|
+
const fetchedUser = await scim.getUser(user.id);
|
|
368
|
+
|
|
369
|
+
// Update user
|
|
370
|
+
await scim.updateUser(user.id, {
|
|
371
|
+
...user,
|
|
372
|
+
active: false,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// List users with filter
|
|
376
|
+
const users = await scim.listUsers({
|
|
377
|
+
filter: 'userName eq "john@example.com"',
|
|
378
|
+
startIndex: 1,
|
|
379
|
+
count: 10,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Delete user
|
|
383
|
+
await scim.deleteUser(user.id);
|
|
384
|
+
|
|
385
|
+
// Group operations
|
|
386
|
+
const group = await scim.createGroup({
|
|
387
|
+
displayName: 'Admins',
|
|
388
|
+
members: [{ value: user.id }],
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Error Handling
|
|
393
|
+
|
|
394
|
+
All validation methods return a result object with `data` or `error`:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
const result = await server.validateToken(token);
|
|
398
|
+
|
|
399
|
+
if (result.error) {
|
|
400
|
+
// Handle specific error codes
|
|
401
|
+
switch (result.error.code) {
|
|
402
|
+
case 'token_expired':
|
|
403
|
+
// Token has expired
|
|
404
|
+
break;
|
|
405
|
+
case 'invalid_issuer':
|
|
406
|
+
// Wrong issuer
|
|
407
|
+
break;
|
|
408
|
+
case 'invalid_audience':
|
|
409
|
+
// Wrong audience
|
|
410
|
+
break;
|
|
411
|
+
case 'signature_invalid':
|
|
412
|
+
// Signature verification failed
|
|
413
|
+
break;
|
|
414
|
+
case 'jwks_key_not_found':
|
|
415
|
+
// Key not found in JWKS
|
|
416
|
+
break;
|
|
417
|
+
default:
|
|
418
|
+
// Other error
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// HTTP status for response
|
|
422
|
+
const httpStatus = result.error.httpStatus; // 401, 503, etc.
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Error Codes
|
|
427
|
+
|
|
428
|
+
| Code | Description | HTTP Status |
|
|
429
|
+
|------|-------------|-------------|
|
|
430
|
+
| `token_malformed` | Token is not a valid JWT | 401 |
|
|
431
|
+
| `token_expired` | Token has expired | 401 |
|
|
432
|
+
| `invalid_issuer` | Issuer doesn't match expected | 401 |
|
|
433
|
+
| `invalid_audience` | Audience doesn't match expected | 401 |
|
|
434
|
+
| `signature_invalid` | Signature verification failed | 401 |
|
|
435
|
+
| `jwks_key_not_found` | Signing key not in JWKS | 401 |
|
|
436
|
+
| `jwks_fetch_failed` | Failed to fetch JWKS | 503 |
|
|
437
|
+
| `dpop_proof_missing` | DPoP proof required but missing | 401 |
|
|
438
|
+
| `dpop_proof_invalid` | DPoP proof validation failed | 401 |
|
|
439
|
+
| `dpop_nonce_required` | Server nonce required | 401 |
|
|
440
|
+
|
|
441
|
+
## Provider Injection
|
|
442
|
+
|
|
443
|
+
All dependencies are injectable for testing and customization:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { AuthrimServer } from '@authrim/server';
|
|
447
|
+
import {
|
|
448
|
+
fetchHttpProvider,
|
|
449
|
+
webCryptoProvider,
|
|
450
|
+
systemClock,
|
|
451
|
+
memoryCache
|
|
452
|
+
} from '@authrim/server/providers';
|
|
453
|
+
|
|
454
|
+
const server = new AuthrimServer({
|
|
455
|
+
issuer: 'https://auth.example.com',
|
|
456
|
+
audience: 'https://api.example.com',
|
|
457
|
+
|
|
458
|
+
// Custom providers
|
|
459
|
+
http: fetchHttpProvider(),
|
|
460
|
+
crypto: webCryptoProvider(),
|
|
461
|
+
clock: systemClock(),
|
|
462
|
+
jwksCache: memoryCache({ ttlMs: 3600_000 }),
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Testing with Mocks
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { vi } from 'vitest';
|
|
470
|
+
|
|
471
|
+
const mockClock = {
|
|
472
|
+
nowMs: () => 1700000000000,
|
|
473
|
+
nowSeconds: () => 1700000000,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const mockCrypto = {
|
|
477
|
+
verifySignature: vi.fn().mockResolvedValue(true),
|
|
478
|
+
importJwk: vi.fn().mockResolvedValue({} as CryptoKey),
|
|
479
|
+
sha256: vi.fn().mockResolvedValue(new Uint8Array(32)),
|
|
480
|
+
calculateThumbprint: vi.fn().mockResolvedValue('thumbprint'),
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const server = new AuthrimServer({
|
|
484
|
+
issuer: 'https://auth.example.com',
|
|
485
|
+
audience: 'https://api.example.com',
|
|
486
|
+
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
487
|
+
crypto: mockCrypto,
|
|
488
|
+
clock: mockClock,
|
|
489
|
+
});
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Security Considerations
|
|
493
|
+
|
|
494
|
+
### HTTPS Enforcement
|
|
495
|
+
|
|
496
|
+
By default, all endpoints must use HTTPS. Disable only for development:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
const server = new AuthrimServer({
|
|
500
|
+
issuer: 'http://localhost:8080', // Will throw error
|
|
501
|
+
requireHttps: false, // Allow HTTP (development only!)
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Timing-Safe Comparisons
|
|
506
|
+
|
|
507
|
+
This SDK uses constant-time string comparisons for security-sensitive values:
|
|
508
|
+
- JWT issuer and audience validation
|
|
509
|
+
- DPoP thumbprint binding verification
|
|
510
|
+
- Back-channel logout subject and session ID validation
|
|
511
|
+
|
|
512
|
+
### JTI Replay Protection
|
|
513
|
+
|
|
514
|
+
For DPoP proofs and logout tokens, `jti` (JWT ID) uniqueness checking is the **application's responsibility**. Implement a cache with TTL:
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
const jtiCache = new Map<string, number>();
|
|
518
|
+
const JTI_LIFETIME_SECONDS = 120;
|
|
519
|
+
|
|
520
|
+
function checkJtiUniqueness(jti: string): boolean {
|
|
521
|
+
const now = Math.floor(Date.now() / 1000);
|
|
522
|
+
|
|
523
|
+
// Clean expired entries
|
|
524
|
+
for (const [key, exp] of jtiCache) {
|
|
525
|
+
if (exp < now) jtiCache.delete(key);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (jtiCache.has(jti)) {
|
|
529
|
+
return false; // Replay detected
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
jtiCache.set(jti, now + JTI_LIFETIME_SECONDS);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### JWKS Security
|
|
538
|
+
|
|
539
|
+
- Cross-origin redirects are blocked by default
|
|
540
|
+
- Cache-Control headers are respected (max 24 hours)
|
|
541
|
+
- Single-flight pattern prevents thundering herd
|
|
542
|
+
|
|
543
|
+
## API Reference
|
|
544
|
+
|
|
545
|
+
### AuthrimServer
|
|
546
|
+
|
|
547
|
+
| Method | Description |
|
|
548
|
+
|--------|-------------|
|
|
549
|
+
| `init()` | Initialize the server (fetch JWKS if needed) |
|
|
550
|
+
| `validateToken(token)` | Validate a JWT access token |
|
|
551
|
+
| `validateDPoP(proof, options)` | Validate a DPoP proof |
|
|
552
|
+
| `introspect(token)` | Introspect a token (RFC 7662) |
|
|
553
|
+
| `revoke(token, tokenTypeHint?)` | Revoke a token (RFC 7009) |
|
|
554
|
+
| `invalidateJwksCache()` | Force JWKS cache refresh |
|
|
555
|
+
|
|
556
|
+
### BackChannelLogoutValidator
|
|
557
|
+
|
|
558
|
+
| Method | Description |
|
|
559
|
+
|--------|-------------|
|
|
560
|
+
| `validate(token, options)` | Validate logout token claims |
|
|
561
|
+
|
|
562
|
+
### ScimClient
|
|
563
|
+
|
|
564
|
+
| Method | Description |
|
|
565
|
+
|--------|-------------|
|
|
566
|
+
| `createUser(user)` | Create a new user |
|
|
567
|
+
| `getUser(id)` | Get user by ID |
|
|
568
|
+
| `updateUser(id, user)` | Replace user |
|
|
569
|
+
| `patchUser(id, operations)` | Patch user |
|
|
570
|
+
| `deleteUser(id)` | Delete user |
|
|
571
|
+
| `listUsers(options?)` | List/search users |
|
|
572
|
+
| `createGroup(group)` | Create a new group |
|
|
573
|
+
| `getGroup(id)` | Get group by ID |
|
|
574
|
+
| `updateGroup(id, group)` | Replace group |
|
|
575
|
+
| `patchGroup(id, operations)` | Patch group |
|
|
576
|
+
| `deleteGroup(id)` | Delete group |
|
|
577
|
+
| `listGroups(options?)` | List/search groups |
|
|
578
|
+
|
|
579
|
+
## RFC Compliance
|
|
580
|
+
|
|
581
|
+
| Specification | Status |
|
|
582
|
+
|---------------|--------|
|
|
583
|
+
| RFC 7519 - JWT | Implemented |
|
|
584
|
+
| RFC 7517 - JWK | Implemented |
|
|
585
|
+
| RFC 7638 - JWK Thumbprint | Implemented |
|
|
586
|
+
| RFC 7662 - Token Introspection | Implemented |
|
|
587
|
+
| RFC 7009 - Token Revocation | Implemented |
|
|
588
|
+
| RFC 9449 - DPoP | Implemented |
|
|
589
|
+
| RFC 7643/7644 - SCIM 2.0 | Implemented |
|
|
590
|
+
| OIDC Core 1.0 | Implemented |
|
|
591
|
+
| OIDC Back-Channel Logout 1.0 | Implemented |
|
|
592
|
+
|
|
593
|
+
## Authrim SDK Family
|
|
594
|
+
|
|
595
|
+
This SDK is part of the Authrim identity platform:
|
|
596
|
+
|
|
597
|
+
| Package | Description |
|
|
598
|
+
|---------|-------------|
|
|
599
|
+
| `@authrim/core` | Client-side SDK for OAuth 2.0 / OIDC flows |
|
|
600
|
+
| `@authrim/server` | Server-side SDK for token validation (this package) |
|
|
601
|
+
|
|
602
|
+
## jose Dependency
|
|
603
|
+
|
|
604
|
+
This SDK uses `jose` for type definitions. Cryptographic operations use the native Web Crypto API. Other Authrim language SDKs may use different libraries (e.g., Nimbus for Java, go-jose for Go, Microsoft.IdentityModel for .NET) as long as the same verification steps are followed.
|
|
605
|
+
|
|
606
|
+
## License
|
|
607
|
+
|
|
608
|
+
Apache-2.0
|
|
609
|
+
|
|
610
|
+
Copyright (c) Authrim
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
'use strict';var chunkO2ALCNXB_cjs=require('../chunk-O2ALCNXB.cjs'),chunkTPROSFE7_cjs=require('../chunk-TPROSFE7.cjs');function x(n,s={}){return async(e,d,o)=>{let u=e.get("host")??"localhost",i=`${e.protocol}://${u}${e.originalUrl}`,r=await chunkTPROSFE7_cjs.c(n,{headers:e.headers,method:e.method,url:i});if(r.error){let t=new chunkTPROSFE7_cjs.a(r.error.code,r.error.message),c=chunkO2ALCNXB_cjs.c(t,{realm:s.realm,scheme:"Bearer"});s.onError&&s.onError(r.error),d.status(r.error.httpStatus).set(c).json(chunkO2ALCNXB_cjs.a(t));return}e.auth=r.data.claims,e.authTokenType=r.data.tokenType,o();}}function E(n,s={}){return async(e,d,o)=>{if(!e.headers.authorization){o();return}let i=e.get("host")??"localhost",r=`${e.protocol}://${i}${e.originalUrl}`,t=await chunkTPROSFE7_cjs.c(n,{headers:e.headers,method:e.method,url:r});t.data&&(e.auth=t.data.claims,e.authTokenType=t.data.tokenType),o();}}
|
|
2
|
+
exports.authrimMiddleware=x;exports.authrimOptionalMiddleware=E;//# sourceMappingURL=express.cjs.map
|
|
3
|
+
//# sourceMappingURL=express.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/express.ts"],"names":["authrimMiddleware","server","options","req","res","next","host","url","result","authenticateRequest","error","AuthrimServerError","headers","buildErrorHeaders","buildErrorResponse","authrimOptionalMiddleware","_options","_res"],"mappings":"uHAwEO,SAASA,EACdC,CAAAA,CACAC,CAAAA,CAA6B,EAAC,CAC9B,CACA,OAAO,MACLC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,GACkB,CAElB,IAAMC,CAAAA,CAAOH,EAAI,GAAA,CAAI,MAAM,GAAK,WAAA,CAC1BI,CAAAA,CAAM,CAAA,EAAGJ,CAAAA,CAAI,QAAQ,CAAA,GAAA,EAAMG,CAAI,GAAGH,CAAAA,CAAI,WAAW,GAGjDK,CAAAA,CAAS,MAAMC,oBAAoBR,CAAAA,CAAQ,CAC/C,QAASE,CAAAA,CAAI,OAAA,CACb,OAAQA,CAAAA,CAAI,MAAA,CACZ,IAAAI,CACF,CAAC,CAAA,CAED,GAAIC,EAAO,KAAA,CAAO,CAChB,IAAME,CAAAA,CAAQ,IAAIC,oBAChBH,CAAAA,CAAO,KAAA,CAAM,IAAA,CACbA,CAAAA,CAAO,MAAM,OACf,CAAA,CAEMI,EAAUC,mBAAAA,CAAkBH,CAAAA,CAAO,CACvC,KAAA,CAAOR,CAAAA,CAAQ,KAAA,CACf,MAAA,CAAQ,QACV,CAAC,CAAA,CAEGA,EAAQ,OAAA,EACVA,CAAAA,CAAQ,QAAQM,CAAAA,CAAO,KAAK,EAG9BJ,CAAAA,CAAI,MAAA,CAAOI,EAAO,KAAA,CAAM,UAAU,EAAE,GAAA,CAAII,CAAO,EAAE,IAAA,CAAKE,mBAAAA,CAAmBJ,CAAK,CAAC,EAC/E,MACF,CAGAP,EAAI,IAAA,CAAOK,CAAAA,CAAO,KAAK,MAAA,CACvBL,CAAAA,CAAI,aAAA,CAAgBK,CAAAA,CAAO,KAAK,SAAA,CAEhCH,CAAAA,GACF,CACF,CASO,SAASU,CAAAA,CACdd,CAAAA,CACAe,CAAAA,CAA8B,GAC9B,CACA,aACEb,CAAAA,CACAc,CAAAA,CACAZ,IACkB,CAGlB,GAAI,CADeF,CAAAA,CAAI,OAAA,CAAQ,cACd,CACfE,CAAAA,GACA,MACF,CAGA,IAAMC,CAAAA,CAAOH,CAAAA,CAAI,GAAA,CAAI,MAAM,GAAK,WAAA,CAC1BI,CAAAA,CAAM,GAAGJ,CAAAA,CAAI,QAAQ,MAAMG,CAAI,CAAA,EAAGH,CAAAA,CAAI,WAAW,GAGjDK,CAAAA,CAAS,MAAMC,oBAAoBR,CAAAA,CAAQ,CAC/C,QAASE,CAAAA,CAAI,OAAA,CACb,MAAA,CAAQA,CAAAA,CAAI,OACZ,GAAA,CAAAI,CACF,CAAC,CAAA,CAEGC,CAAAA,CAAO,OACTL,CAAAA,CAAI,IAAA,CAAOK,EAAO,IAAA,CAAK,MAAA,CACvBL,EAAI,aAAA,CAAgBK,CAAAA,CAAO,KAAK,SAAA,CAAA,CAGlCH,CAAAA,GACF,CACF","file":"express.cjs","sourcesContent":["/**\r\n * Express Adapter\r\n *\r\n * Thin wrapper around authenticateRequest for Express framework.\r\n */\r\n\r\nimport type { AuthrimServer } from '../core/client.js';\r\nimport type { MiddlewareOptions } from '../middleware/types.js';\r\nimport type { ValidatedToken } from '../types/claims.js';\r\nimport { authenticateRequest } from '../middleware/authenticate.js';\r\nimport { buildErrorResponse, buildErrorHeaders } from '../utils/error-response.js';\r\nimport { AuthrimServerError } from '../types/errors.js';\r\n\r\n/**\r\n * Extended Express Request with auth property\r\n */\r\nexport interface AuthrimExpressRequest {\r\n auth?: ValidatedToken;\r\n authTokenType?: 'Bearer' | 'DPoP';\r\n}\r\n\r\n/**\r\n * Express request type (minimal interface)\r\n */\r\ninterface ExpressRequest {\r\n headers: Record<string, string | string[] | undefined>;\r\n method: string;\r\n protocol: string;\r\n get(name: string): string | undefined;\r\n originalUrl: string;\r\n}\r\n\r\n/**\r\n * Express response type (minimal interface)\r\n */\r\ninterface ExpressResponse {\r\n status(code: number): ExpressResponse;\r\n set(headers: Record<string, string>): ExpressResponse;\r\n json(body: unknown): void;\r\n}\r\n\r\n/**\r\n * Express next function\r\n */\r\ntype ExpressNextFunction = (err?: unknown) => void;\r\n\r\n/**\r\n * Create Express middleware for token validation\r\n *\r\n * @param server - AuthrimServer instance\r\n * @param options - Middleware options\r\n * @returns Express middleware function\r\n *\r\n * @example\r\n * ```typescript\r\n * import express from 'express';\r\n * import { createAuthrimServer } from '@authrim/server';\r\n * import { authrimMiddleware } from '@authrim/server/adapters/express';\r\n *\r\n * const app = express();\r\n * const server = createAuthrimServer({\r\n * issuer: 'https://auth.example.com',\r\n * audience: 'https://api.example.com',\r\n * });\r\n *\r\n * app.use('/api', authrimMiddleware(server));\r\n *\r\n * app.get('/api/protected', (req, res) => {\r\n * res.json({ user: req.auth.claims.sub });\r\n * });\r\n * ```\r\n */\r\nexport function authrimMiddleware(\r\n server: AuthrimServer,\r\n options: MiddlewareOptions = {}\r\n) {\r\n return async (\r\n req: ExpressRequest & AuthrimExpressRequest,\r\n res: ExpressResponse,\r\n next: ExpressNextFunction\r\n ): Promise<void> => {\r\n // Build URL\r\n const host = req.get('host') ?? 'localhost';\r\n const url = `${req.protocol}://${host}${req.originalUrl}`;\r\n\r\n // Authenticate\r\n const result = await authenticateRequest(server, {\r\n headers: req.headers as Record<string, string | string[] | undefined>,\r\n method: req.method,\r\n url,\r\n });\r\n\r\n if (result.error) {\r\n const error = new AuthrimServerError(\r\n result.error.code as any,\r\n result.error.message\r\n );\r\n\r\n const headers = buildErrorHeaders(error, {\r\n realm: options.realm,\r\n scheme: 'Bearer',\r\n });\r\n\r\n if (options.onError) {\r\n options.onError(result.error);\r\n }\r\n\r\n res.status(result.error.httpStatus).set(headers).json(buildErrorResponse(error));\r\n return;\r\n }\r\n\r\n // Attach auth to request\r\n req.auth = result.data.claims;\r\n req.authTokenType = result.data.tokenType;\r\n\r\n next();\r\n };\r\n}\r\n\r\n/**\r\n * Create optional auth middleware (doesn't fail if no token)\r\n *\r\n * @param server - AuthrimServer instance\r\n * @param options - Middleware options\r\n * @returns Express middleware function\r\n */\r\nexport function authrimOptionalMiddleware(\r\n server: AuthrimServer,\r\n _options: MiddlewareOptions = {}\r\n) {\r\n return async (\r\n req: ExpressRequest & AuthrimExpressRequest,\r\n _res: ExpressResponse,\r\n next: ExpressNextFunction\r\n ): Promise<void> => {\r\n // Check if Authorization header exists\r\n const authHeader = req.headers['authorization'];\r\n if (!authHeader) {\r\n next();\r\n return;\r\n }\r\n\r\n // Build URL\r\n const host = req.get('host') ?? 'localhost';\r\n const url = `${req.protocol}://${host}${req.originalUrl}`;\r\n\r\n // Authenticate\r\n const result = await authenticateRequest(server, {\r\n headers: req.headers as Record<string, string | string[] | undefined>,\r\n method: req.method,\r\n url,\r\n });\r\n\r\n if (result.data) {\r\n req.auth = result.data.claims;\r\n req.authTokenType = result.data.tokenType;\r\n }\r\n\r\n next();\r\n };\r\n}\r\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { V as ValidatedToken, A as AuthrimServer, M as MiddlewareOptions } from '../types-CzpMdWFR.cjs';
|
|
2
|
+
import '../config-I0GIVJA_.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Express Adapter
|
|
6
|
+
*
|
|
7
|
+
* Thin wrapper around authenticateRequest for Express framework.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extended Express Request with auth property
|
|
12
|
+
*/
|
|
13
|
+
interface AuthrimExpressRequest {
|
|
14
|
+
auth?: ValidatedToken;
|
|
15
|
+
authTokenType?: 'Bearer' | 'DPoP';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Express request type (minimal interface)
|
|
19
|
+
*/
|
|
20
|
+
interface ExpressRequest {
|
|
21
|
+
headers: Record<string, string | string[] | undefined>;
|
|
22
|
+
method: string;
|
|
23
|
+
protocol: string;
|
|
24
|
+
get(name: string): string | undefined;
|
|
25
|
+
originalUrl: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Express response type (minimal interface)
|
|
29
|
+
*/
|
|
30
|
+
interface ExpressResponse {
|
|
31
|
+
status(code: number): ExpressResponse;
|
|
32
|
+
set(headers: Record<string, string>): ExpressResponse;
|
|
33
|
+
json(body: unknown): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Express next function
|
|
37
|
+
*/
|
|
38
|
+
type ExpressNextFunction = (err?: unknown) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Create Express middleware for token validation
|
|
41
|
+
*
|
|
42
|
+
* @param server - AuthrimServer instance
|
|
43
|
+
* @param options - Middleware options
|
|
44
|
+
* @returns Express middleware function
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* import express from 'express';
|
|
49
|
+
* import { createAuthrimServer } from '@authrim/server';
|
|
50
|
+
* import { authrimMiddleware } from '@authrim/server/adapters/express';
|
|
51
|
+
*
|
|
52
|
+
* const app = express();
|
|
53
|
+
* const server = createAuthrimServer({
|
|
54
|
+
* issuer: 'https://auth.example.com',
|
|
55
|
+
* audience: 'https://api.example.com',
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* app.use('/api', authrimMiddleware(server));
|
|
59
|
+
*
|
|
60
|
+
* app.get('/api/protected', (req, res) => {
|
|
61
|
+
* res.json({ user: req.auth.claims.sub });
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function authrimMiddleware(server: AuthrimServer, options?: MiddlewareOptions): (req: ExpressRequest & AuthrimExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Create optional auth middleware (doesn't fail if no token)
|
|
68
|
+
*
|
|
69
|
+
* @param server - AuthrimServer instance
|
|
70
|
+
* @param options - Middleware options
|
|
71
|
+
* @returns Express middleware function
|
|
72
|
+
*/
|
|
73
|
+
declare function authrimOptionalMiddleware(server: AuthrimServer, _options?: MiddlewareOptions): (req: ExpressRequest & AuthrimExpressRequest, _res: ExpressResponse, next: ExpressNextFunction) => Promise<void>;
|
|
74
|
+
|
|
75
|
+
export { type AuthrimExpressRequest, authrimMiddleware, authrimOptionalMiddleware };
|