@carisls/sso-standard 1.1.5 → 1.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/.gitlab-ci.yml +6 -3
- package/README.md +351 -0
- package/cjs/authorize.cjs +7 -9
- package/cjs/index.cjs +15 -10
- package/cjs/shared/cookies/cookieParser.cjs +1 -1
- package/cjs/standard/index.cjs +5 -2
- package/cjs/standard/refreshRequest.cjs +3 -3
- package/esm/authorize.js +7 -9
- package/esm/index.js +16 -12
- package/esm/shared/cookies/cookieParser.js +1 -1
- package/esm/standard/index.js +5 -2
- package/esm/standard/refreshRequest.js +3 -3
- package/package.json +6 -6
package/.gitlab-ci.yml
CHANGED
|
@@ -3,9 +3,12 @@ stages:
|
|
|
3
3
|
|
|
4
4
|
publish:
|
|
5
5
|
stage: publish
|
|
6
|
-
image: node:
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
image: node:24-alpine
|
|
7
|
+
id_tokens:
|
|
8
|
+
NPM_ID_TOKEN:
|
|
9
|
+
aud: "npm:registry.npmjs.org"
|
|
10
|
+
SIGSTORE_ID_TOKEN:
|
|
11
|
+
aud: sigstore
|
|
9
12
|
script:
|
|
10
13
|
- npm i
|
|
11
14
|
- npm run build --if-present
|
package/README.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# @carisls/sso-standard
|
|
2
|
+
|
|
3
|
+
A middleware implementing standard OIDC/OAuth2 authorization code flow SSO for Express.js applications. This package provides a complete, ready-to-use authentication solution with automatic token management, session handling, and user mapping.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 Complete OIDC/OAuth2 authorization code flow implementation
|
|
8
|
+
- 🍪 Automatic cookie-based session management with encryption
|
|
9
|
+
- 🔄 Automatic token refresh handling
|
|
10
|
+
- 👤 User token mapping and request injection
|
|
11
|
+
- 🛡️ Built-in authorization middleware
|
|
12
|
+
- 🌐 Multi-provider support (Keycloak, Okta, and other OIDC providers)
|
|
13
|
+
- 🔑 JWKS-based public key validation with caching
|
|
14
|
+
- 📦 Supports both ESM and CommonJS
|
|
15
|
+
- 🎯 Customizable endpoints and paths
|
|
16
|
+
- 🔒 Secure cookie handling with encryption
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @carisls/sso-standard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### ESM (ECMAScript Modules)
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
import express from 'express';
|
|
30
|
+
import { router } from '@carisls/sso-standard';
|
|
31
|
+
|
|
32
|
+
const app = express();
|
|
33
|
+
|
|
34
|
+
app.use(router({
|
|
35
|
+
clientId: 'your-client-id',
|
|
36
|
+
clientSecret: 'your-client-secret',
|
|
37
|
+
// Simple format: comma-separated string
|
|
38
|
+
providers: 'https://auth.example.com',
|
|
39
|
+
// Or array format: providers: [{ ssoUrl: 'https://auth.example.com' }],
|
|
40
|
+
encPassword: 'your-encryption-password'
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
app.listen(3000);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### CommonJS
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
const express = require('express');
|
|
50
|
+
const { router } = require('@carisls/sso-standard');
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
|
|
54
|
+
app.use(router({
|
|
55
|
+
clientId: 'your-client-id',
|
|
56
|
+
clientSecret: 'your-client-secret',
|
|
57
|
+
// Simple format: comma-separated string
|
|
58
|
+
providers: 'https://auth.example.com',
|
|
59
|
+
// Or array format: providers: [{ ssoUrl: 'https://auth.example.com' }],
|
|
60
|
+
encPassword: 'your-encryption-password'
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
app.listen(3000);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API Reference
|
|
67
|
+
|
|
68
|
+
### `router(options)`
|
|
69
|
+
|
|
70
|
+
Creates and configures an Express router with complete SSO authentication flow.
|
|
71
|
+
|
|
72
|
+
**Parameters:**
|
|
73
|
+
|
|
74
|
+
- `options` (Object): Configuration object with the following properties:
|
|
75
|
+
- `clientId` (string, **required**): SSO Client ID registered with your identity provider
|
|
76
|
+
- `clientSecret` (string, **required**): SSO Client Secret
|
|
77
|
+
- `providers` (string | Array<Object>, **required**): Provider configuration(s). Can be:
|
|
78
|
+
- A comma-separated string of provider URLs (e.g., `'https://auth1.example.com,https://auth2.example.com'`)
|
|
79
|
+
- An array of provider configuration objects:
|
|
80
|
+
- `ssoUrl` (string): Base URL for the provider (usually equal to `iss`). Used to discover OpenID Connect configuration
|
|
81
|
+
- `iss` (string, optional): Issuer identifier. If not provided, defaults to `ssoUrl`
|
|
82
|
+
- `publicKey` (string, optional): Static public key in PEM format (if not using JWKS)
|
|
83
|
+
- `encPassword` (string, **required**): Password used for encrypting cookies
|
|
84
|
+
- `encPasswordSalt` (string, optional): Salt for password derivation (default: `'C5mp4Hl$X9wby#s5'`)
|
|
85
|
+
- `encIterationCount` (number, optional): Iteration count for key derivation (default: `123123`)
|
|
86
|
+
- `publicKeyCache` (number, optional): Public key caching expiration in seconds (default: `300`)
|
|
87
|
+
- `expOffset` (number, optional): Force renewal of tokens earlier (in seconds) to handle clock skew issues (default: `0`)
|
|
88
|
+
- `groups` (boolean, optional): Request groups scope from the provider (default: `true`)
|
|
89
|
+
- `userMapper` (function, optional): Custom function to map tokens to user object. Signature: `(token, idToken) => userObject`
|
|
90
|
+
- `paths` (Object, optional): Customize default endpoint paths
|
|
91
|
+
- `login` (string, default: `'/login'`): Login initiation endpoint
|
|
92
|
+
- `sso` (string, default: `'/sso'`): SSO callback endpoint
|
|
93
|
+
- `afterLogin` (string, default: `'/'`): Redirect after successful login
|
|
94
|
+
- `logout` (string, default: `'/logout'`): Logout initiation endpoint
|
|
95
|
+
- `afterLogout` (string, default: `'/'`): Redirect after successful logout
|
|
96
|
+
|
|
97
|
+
**Returns:** Express Router instance with the following endpoints configured:
|
|
98
|
+
- `GET /login` (or custom path): Initiates SSO login flow
|
|
99
|
+
- `GET /sso` (or custom path): Handles SSO callback and token exchange
|
|
100
|
+
- `GET /logout` (or custom path): Initiates logout flow
|
|
101
|
+
- Automatic token refresh middleware
|
|
102
|
+
- User injection middleware (adds `req.user` to authenticated requests)
|
|
103
|
+
|
|
104
|
+
**Example:**
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
import express from 'express';
|
|
108
|
+
import { router } from '@carisls/sso-standard';
|
|
109
|
+
|
|
110
|
+
const app = express();
|
|
111
|
+
|
|
112
|
+
app.use(router({
|
|
113
|
+
clientId: 'my-app-client',
|
|
114
|
+
clientSecret: 'my-secret-key',
|
|
115
|
+
// Simple format: comma-separated string
|
|
116
|
+
providers: 'https://auth.example.com',
|
|
117
|
+
// Or detailed format: array of objects
|
|
118
|
+
// providers: [
|
|
119
|
+
// {
|
|
120
|
+
// ssoUrl: 'https://auth.example.com',
|
|
121
|
+
// // iss defaults to ssoUrl if not provided
|
|
122
|
+
// }
|
|
123
|
+
// ],
|
|
124
|
+
encPassword: 'my-encryption-password-123',
|
|
125
|
+
publicKeyCache: 600,
|
|
126
|
+
expOffset: 60,
|
|
127
|
+
groups: true,
|
|
128
|
+
paths: {
|
|
129
|
+
login: '/auth/login',
|
|
130
|
+
sso: '/auth/callback',
|
|
131
|
+
afterLogin: '/dashboard',
|
|
132
|
+
logout: '/auth/logout',
|
|
133
|
+
afterLogout: '/'
|
|
134
|
+
},
|
|
135
|
+
userMapper: (token, idToken) => {
|
|
136
|
+
return {
|
|
137
|
+
id: token.sub,
|
|
138
|
+
email: token.email,
|
|
139
|
+
name: token.name,
|
|
140
|
+
roles: token.roles || []
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
// Protected route - user is automatically available via req.user
|
|
146
|
+
app.get('/dashboard', (req, res) => {
|
|
147
|
+
res.json({
|
|
148
|
+
message: 'Welcome!',
|
|
149
|
+
user: req.user
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
app.listen(3000);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `authorize(role, exceptions, redirectToLogin)`
|
|
157
|
+
|
|
158
|
+
Express.js middleware for authorization filtering based on user roles. This middleware works in conjunction with the router to provide role-based access control.
|
|
159
|
+
|
|
160
|
+
**Parameters:**
|
|
161
|
+
|
|
162
|
+
- `role` (string | string[] | undefined, optional): Role(s) to filter by. If `undefined`, only checks for authenticated user.
|
|
163
|
+
- `exceptions` (string[] | undefined, optional): Array of URL paths to skip authorization checks.
|
|
164
|
+
- `redirectToLogin` (boolean, default: `false`): If `true`, redirects to login page instead of returning 401.
|
|
165
|
+
|
|
166
|
+
**Returns:** Express middleware function `(req, res, next) => void`
|
|
167
|
+
|
|
168
|
+
**Example:**
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
import express from 'express';
|
|
172
|
+
import { router, authorize } from '@carisls/sso-standard';
|
|
173
|
+
|
|
174
|
+
const app = express();
|
|
175
|
+
|
|
176
|
+
// Setup SSO router
|
|
177
|
+
app.use(router({
|
|
178
|
+
clientId: 'my-client-id',
|
|
179
|
+
clientSecret: 'my-secret',
|
|
180
|
+
// Simple format: comma-separated string
|
|
181
|
+
providers: 'https://auth.example.com',
|
|
182
|
+
// Or array format: providers: [{ ssoUrl: 'https://auth.example.com' }],
|
|
183
|
+
encPassword: 'encryption-password'
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
// Require any authenticated user
|
|
187
|
+
app.use(authorize());
|
|
188
|
+
|
|
189
|
+
// Require specific role
|
|
190
|
+
app.get('/admin', authorize('admin'), (req, res) => {
|
|
191
|
+
res.json({ message: 'Admin access granted', user: req.user });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Require one of multiple roles
|
|
195
|
+
app.get('/dashboard', authorize(['admin', 'user']), (req, res) => {
|
|
196
|
+
res.json({ message: 'Dashboard access', user: req.user });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// With exceptions and redirect
|
|
200
|
+
app.use(authorize('user', ['/public', '/health'], true));
|
|
201
|
+
|
|
202
|
+
app.listen(3000);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Complete Example
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
import express from 'express';
|
|
209
|
+
import { router, authorize } from '@carisls/sso-standard';
|
|
210
|
+
|
|
211
|
+
const app = express();
|
|
212
|
+
|
|
213
|
+
// Configure SSO middleware
|
|
214
|
+
app.use(router({
|
|
215
|
+
clientId: process.env.SSO_CLIENT_ID,
|
|
216
|
+
clientSecret: process.env.SSO_CLIENT_SECRET,
|
|
217
|
+
// Simple format: comma-separated string (iss defaults to ssoUrl)
|
|
218
|
+
providers: process.env.SSO_URL,
|
|
219
|
+
// Or array format with custom iss:
|
|
220
|
+
// providers: [
|
|
221
|
+
// {
|
|
222
|
+
// ssoUrl: process.env.SSO_URL,
|
|
223
|
+
// iss: process.env.SSO_ISSUER // optional, defaults to ssoUrl
|
|
224
|
+
// }
|
|
225
|
+
// ],
|
|
226
|
+
encPassword: process.env.ENCRYPTION_PASSWORD,
|
|
227
|
+
publicKeyCache: 600,
|
|
228
|
+
paths: {
|
|
229
|
+
login: '/login',
|
|
230
|
+
sso: '/sso',
|
|
231
|
+
afterLogin: '/dashboard',
|
|
232
|
+
logout: '/logout',
|
|
233
|
+
afterLogout: '/'
|
|
234
|
+
}
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
// Public routes
|
|
238
|
+
app.get('/', (req, res) => {
|
|
239
|
+
res.send('Welcome! <a href="/login">Login</a>');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Protected routes - require authentication
|
|
243
|
+
app.get('/dashboard', authorize(), (req, res) => {
|
|
244
|
+
res.json({
|
|
245
|
+
message: 'Dashboard',
|
|
246
|
+
user: req.user
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Admin routes - require admin role
|
|
251
|
+
app.get('/admin', authorize('admin'), (req, res) => {
|
|
252
|
+
res.json({
|
|
253
|
+
message: 'Admin panel',
|
|
254
|
+
user: req.user
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// API routes with role-based access
|
|
259
|
+
app.get('/api/users', authorize(['admin', 'user-manager']), (req, res) => {
|
|
260
|
+
res.json({ users: [] });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
app.listen(3000, () => {
|
|
264
|
+
console.log('Server running on http://localhost:3000');
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## How It Works
|
|
269
|
+
|
|
270
|
+
1. **Login Flow**: When a user visits `/login`, they are redirected to the identity provider's authorization endpoint with the appropriate OAuth2 parameters.
|
|
271
|
+
|
|
272
|
+
2. **Callback Handling**: After authentication, the provider redirects back to `/sso` with an authorization code. The middleware exchanges this code for access, refresh, and ID tokens.
|
|
273
|
+
|
|
274
|
+
3. **Token Storage**: Tokens are encrypted and stored in HTTP-only cookies:
|
|
275
|
+
- `x-session`: Encrypted access token
|
|
276
|
+
- `x-session-sso`: Encrypted refresh token
|
|
277
|
+
- `x-session-id`: Encrypted ID token (used for logout)
|
|
278
|
+
|
|
279
|
+
4. **User Injection**: On each request, the middleware decrypts tokens, validates them, and injects a `req.user` object containing user information.
|
|
280
|
+
|
|
281
|
+
5. **Token Refresh**: The middleware automatically refreshes access tokens when they expire using the stored refresh token.
|
|
282
|
+
|
|
283
|
+
6. **Logout**: Visiting `/logout` clears all cookies and redirects to the identity provider's logout endpoint.
|
|
284
|
+
|
|
285
|
+
## Supported Providers
|
|
286
|
+
|
|
287
|
+
This package works with any OIDC-compliant identity provider, including:
|
|
288
|
+
|
|
289
|
+
- **Keycloak**
|
|
290
|
+
- **Okta**
|
|
291
|
+
- **Auth0**
|
|
292
|
+
- **Azure AD**
|
|
293
|
+
- **Google Identity Platform**
|
|
294
|
+
- Any other OIDC/OAuth2 provider
|
|
295
|
+
|
|
296
|
+
## User Object Structure
|
|
297
|
+
|
|
298
|
+
By default, the `req.user` object follows this structure (can be customized with `userMapper`):
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
{
|
|
302
|
+
iss: string; // Issuer
|
|
303
|
+
id: string; // User ID (from 'sub')
|
|
304
|
+
sid?: string; // Session ID
|
|
305
|
+
sa: boolean; // Service account flag
|
|
306
|
+
azp?: string; // Authorized party
|
|
307
|
+
name?: { // Name object (if available)
|
|
308
|
+
full: string;
|
|
309
|
+
familyName: string;
|
|
310
|
+
givenName: string;
|
|
311
|
+
};
|
|
312
|
+
email?: string; // Email (if available)
|
|
313
|
+
roles: string[]; // User roles
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Environment Variables
|
|
318
|
+
|
|
319
|
+
For production use, store sensitive values in environment variables:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
SSO_CLIENT_ID=your-client-id
|
|
323
|
+
SSO_CLIENT_SECRET=your-client-secret
|
|
324
|
+
SSO_URL=https://auth.example.com
|
|
325
|
+
SSO_ISSUER=https://auth.example.com
|
|
326
|
+
ENCRYPTION_PASSWORD=your-strong-encryption-password
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Security Considerations
|
|
330
|
+
|
|
331
|
+
- **Cookie Encryption**: All tokens are encrypted before being stored in cookies
|
|
332
|
+
- **HTTP-Only Cookies**: Prevents XSS attacks by making cookies inaccessible to JavaScript
|
|
333
|
+
- **Secure Cookies**: Automatically enabled when using HTTPS
|
|
334
|
+
- **Token Validation**: All tokens are validated using JWKS before use
|
|
335
|
+
- **Automatic Refresh**: Tokens are refreshed before expiration to prevent service interruption
|
|
336
|
+
|
|
337
|
+
## Dependencies
|
|
338
|
+
|
|
339
|
+
- `@carisls/sso-core`: Core SSO utilities (token validation, encryption, etc.)
|
|
340
|
+
- `express`: Web framework
|
|
341
|
+
- `cookie-parser`: Cookie parsing middleware
|
|
342
|
+
- `debug`: Debug logging utility
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
MIT
|
|
347
|
+
|
|
348
|
+
## Author
|
|
349
|
+
|
|
350
|
+
Mihovil Strujic <mstrujic@carisls.com>
|
|
351
|
+
|
package/cjs/authorize.cjs
CHANGED
|
@@ -6,19 +6,17 @@ function checkException (url, exceptions) {
|
|
|
6
6
|
return false;
|
|
7
7
|
|
|
8
8
|
for (let i = 0; i < exceptions.length; i++) {
|
|
9
|
-
if (url.
|
|
10
|
-
return true;
|
|
11
|
-
else if (url.length > exceptions[i].length && url.substr(0, exceptions[i].length) === exceptions[i])
|
|
9
|
+
if (url.startsWith(exceptions[i]))
|
|
12
10
|
return true;
|
|
13
11
|
}
|
|
14
12
|
return false;
|
|
15
13
|
}
|
|
16
14
|
/**
|
|
17
15
|
* @description A helper middleware to perform authorization filtering
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
21
|
-
* @returns
|
|
16
|
+
* @param {string|string[]} role Role (string) or an array of roles to filter by (optional)
|
|
17
|
+
* @param {string[]} exceptions Array of urls to skip
|
|
18
|
+
* @param {boolean} redirectToLogin If user needs to be redirected to login in case of unsuccessful validation
|
|
19
|
+
* @returns {import('express').RequestHandler} Express middleware function
|
|
22
20
|
*/
|
|
23
21
|
|
|
24
22
|
function authorize (role, exceptions, redirectToLogin = false) {
|
|
@@ -26,13 +24,13 @@ function authorize (role, exceptions, redirectToLogin = false) {
|
|
|
26
24
|
// Check if this is login or sso url
|
|
27
25
|
if (req.url.match(/^\/login/i) || req.url.match(/^\/sso/i)) {
|
|
28
26
|
debug(`Url ${req.url} is exception from default authorization rules`);
|
|
29
|
-
next();
|
|
27
|
+
return next();
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
// Apply exceptions
|
|
33
31
|
if (checkException(req.url, exceptions)) {
|
|
34
32
|
debug(`Url ${req.url} is exception from authorization rules`);
|
|
35
|
-
next();
|
|
33
|
+
return next();
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
// If there is no known user, return 401
|
package/cjs/index.cjs
CHANGED
|
@@ -6,14 +6,14 @@ const authorize = require('./authorize.cjs');
|
|
|
6
6
|
/**
|
|
7
7
|
*
|
|
8
8
|
* @param {Object} options Options for this router
|
|
9
|
-
* @param {string} options.ssoUrl Base url for provider (usually equal to iss)
|
|
10
9
|
* @param {string} options.clientId SSO Client ID
|
|
10
|
+
* @param {string} options.clientSecret SSO Client Secret
|
|
11
11
|
* @param {number} options.publicKeyCache Public key caching expiration (in seconds)
|
|
12
12
|
* @param {number} options.expOffset Force nenewal of tokens a bit earlier (in case of problems)
|
|
13
|
-
* @param {Object[]} options.providers
|
|
14
|
-
* @param {
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {
|
|
13
|
+
* @param {string|Object[]} options.providers Provider configuration(s). Can be a comma-separated string of provider URLs or an array of provider objects
|
|
14
|
+
* @param {string} options.providers[].ssoUrl Base url for provider (usually equal to iss). Used to discover OpenID Connect configuration
|
|
15
|
+
* @param {string} [options.providers[].iss] Issuer identifier. If not provided, defaults to ssoUrl
|
|
16
|
+
* @param {string} [options.providers[].publicKey] Static public key in PEM format (if not using JWKS)
|
|
17
17
|
* @param {string} options.encPassword Password to be used for cookies encryption
|
|
18
18
|
* @param {string} options.encPasswordSalt Password salt to be used for cookies encription (optional)
|
|
19
19
|
* @param {string} options.encIterationCount Iteration Count to be used for cookies encription (optional)
|
|
@@ -25,22 +25,27 @@ const authorize = require('./authorize.cjs');
|
|
|
25
25
|
* @param {string} options.paths.logout Change default logout init endpoint from /logout
|
|
26
26
|
* @param {string} options.paths.afterLogout Change default logout final endpoint after a successful logout from /
|
|
27
27
|
* @param {boolean} options.groups To request groups scope (default is true)
|
|
28
|
-
* @returns
|
|
28
|
+
* @returns {import('express').Router} Express router instance
|
|
29
29
|
*/
|
|
30
30
|
const router = (options) => {
|
|
31
31
|
// Check options
|
|
32
32
|
if (!options)
|
|
33
33
|
throw Error('You need to set options parameter');
|
|
34
34
|
|
|
35
|
+
if (!options.providers)
|
|
36
|
+
throw Error('providers is always required');
|
|
37
|
+
|
|
35
38
|
// Extract providers
|
|
36
|
-
const providers = options.providers
|
|
39
|
+
const providers = typeof options.providers === 'string'
|
|
40
|
+
? options.providers.split(',').map(i => ({ ssoUrl: i.trim() }))
|
|
41
|
+
: options.providers;
|
|
42
|
+
|
|
43
|
+
if (!Array.isArray(providers) || providers.length === 0)
|
|
44
|
+
throw Error('providers must be a non-empty array or comma-separated string');
|
|
37
45
|
|
|
38
46
|
// Pick the first provider
|
|
39
47
|
providers.splice(1);
|
|
40
48
|
|
|
41
|
-
if (!options.providers)
|
|
42
|
-
throw Error('providers is always required');
|
|
43
|
-
|
|
44
49
|
if (!options.clientId || !options.clientSecret)
|
|
45
50
|
throw Error('clientId and clientSecret are required');
|
|
46
51
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cookieParserModule = require('cookie-parser');
|
|
2
2
|
function cookieParser (router) {
|
|
3
3
|
router.use((req, res, next) => {
|
|
4
|
-
if (req.
|
|
4
|
+
if (req.cookies)
|
|
5
5
|
next(); // Cookie parser is already attached
|
|
6
6
|
else
|
|
7
7
|
cookieParserModule()(req, res, next); // Cookie parser is needed
|
package/cjs/standard/index.cjs
CHANGED
|
@@ -18,7 +18,10 @@ function index (router, clientId, clientSecret, issuerSelector, encryptor, userM
|
|
|
18
18
|
cookieParserModule(router);
|
|
19
19
|
|
|
20
20
|
// Issuer Selector
|
|
21
|
-
const
|
|
21
|
+
const providerKeys = Object.keys(issuerSelector.providers);
|
|
22
|
+
if (providerKeys.length === 0)
|
|
23
|
+
throw new Error('No providers configured in issuerSelector');
|
|
24
|
+
const tokenValidator = issuerSelector.providers[providerKeys[0]].validator;
|
|
22
25
|
|
|
23
26
|
// Instantiate client
|
|
24
27
|
const client = {
|
|
@@ -26,7 +29,7 @@ function index (router, clientId, clientSecret, issuerSelector, encryptor, userM
|
|
|
26
29
|
client_id: clientId,
|
|
27
30
|
client_secret: clientSecret
|
|
28
31
|
},
|
|
29
|
-
issuer: issuerSelector.providers[
|
|
32
|
+
issuer: issuerSelector.providers[providerKeys[0]].issuer
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
// Instantiate token modules
|
|
@@ -21,9 +21,9 @@ function refreshRequest (router, client, encryptor, accessTokenModule, refreshTo
|
|
|
21
21
|
idTokenModule(res, tokens.id_token, req.protocol === 'https'),
|
|
22
22
|
refreshTokenModule(res, tokens.refresh_token, req.protocol === 'https')
|
|
23
23
|
]))
|
|
24
|
-
.then((
|
|
25
|
-
req.token = token;
|
|
26
|
-
req.idToken = idToken;
|
|
24
|
+
.then((results) => {
|
|
25
|
+
if (results[0]?.token) req.token = results[0].token;
|
|
26
|
+
if (results[1]?.idToken) req.idToken = results[1].idToken;
|
|
27
27
|
})
|
|
28
28
|
.catch((err) => {
|
|
29
29
|
console.error(err);
|
package/esm/authorize.js
CHANGED
|
@@ -6,19 +6,17 @@ function checkException (url, exceptions) {
|
|
|
6
6
|
return false;
|
|
7
7
|
|
|
8
8
|
for (let i = 0; i < exceptions.length; i++) {
|
|
9
|
-
if (url.
|
|
10
|
-
return true;
|
|
11
|
-
else if (url.length > exceptions[i].length && url.substr(0, exceptions[i].length) === exceptions[i])
|
|
9
|
+
if (url.startsWith(exceptions[i]))
|
|
12
10
|
return true;
|
|
13
11
|
}
|
|
14
12
|
return false;
|
|
15
13
|
}
|
|
16
14
|
/**
|
|
17
15
|
* @description A helper middleware to perform authorization filtering
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
21
|
-
* @returns
|
|
16
|
+
* @param {string|string[]} role Role (string) or an array of roles to filter by (optional)
|
|
17
|
+
* @param {string[]} exceptions Array of urls to skip
|
|
18
|
+
* @param {boolean} redirectToLogin If user needs to be redirected to login in case of unsuccessful validation
|
|
19
|
+
* @returns {import('express').RequestHandler} Express middleware function
|
|
22
20
|
*/
|
|
23
21
|
|
|
24
22
|
function authorize (role, exceptions, redirectToLogin = false) {
|
|
@@ -26,13 +24,13 @@ function authorize (role, exceptions, redirectToLogin = false) {
|
|
|
26
24
|
// Check if this is login or sso url
|
|
27
25
|
if (req.url.match(/^\/login/i) || req.url.match(/^\/sso/i)) {
|
|
28
26
|
debug(`Url ${req.url} is exception from default authorization rules`);
|
|
29
|
-
next();
|
|
27
|
+
return next();
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
// Apply exceptions
|
|
33
31
|
if (checkException(req.url, exceptions)) {
|
|
34
32
|
debug(`Url ${req.url} is exception from authorization rules`);
|
|
35
|
-
next();
|
|
33
|
+
return next();
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
// If there is no known user, return 401
|
package/esm/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { issuerSelectorModule, encryptorModule, HttpError } from '@carisls/sso-core';
|
|
3
3
|
import standardRouterModule from './standard/index.js';
|
|
4
|
-
import authorize from '
|
|
4
|
+
import authorize from './authorize.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
*
|
|
8
8
|
* @param {Object} options Options for this router
|
|
9
|
-
* @param {string} options.ssoUrl Base url for provider (usually equal to iss)
|
|
10
9
|
* @param {string} options.clientId SSO Client ID
|
|
10
|
+
* @param {string} options.clientSecret SSO Client Secret
|
|
11
11
|
* @param {number} options.publicKeyCache Public key caching expiration (in seconds)
|
|
12
12
|
* @param {number} options.expOffset Force nenewal of tokens a bit earlier (in case of problems)
|
|
13
|
-
* @param {Object[]} options.providers
|
|
14
|
-
* @param {
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {
|
|
13
|
+
* @param {string|Object[]} options.providers Provider configuration(s). Can be a comma-separated string of provider URLs or an array of provider objects
|
|
14
|
+
* @param {string} options.providers[].ssoUrl Base url for provider (usually equal to iss). Used to discover OpenID Connect configuration
|
|
15
|
+
* @param {string} [options.providers[].iss] Issuer identifier. If not provided, defaults to ssoUrl
|
|
16
|
+
* @param {string} [options.providers[].publicKey] Static public key in PEM format (if not using JWKS)
|
|
17
17
|
* @param {string} options.encPassword Password to be used for cookies encryption
|
|
18
18
|
* @param {string} options.encPasswordSalt Password salt to be used for cookies encription (optional)
|
|
19
19
|
* @param {string} options.encIterationCount Iteration Count to be used for cookies encription (optional)
|
|
@@ -24,24 +24,28 @@ import authorize from '../cjs/authorize.cjs';
|
|
|
24
24
|
* @param {string} options.paths.afterLogin Change default final endpoint after successful login from /
|
|
25
25
|
* @param {string} options.paths.logout Change default logout init endpoint from /logout
|
|
26
26
|
* @param {string} options.paths.afterLogout Change default logout final endpoint after a successful logout from /
|
|
27
|
-
* @param {boolean} options.useCachedSession Whether or not to store access token to cache instead of a cookie (if larger than 4096)
|
|
28
27
|
* @param {boolean} options.groups To request groups scope (default is true)
|
|
29
|
-
* @returns
|
|
28
|
+
* @returns {import('express').Router} Express router instance
|
|
30
29
|
*/
|
|
31
30
|
const router = (options) => {
|
|
32
31
|
// Check options
|
|
33
32
|
if (!options)
|
|
34
33
|
throw Error('You need to set options parameter');
|
|
35
34
|
|
|
35
|
+
if (!options.providers)
|
|
36
|
+
throw Error('providers is always required');
|
|
37
|
+
|
|
36
38
|
// Extract providers
|
|
37
|
-
const providers = options.providers
|
|
39
|
+
const providers = typeof options.providers === 'string'
|
|
40
|
+
? options.providers.split(',').map(i => ({ ssoUrl: i.trim() }))
|
|
41
|
+
: options.providers;
|
|
42
|
+
|
|
43
|
+
if (!Array.isArray(providers) || providers.length === 0)
|
|
44
|
+
throw Error('providers must be a non-empty array or comma-separated string');
|
|
38
45
|
|
|
39
46
|
// Pick the first provider
|
|
40
47
|
providers.splice(1);
|
|
41
48
|
|
|
42
|
-
if (!options.providers)
|
|
43
|
-
throw Error('providers is always required');
|
|
44
|
-
|
|
45
49
|
if (!options.clientId || !options.clientSecret)
|
|
46
50
|
throw Error('clientId and clientSecret are required');
|
|
47
51
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import cookieParserModule from 'cookie-parser';
|
|
2
2
|
function cookieParser (router) {
|
|
3
3
|
router.use((req, res, next) => {
|
|
4
|
-
if (req.
|
|
4
|
+
if (req.cookies)
|
|
5
5
|
next(); // Cookie parser is already attached
|
|
6
6
|
else
|
|
7
7
|
cookieParserModule()(req, res, next); // Cookie parser is needed
|
package/esm/standard/index.js
CHANGED
|
@@ -18,7 +18,10 @@ function index (router, clientId, clientSecret, issuerSelector, encryptor, userM
|
|
|
18
18
|
cookieParserModule(router);
|
|
19
19
|
|
|
20
20
|
// Issuer Selector
|
|
21
|
-
const
|
|
21
|
+
const providerKeys = Object.keys(issuerSelector.providers);
|
|
22
|
+
if (providerKeys.length === 0)
|
|
23
|
+
throw new Error('No providers configured in issuerSelector');
|
|
24
|
+
const tokenValidator = issuerSelector.providers[providerKeys[0]].validator;
|
|
22
25
|
|
|
23
26
|
// Instantiate client
|
|
24
27
|
const client = {
|
|
@@ -26,7 +29,7 @@ function index (router, clientId, clientSecret, issuerSelector, encryptor, userM
|
|
|
26
29
|
client_id: clientId,
|
|
27
30
|
client_secret: clientSecret
|
|
28
31
|
},
|
|
29
|
-
issuer: issuerSelector.providers[
|
|
32
|
+
issuer: issuerSelector.providers[providerKeys[0]].issuer
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
// Instantiate token modules
|
|
@@ -21,9 +21,9 @@ function refreshRequest (router, client, encryptor, accessTokenModule, refreshTo
|
|
|
21
21
|
idTokenModule(res, tokens.id_token, req.protocol === 'https'),
|
|
22
22
|
refreshTokenModule(res, tokens.refresh_token, req.protocol === 'https')
|
|
23
23
|
]))
|
|
24
|
-
.then((
|
|
25
|
-
req.token = token;
|
|
26
|
-
req.idToken = idToken;
|
|
24
|
+
.then((results) => {
|
|
25
|
+
if (results[0]?.token) req.token = results[0].token;
|
|
26
|
+
if (results[1]?.idToken) req.idToken = results[1].idToken;
|
|
27
27
|
})
|
|
28
28
|
.catch((err) => {
|
|
29
29
|
console.error(err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carisls/sso-standard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A middleware implementing standard flow SSO",
|
|
5
5
|
"main": "cjs/index.cjs",
|
|
6
6
|
"module": "esm/index.js",
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"author": "Mihovil Strujic <mstrujic@carisls.com>",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@carisls/sso-core": "1.
|
|
28
|
+
"@carisls/sso-core": "1.2.0",
|
|
29
29
|
"cookie-parser": "^1.4.7",
|
|
30
|
-
"debug": "^4.4.
|
|
31
|
-
"express": "^5.1
|
|
30
|
+
"debug": "^4.4.3",
|
|
31
|
+
"express": "^5.2.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"eslint": "^9.
|
|
35
|
-
"neostandard": "^0.12.
|
|
34
|
+
"eslint": "^9.39.2",
|
|
35
|
+
"neostandard": "^0.12.2"
|
|
36
36
|
}
|
|
37
37
|
}
|