@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 CHANGED
@@ -3,9 +3,12 @@ stages:
3
3
 
4
4
  publish:
5
5
  stage: publish
6
- image: node:22-alpine
7
- before_script:
8
- - echo -e "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
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.length === exceptions[i].length)
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 {*} role Role (string) or an array of roles to filter by (optional)
19
- * @param {*} exceptions Array of urls to skip
20
- * @param {*} redirectToLogin If user needs to be redirected to login in case of unsuccessful validation
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 An array of different providers
14
- * @param {Object[]} options.providers[].ssoUrl Base url for provider (usually equal to iss)
15
- * @param {Object[]} options.providers[].iss If different than ssoUrl
16
- * @param {Object[]} options.providers[].publicKey If different than ssoUrl
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?.split ? options.providers.split(',').map(i => ({ ssoUrl: i })) : 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.cookie)
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
@@ -18,7 +18,10 @@ function index (router, clientId, clientSecret, issuerSelector, encryptor, userM
18
18
  cookieParserModule(router);
19
19
 
20
20
  // Issuer Selector
21
- const tokenValidator = issuerSelector.providers[Object.keys(issuerSelector.providers)[0]].validator;
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[Object.keys(issuerSelector.providers)[0]].issuer
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(([{ token }, { idToken }]) => {
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.length === exceptions[i].length)
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 {*} role Role (string) or an array of roles to filter by (optional)
19
- * @param {*} exceptions Array of urls to skip
20
- * @param {*} redirectToLogin If user needs to be redirected to login in case of unsuccessful validation
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 '../cjs/authorize.cjs';
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 An array of different providers
14
- * @param {Object[]} options.providers[].ssoUrl Base url for provider (usually equal to iss)
15
- * @param {Object[]} options.providers[].iss If different than ssoUrl
16
- * @param {Object[]} options.providers[].publicKey If different than ssoUrl
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?.split ? options.providers.split(',').map(i => ({ ssoUrl: i })) : 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.cookie)
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
@@ -18,7 +18,10 @@ function index (router, clientId, clientSecret, issuerSelector, encryptor, userM
18
18
  cookieParserModule(router);
19
19
 
20
20
  // Issuer Selector
21
- const tokenValidator = issuerSelector.providers[Object.keys(issuerSelector.providers)[0]].validator;
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[Object.keys(issuerSelector.providers)[0]].issuer
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(([{ token }, { idToken }]) => {
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.1.5",
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.1.3",
28
+ "@carisls/sso-core": "1.2.0",
29
29
  "cookie-parser": "^1.4.7",
30
- "debug": "^4.4.0",
31
- "express": "^5.1.0"
30
+ "debug": "^4.4.3",
31
+ "express": "^5.2.1"
32
32
  },
33
33
  "devDependencies": {
34
- "eslint": "^9.24.0",
35
- "neostandard": "^0.12.1"
34
+ "eslint": "^9.39.2",
35
+ "neostandard": "^0.12.2"
36
36
  }
37
37
  }