@carisls/sso-standard 1.0.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 +16 -0
- package/cjs/authorize.cjs +67 -0
- package/cjs/index.cjs +79 -0
- package/cjs/shared/cookies/accessToken.cjs +20 -0
- package/cjs/shared/cookies/cookieParser.cjs +11 -0
- package/cjs/shared/cookies/idToken.cjs +23 -0
- package/cjs/shared/cookies/refreshToken.cjs +20 -0
- package/cjs/shared/logout.cjs +25 -0
- package/cjs/shared/user.cjs +31 -0
- package/cjs/standard/codeRequest.cjs +17 -0
- package/cjs/standard/index.cjs +74 -0
- package/cjs/standard/login.cjs +15 -0
- package/cjs/standard/refreshRequest.cjs +47 -0
- package/cjs/standard/sso.cjs +25 -0
- package/esm/authorize.js +67 -0
- package/esm/index.js +82 -0
- package/esm/shared/cookies/accessToken.js +20 -0
- package/esm/shared/cookies/cookieParser.js +11 -0
- package/esm/shared/cookies/idToken.js +23 -0
- package/esm/shared/cookies/refreshToken.js +20 -0
- package/esm/shared/logout.js +25 -0
- package/esm/shared/user.js +31 -0
- package/esm/standard/codeRequest.js +17 -0
- package/esm/standard/index.js +74 -0
- package/esm/standard/login.js +15 -0
- package/esm/standard/refreshRequest.js +47 -0
- package/esm/standard/sso.js +25 -0
- package/package.json +24 -0
package/.gitlab-ci.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- publish
|
|
3
|
+
|
|
4
|
+
publish:
|
|
5
|
+
stage: publish
|
|
6
|
+
image: node:22-alpine
|
|
7
|
+
before_script:
|
|
8
|
+
- echo -e "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
|
|
9
|
+
script:
|
|
10
|
+
- npm i
|
|
11
|
+
- npm run build --if-present
|
|
12
|
+
- npm publish --access public
|
|
13
|
+
only:
|
|
14
|
+
- main
|
|
15
|
+
tags:
|
|
16
|
+
- build-image
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const debugModule = require('debug');
|
|
2
|
+
const debug = debugModule('keycloak-client:token:authorize');
|
|
3
|
+
|
|
4
|
+
function checkException (url, exceptions) {
|
|
5
|
+
if (!exceptions || !exceptions.length)
|
|
6
|
+
return false;
|
|
7
|
+
|
|
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])
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @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
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function authorize (role, exceptions, redirectToLogin = false) {
|
|
25
|
+
return (req, res, next) => {
|
|
26
|
+
// Check if this is login or sso url
|
|
27
|
+
if (req.url.match(/^\/login/i) || req.url.match(/^\/sso/i)) {
|
|
28
|
+
debug(`Url ${req.url} is exception from default authorization rules`);
|
|
29
|
+
next();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Apply exceptions
|
|
33
|
+
if (checkException(req.url, exceptions)) {
|
|
34
|
+
debug(`Url ${req.url} is exception from authorization rules`);
|
|
35
|
+
next();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If there is no known user, return 401
|
|
39
|
+
else if (!req.user) {
|
|
40
|
+
debug('User is unknown where required');
|
|
41
|
+
if (!redirectToLogin)
|
|
42
|
+
next({ status: 401, message: 'Unauthorized' });
|
|
43
|
+
else
|
|
44
|
+
res.redirect('/login?ReturnUrl=' + encodeURIComponent(req.originalUrl));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If user exists but does not belong to a specified role, return 403
|
|
48
|
+
else if (role && typeof role === 'string' && !req.user.roles?.includes(role)) {
|
|
49
|
+
debug(`User is known, but it does not have an appropriate role '${role}'`);
|
|
50
|
+
next({ status: 403, message: 'Forbidden' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If user exists but does not belong to any of specified roles, return 403
|
|
54
|
+
else if (role && Array.isArray(role) && !role.some(i => req.user.roles?.includes(i))) {
|
|
55
|
+
debug(`User is known, but it does not have any required role from the list of roles '${role}'`);
|
|
56
|
+
next({ status: 403, message: 'Forbidden' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If user is authorized
|
|
60
|
+
else {
|
|
61
|
+
debug('User is known' + (role ? ` and has a needed role '${role}'` : ''));
|
|
62
|
+
next();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
module.exports = authorize;
|
package/cjs/index.cjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { Router } = require('express');
|
|
2
|
+
const { issuerSelectorModule, encryptorModule, HttpError } = require('@carisls/sso-core');
|
|
3
|
+
const standardRouterModule = require('./standard/index.cjs');
|
|
4
|
+
const authorize = require('./authorize.cjs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options Options for this router
|
|
9
|
+
* @param {string} options.ssoUrl Base url for provider (usually equal to iss)
|
|
10
|
+
* @param {string} options.clientId SSO Client ID
|
|
11
|
+
* @param {number} options.publicKeyCache Public key caching expiration (in seconds)
|
|
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
|
|
17
|
+
* @param {string} options.encPassword Password to be used for cookies encryption
|
|
18
|
+
* @param {string} options.encPasswordSalt Password salt to be used for cookies encription (optional)
|
|
19
|
+
* @param {string} options.encIterationCount Iteration Count to be used for cookies encription (optional)
|
|
20
|
+
* @param {function} options.userMapper A custom userMapper (maps token to user object attached to a request)
|
|
21
|
+
* @param {Object} options.paths Customizing of default paths used
|
|
22
|
+
* @param {string} options.paths.login Change default login endpoint from /login
|
|
23
|
+
* @param {string} options.paths.sso Change default sso return endpoint from /sso
|
|
24
|
+
* @param {string} options.paths.afterLogin Change default final endpoint after successful login from /
|
|
25
|
+
* @param {string} options.paths.logout Change default logout init endpoint from /logout
|
|
26
|
+
* @param {string} options.paths.afterLogout Change default logout final endpoint after a successful logout from /
|
|
27
|
+
* @returns
|
|
28
|
+
*/
|
|
29
|
+
const router = (options) => {
|
|
30
|
+
// Check options
|
|
31
|
+
if (!options)
|
|
32
|
+
throw Error('You need to set options parameter');
|
|
33
|
+
|
|
34
|
+
if (!options.ssoUrl && !options.providers)
|
|
35
|
+
throw Error('ssoUrl is always required');
|
|
36
|
+
|
|
37
|
+
if (!options.clientId && !options.providers)
|
|
38
|
+
throw Error('clientId is required');
|
|
39
|
+
|
|
40
|
+
if (!options.publicKeyCache)
|
|
41
|
+
options.publicKeyCache = 300;
|
|
42
|
+
|
|
43
|
+
if (!options.expOffset)
|
|
44
|
+
options.expOffset = 0;
|
|
45
|
+
|
|
46
|
+
const router = Router();
|
|
47
|
+
|
|
48
|
+
(async () => {
|
|
49
|
+
const providers = options.providers || [];
|
|
50
|
+
if (!providers.length && options.ssoUrl)
|
|
51
|
+
providers.push({ iss: options.iss || options.ssoUrl, ssoUrl: options.ssoUrl, publicKey: options.publicKey });
|
|
52
|
+
const issuerSelector = await issuerSelectorModule(providers, options.publicKeyCache);
|
|
53
|
+
const encryptor = encryptorModule(options.encPassword, options.encPasswordSalt, options.encIterationCount);
|
|
54
|
+
|
|
55
|
+
standardRouterModule(
|
|
56
|
+
router,
|
|
57
|
+
options.clientId,
|
|
58
|
+
options.clientSecret,
|
|
59
|
+
issuerSelector,
|
|
60
|
+
encryptor,
|
|
61
|
+
options.userMapper,
|
|
62
|
+
options.paths,
|
|
63
|
+
options.expOffset);
|
|
64
|
+
|
|
65
|
+
})()
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
router.all('*', (req, res, next) => {
|
|
68
|
+
next(new HttpError(500, 'SSO settings have failed to load'));
|
|
69
|
+
});
|
|
70
|
+
console.error(err);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return router;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
router,
|
|
78
|
+
authorize
|
|
79
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function accessTokenModule (tokenValidator, encryptor, expOffset = 0) {
|
|
2
|
+
return async function (res, accessToken, cookiesSecure, nonce) {
|
|
3
|
+
// Process access token if returned
|
|
4
|
+
const { token } = await tokenValidator(accessToken, nonce || undefined);
|
|
5
|
+
|
|
6
|
+
// Add access_token to cookies
|
|
7
|
+
const encToken = await encryptor.encrypt(accessToken);
|
|
8
|
+
res.cookie('x-session', encToken, {
|
|
9
|
+
httpOnly: true,
|
|
10
|
+
expires: new Date((token.exp + expOffset) * 1000),
|
|
11
|
+
secure: cookiesSecure,
|
|
12
|
+
sameSite: 'lax'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Return token
|
|
16
|
+
return { token: accessToken };
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = accessTokenModule;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const cookieParserModule = require('cookie-parser');
|
|
2
|
+
function cookieParser (router) {
|
|
3
|
+
router.all('*', (req, res, next) => {
|
|
4
|
+
if (req.cookie)
|
|
5
|
+
next(); // Cookie parser is already attached
|
|
6
|
+
else
|
|
7
|
+
cookieParserModule()(req, res, next); // Cookie parser is needed
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
module.exports = cookieParser;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { tokenDecoderModule: decodeToken } = require('@carisls/sso-core');
|
|
2
|
+
|
|
3
|
+
function idTokenModule (encryptor) {
|
|
4
|
+
return async function (res, idToken, cookiesSecure) {
|
|
5
|
+
// Process access token if returned
|
|
6
|
+
const token = idToken.includes('.') ? decodeToken.data(idToken) : idToken;
|
|
7
|
+
|
|
8
|
+
// Add access_token to cookies
|
|
9
|
+
const encToken = await encryptor.encrypt(idToken);
|
|
10
|
+
res.cookie('x-session-id', encToken, {
|
|
11
|
+
httpOnly: true,
|
|
12
|
+
// If not provided, refresh_token expires in 8 hours
|
|
13
|
+
expires: token.exp ? new Date(token.exp * 1000) : new Date(new Date().getTime() + 8 * 60 * 60 * 1000),
|
|
14
|
+
secure: cookiesSecure,
|
|
15
|
+
sameSite: 'lax'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Return token
|
|
19
|
+
return { idToken };
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = idTokenModule;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { tokenDecoderModule: decodeToken } = require('@carisls/sso-core');
|
|
2
|
+
|
|
3
|
+
function refreshTokenModule (tokenValidator, encryptor) {
|
|
4
|
+
return async function (res, refreshToken, cookiesSecure) {
|
|
5
|
+
// Process access token if returned
|
|
6
|
+
const token = refreshToken.includes('.') ? decodeToken.data(refreshToken) : refreshToken;
|
|
7
|
+
|
|
8
|
+
// Add access_token to cookies
|
|
9
|
+
const encToken = await encryptor.encrypt(refreshToken);
|
|
10
|
+
res.cookie('x-session-sso', encToken, {
|
|
11
|
+
httpOnly: true,
|
|
12
|
+
// If not provided, refresh_token expires in 8 hours
|
|
13
|
+
expires: token.exp ? new Date(token.exp * 1000) : new Date(new Date().getTime() + 8 * 60 * 60 * 1000),
|
|
14
|
+
secure: cookiesSecure,
|
|
15
|
+
sameSite: 'lax'
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = refreshTokenModule;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function logout (router, client, encryptor, logoutPath, afterLogoutPath) {
|
|
2
|
+
router.get(logoutPath, async (req, res) => {
|
|
3
|
+
let idToken = '';
|
|
4
|
+
if (req.cookies['x-session'])
|
|
5
|
+
res.cookie('x-session', 'Removing...', { maxAge: 0 });
|
|
6
|
+
if (req.cookies['x-session-sso'])
|
|
7
|
+
res.cookie('x-session-sso', 'Removing...', { maxAge: 0 });
|
|
8
|
+
if (req.cookies['x-session-id']) {
|
|
9
|
+
idToken = await encryptor.decrypt(req.cookies['x-session-id']);
|
|
10
|
+
res.cookie('x-session-id', 'Removing...', { maxAge: 0 });
|
|
11
|
+
}
|
|
12
|
+
res.redirect(client.endSessionUrl({
|
|
13
|
+
|
|
14
|
+
// redirect_uri for keycloak
|
|
15
|
+
redirect_uri: `${req.protocol}://${req.headers.host}${afterLogoutPath}`,
|
|
16
|
+
|
|
17
|
+
// id_token_hint and post_logout_redirect_uri for okta
|
|
18
|
+
id_token_hint: idToken,
|
|
19
|
+
post_logout_redirect_uri: `${req.protocol}://${req.headers.host}${afterLogoutPath}`
|
|
20
|
+
|
|
21
|
+
}));
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
module.exports = logout;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { tokenDecoderModule: decodeToken, userMapper: userMapperDefault } = require('@carisls/sso-core');
|
|
2
|
+
|
|
3
|
+
function user (router, encryptor, userMapper) {
|
|
4
|
+
router.all('*', (req, res, next) => {
|
|
5
|
+
// Check if cookie is there
|
|
6
|
+
if (!req.cookies['x-session'] && !req.token) {
|
|
7
|
+
next();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const accessTokenDecoder = req.token ? Promise.resolve(req.token) : encryptor.decrypt(req.cookies['x-session']);
|
|
12
|
+
const idTokenDecoder = req.idToken ? Promise.resolve(req.idToken) : encryptor.decrypt(req.cookies['x-session-id']) ?? Promise.resolve();
|
|
13
|
+
|
|
14
|
+
Promise.all([accessTokenDecoder, idTokenDecoder])
|
|
15
|
+
.then(([tokenEnc, idTokenEnc]) => {
|
|
16
|
+
req.token = tokenEnc;
|
|
17
|
+
req.idToken = idTokenEnc;
|
|
18
|
+
const token = tokenEnc ? decodeToken.data(tokenEnc) : null;
|
|
19
|
+
const idToken = idTokenEnc ? decodeToken.data(idTokenEnc) : null;
|
|
20
|
+
req.user = userMapper ? userMapper(token, idToken) : userMapperDefault(token, idToken);
|
|
21
|
+
})
|
|
22
|
+
.catch(() => {
|
|
23
|
+
res.cookie('x-session', 'Removing...', { maxAge: 0, httpOnly: true, secure: req.protocol === 'https', sameSite: 'lax' });
|
|
24
|
+
})
|
|
25
|
+
.finally(() => {
|
|
26
|
+
next();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = user;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { postForm } = require('@carisls/sso-core');
|
|
2
|
+
|
|
3
|
+
function codeRequest (client) {
|
|
4
|
+
const clientId = client.metadata.client_id;
|
|
5
|
+
const clientSecret = client.metadata.client_secret || undefined;
|
|
6
|
+
const url = client.issuer.token_endpoint;
|
|
7
|
+
|
|
8
|
+
return (code, redirectUri) => postForm(url, {
|
|
9
|
+
client_id: clientId,
|
|
10
|
+
client_secret: clientSecret,
|
|
11
|
+
grant_type: 'authorization_code',
|
|
12
|
+
code,
|
|
13
|
+
redirect_uri: redirectUri
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = codeRequest;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const cookieParserModule = require('../shared/cookies/cookieParser.cjs');
|
|
2
|
+
|
|
3
|
+
const loginModule = require('./login.cjs');
|
|
4
|
+
const ssoModule = require('./sso.cjs');
|
|
5
|
+
const logoutModule = require('../shared/logout.cjs');
|
|
6
|
+
const refreshRequestModule = require('./refreshRequest.cjs');
|
|
7
|
+
const refreshTokenModule2 = require('../shared/cookies/refreshToken.cjs');
|
|
8
|
+
|
|
9
|
+
const accessTokenModule2 = require('../shared/cookies/accessToken.cjs');
|
|
10
|
+
|
|
11
|
+
const idTokenModule2 = require('../shared/cookies/idToken.cjs');
|
|
12
|
+
const codeRequestModule2 = require('./codeRequest.cjs');
|
|
13
|
+
|
|
14
|
+
const userModule = require('../shared/user.cjs');
|
|
15
|
+
|
|
16
|
+
function index (router, clientId, clientSecret, issuerSelector, encryptor, userMapper, paths, expOffset = 0) {
|
|
17
|
+
// Add cookieParser
|
|
18
|
+
cookieParserModule(router);
|
|
19
|
+
|
|
20
|
+
// Issuer Selector
|
|
21
|
+
const keycloakIssuer = issuerSelector.providers[Object.keys(issuerSelector.providers)[0]].issuer;
|
|
22
|
+
const tokenValidator = issuerSelector.providers[Object.keys(issuerSelector.providers)[0]].validator;
|
|
23
|
+
|
|
24
|
+
// Instantiate client
|
|
25
|
+
const client = new keycloakIssuer.Client({
|
|
26
|
+
client_id: clientId,
|
|
27
|
+
client_secret: clientSecret,
|
|
28
|
+
response_types: ['code']
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Instantiate token modules
|
|
32
|
+
const accessTokenModule = accessTokenModule2(tokenValidator, encryptor, expOffset);
|
|
33
|
+
const refreshTokenModule = refreshTokenModule2(tokenValidator, encryptor);
|
|
34
|
+
const idTokenModule = idTokenModule2(encryptor); // id_token must be saved for Okta logout
|
|
35
|
+
|
|
36
|
+
const codeRequestModule = codeRequestModule2(client);
|
|
37
|
+
|
|
38
|
+
// Send to SSO login
|
|
39
|
+
loginModule(router,
|
|
40
|
+
client,
|
|
41
|
+
paths?.login || '/login',
|
|
42
|
+
paths?.sso || '/sso',
|
|
43
|
+
paths?.afterLogin || '/'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Handle return from standard SSO
|
|
47
|
+
ssoModule(router, client,
|
|
48
|
+
paths?.sso || '/sso',
|
|
49
|
+
codeRequestModule,
|
|
50
|
+
accessTokenModule,
|
|
51
|
+
refreshTokenModule,
|
|
52
|
+
idTokenModule);
|
|
53
|
+
|
|
54
|
+
// Remove session
|
|
55
|
+
logoutModule(router,
|
|
56
|
+
client,
|
|
57
|
+
encryptor,
|
|
58
|
+
paths?.logout || '/logout',
|
|
59
|
+
paths?.afterLogout || '/'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Check if session needs/can be refreshed
|
|
63
|
+
refreshRequestModule(router,
|
|
64
|
+
client,
|
|
65
|
+
encryptor,
|
|
66
|
+
accessTokenModule,
|
|
67
|
+
refreshTokenModule,
|
|
68
|
+
idTokenModule);
|
|
69
|
+
|
|
70
|
+
// Inject user from session cookie if present
|
|
71
|
+
userModule(router, encryptor, userMapper);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
module.exports = index;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function login (router, client, loginPath, ssoPath, afterLoginPath) {
|
|
2
|
+
router.get(loginPath, (req, res) => {
|
|
3
|
+
// Set redirectTo
|
|
4
|
+
const redirectTo = req.query.ReturnUrl || afterLoginPath;
|
|
5
|
+
|
|
6
|
+
// Standard flow initiates directly
|
|
7
|
+
res.redirect(client.authorizationUrl({
|
|
8
|
+
redirect_uri: `${req.protocol}://${req.headers.host}${ssoPath}`,
|
|
9
|
+
state: redirectTo ? Buffer.from(redirectTo).toString('hex') : undefined,
|
|
10
|
+
scope: 'openid profile email offline_access groups'
|
|
11
|
+
}));
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
module.exports = login;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { postForm } = require('@carisls/sso-core');
|
|
2
|
+
|
|
3
|
+
function refreshRequest (router, client, encryptor, accessTokenModule, refreshTokenModule, idTokenModule) {
|
|
4
|
+
const clientId = client.metadata.client_id;
|
|
5
|
+
const clientSecret = client.metadata.client_secret || undefined;
|
|
6
|
+
const url = client.issuer.token_endpoint;
|
|
7
|
+
|
|
8
|
+
router.all('*', (req, res, next) => {
|
|
9
|
+
// Check if SSO cookie is there
|
|
10
|
+
if (req.cookies['x-session']?.length > 36 || req.token || !req.cookies['x-session-sso']) {
|
|
11
|
+
next();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
encryptor
|
|
16
|
+
.decrypt(req.cookies['x-session-sso'])
|
|
17
|
+
.then(token => postForm(url, {
|
|
18
|
+
client_id: clientId,
|
|
19
|
+
client_secret: clientSecret,
|
|
20
|
+
grant_type: 'refresh_token',
|
|
21
|
+
refresh_token: token
|
|
22
|
+
}))
|
|
23
|
+
.then(tokens => Promise.all([
|
|
24
|
+
accessTokenModule(res, tokens.access_token, req.protocol === 'https'),
|
|
25
|
+
idTokenModule(res, tokens.id_token, req.protocol === 'https'),
|
|
26
|
+
refreshTokenModule(res, tokens.refresh_token, req.protocol === 'https')
|
|
27
|
+
]))
|
|
28
|
+
.then(([{ token }, { idToken }]) => {
|
|
29
|
+
req.token = token;
|
|
30
|
+
req.idToken = idToken;
|
|
31
|
+
})
|
|
32
|
+
.catch((err) => {
|
|
33
|
+
console.error(err);
|
|
34
|
+
res.cookie('x-session-sso', 'Removing...', {
|
|
35
|
+
maxAge: 0,
|
|
36
|
+
httpOnly: true,
|
|
37
|
+
secure: req.protocol === 'https',
|
|
38
|
+
sameSite: 'lax'
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
.finally(() => {
|
|
42
|
+
next();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = refreshRequest;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { HttpError } = require('@carisls/sso-core');
|
|
2
|
+
|
|
3
|
+
function sso (router, client, ssoPath, codeRequestModule, accessTokenModule, refreshTokenModule, idTokenModule) {
|
|
4
|
+
router.get(ssoPath, (req, res, next) => {
|
|
5
|
+
if (!req.query.code) {
|
|
6
|
+
next(new HttpError(500, req.query.error));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
codeRequestModule(req.query.code, `${req.protocol}://${req.headers.host}${ssoPath}`)
|
|
11
|
+
.then((tokens) => Promise.all([
|
|
12
|
+
accessTokenModule(res, tokens.access_token, req.protocol === 'https'),
|
|
13
|
+
refreshTokenModule(res, tokens.refresh_token, req.protocol === 'https'),
|
|
14
|
+
idTokenModule(res, tokens.id_token, req.protocol === 'https')
|
|
15
|
+
]))
|
|
16
|
+
.then(() => {
|
|
17
|
+
res.redirect(req.query.state ? Buffer.from(req.query.state, 'hex').toString('utf8') : '/');
|
|
18
|
+
})
|
|
19
|
+
.catch((err) => {
|
|
20
|
+
next(err);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
module.exports = sso;
|
package/esm/authorize.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import debugModule from 'debug';
|
|
2
|
+
const debug = debugModule('keycloak-client:token:authorize');
|
|
3
|
+
|
|
4
|
+
function checkException (url, exceptions) {
|
|
5
|
+
if (!exceptions || !exceptions.length)
|
|
6
|
+
return false;
|
|
7
|
+
|
|
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])
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @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
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function authorize (role, exceptions, redirectToLogin = false) {
|
|
25
|
+
return (req, res, next) => {
|
|
26
|
+
// Check if this is login or sso url
|
|
27
|
+
if (req.url.match(/^\/login/i) || req.url.match(/^\/sso/i)) {
|
|
28
|
+
debug(`Url ${req.url} is exception from default authorization rules`);
|
|
29
|
+
next();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Apply exceptions
|
|
33
|
+
if (checkException(req.url, exceptions)) {
|
|
34
|
+
debug(`Url ${req.url} is exception from authorization rules`);
|
|
35
|
+
next();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If there is no known user, return 401
|
|
39
|
+
else if (!req.user) {
|
|
40
|
+
debug('User is unknown where required');
|
|
41
|
+
if (!redirectToLogin)
|
|
42
|
+
next({ status: 401, message: 'Unauthorized' });
|
|
43
|
+
else
|
|
44
|
+
res.redirect('/login?ReturnUrl=' + encodeURIComponent(req.originalUrl));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If user exists but does not belong to a specified role, return 403
|
|
48
|
+
else if (role && typeof role === 'string' && !req.user.roles?.includes(role)) {
|
|
49
|
+
debug(`User is known, but it does not have an appropriate role '${role}'`);
|
|
50
|
+
next({ status: 403, message: 'Forbidden' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If user exists but does not belong to any of specified roles, return 403
|
|
54
|
+
else if (role && Array.isArray(role) && !role.some(i => req.user.roles?.includes(i))) {
|
|
55
|
+
debug(`User is known, but it does not have any required role from the list of roles '${role}'`);
|
|
56
|
+
next({ status: 403, message: 'Forbidden' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If user is authorized
|
|
60
|
+
else {
|
|
61
|
+
debug('User is known' + (role ? ` and has a needed role '${role}'` : ''));
|
|
62
|
+
next();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default authorize;
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { issuerSelectorModule, encryptorModule, HttpError } from '@carisls/sso-core';
|
|
3
|
+
import standardRouterModule from './standard/index.js';
|
|
4
|
+
import authorize from '../cjs/authorize.cjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options Options for this router
|
|
9
|
+
* @param {string} options.ssoUrl Base url for provider (usually equal to iss)
|
|
10
|
+
* @param {string} options.clientId SSO Client ID
|
|
11
|
+
* @param {number} options.publicKeyCache Public key caching expiration (in seconds)
|
|
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
|
|
17
|
+
* @param {string} options.encPassword Password to be used for cookies encryption
|
|
18
|
+
* @param {string} options.encPasswordSalt Password salt to be used for cookies encription (optional)
|
|
19
|
+
* @param {string} options.encIterationCount Iteration Count to be used for cookies encription (optional)
|
|
20
|
+
* @param {function} options.userMapper A custom userMapper (maps token to user object attached to a request)
|
|
21
|
+
* @param {Object} options.paths Customizing of default paths used
|
|
22
|
+
* @param {string} options.paths.login Change default login endpoint from /login
|
|
23
|
+
* @param {string} options.paths.sso Change default sso return endpoint from /sso
|
|
24
|
+
* @param {string} options.paths.afterLogin Change default final endpoint after successful login from /
|
|
25
|
+
* @param {string} options.paths.logout Change default logout init endpoint from /logout
|
|
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
|
+
* @returns
|
|
29
|
+
*/
|
|
30
|
+
const router = (options) => {
|
|
31
|
+
// Check options
|
|
32
|
+
if (!options)
|
|
33
|
+
throw Error('You need to set options parameter');
|
|
34
|
+
|
|
35
|
+
if (!options.ssoUrl && !options.providers)
|
|
36
|
+
throw Error('ssoUrl is always required');
|
|
37
|
+
|
|
38
|
+
if (!options.clientId && !options.providers)
|
|
39
|
+
throw Error('clientId is required');
|
|
40
|
+
|
|
41
|
+
if (!options.publicKeyCache)
|
|
42
|
+
options.publicKeyCache = 300;
|
|
43
|
+
|
|
44
|
+
if (!options.expOffset)
|
|
45
|
+
options.expOffset = 0;
|
|
46
|
+
|
|
47
|
+
const router = Router();
|
|
48
|
+
|
|
49
|
+
(async () => {
|
|
50
|
+
const providers = options.providers || [];
|
|
51
|
+
if (!providers.length && options.ssoUrl)
|
|
52
|
+
providers.push({ iss: options.iss || options.ssoUrl, ssoUrl: options.ssoUrl, publicKey: options.publicKey });
|
|
53
|
+
const issuerSelector = await issuerSelectorModule(providers, options.publicKeyCache);
|
|
54
|
+
const encryptor = encryptorModule(options.encPassword, options.encPasswordSalt, options.encIterationCount);
|
|
55
|
+
|
|
56
|
+
standardRouterModule(
|
|
57
|
+
router,
|
|
58
|
+
options.clientId,
|
|
59
|
+
options.clientSecret,
|
|
60
|
+
issuerSelector,
|
|
61
|
+
encryptor,
|
|
62
|
+
options.userMapper,
|
|
63
|
+
options.paths,
|
|
64
|
+
options.expOffset);
|
|
65
|
+
|
|
66
|
+
})()
|
|
67
|
+
.catch((err) => {
|
|
68
|
+
router.all('*', (req, res, next) => {
|
|
69
|
+
next(new HttpError(500, 'SSO settings have failed to load'));
|
|
70
|
+
});
|
|
71
|
+
console.error(err);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return router;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export { router, authorize };
|
|
78
|
+
|
|
79
|
+
export default {
|
|
80
|
+
router,
|
|
81
|
+
authorize
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function accessTokenModule (tokenValidator, encryptor, expOffset = 0) {
|
|
2
|
+
return async function (res, accessToken, cookiesSecure, nonce) {
|
|
3
|
+
// Process access token if returned
|
|
4
|
+
const { token } = await tokenValidator(accessToken, nonce || undefined);
|
|
5
|
+
|
|
6
|
+
// Add access_token to cookies
|
|
7
|
+
const encToken = await encryptor.encrypt(accessToken);
|
|
8
|
+
res.cookie('x-session', encToken, {
|
|
9
|
+
httpOnly: true,
|
|
10
|
+
expires: new Date((token.exp + expOffset) * 1000),
|
|
11
|
+
secure: cookiesSecure,
|
|
12
|
+
sameSite: 'lax'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Return token
|
|
16
|
+
return { token: accessToken };
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default accessTokenModule;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import cookieParserModule from 'cookie-parser';
|
|
2
|
+
function cookieParser (router) {
|
|
3
|
+
router.all('*', (req, res, next) => {
|
|
4
|
+
if (req.cookie)
|
|
5
|
+
next(); // Cookie parser is already attached
|
|
6
|
+
else
|
|
7
|
+
cookieParserModule()(req, res, next); // Cookie parser is needed
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default cookieParser;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { tokenDecoderModule as decodeToken } from '@carisls/sso-core';
|
|
2
|
+
|
|
3
|
+
function idTokenModule (encryptor) {
|
|
4
|
+
return async function (res, idToken, cookiesSecure) {
|
|
5
|
+
// Process access token if returned
|
|
6
|
+
const token = idToken.includes('.') ? decodeToken.data(idToken) : idToken;
|
|
7
|
+
|
|
8
|
+
// Add access_token to cookies
|
|
9
|
+
const encToken = await encryptor.encrypt(idToken);
|
|
10
|
+
res.cookie('x-session-id', encToken, {
|
|
11
|
+
httpOnly: true,
|
|
12
|
+
// If not provided, refresh_token expires in 8 hours
|
|
13
|
+
expires: token.exp ? new Date(token.exp * 1000) : new Date(new Date().getTime() + 8 * 60 * 60 * 1000),
|
|
14
|
+
secure: cookiesSecure,
|
|
15
|
+
sameSite: 'lax'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Return token
|
|
19
|
+
return { idToken };
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default idTokenModule;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { tokenDecoderModule as decodeToken } from '@carisls/sso-core';
|
|
2
|
+
|
|
3
|
+
function refreshTokenModule (tokenValidator, encryptor) {
|
|
4
|
+
return async function (res, refreshToken, cookiesSecure) {
|
|
5
|
+
// Process access token if returned
|
|
6
|
+
const token = refreshToken.includes('.') ? decodeToken.data(refreshToken) : refreshToken;
|
|
7
|
+
|
|
8
|
+
// Add access_token to cookies
|
|
9
|
+
const encToken = await encryptor.encrypt(refreshToken);
|
|
10
|
+
res.cookie('x-session-sso', encToken, {
|
|
11
|
+
httpOnly: true,
|
|
12
|
+
// If not provided, refresh_token expires in 8 hours
|
|
13
|
+
expires: token.exp ? new Date(token.exp * 1000) : new Date(new Date().getTime() + 8 * 60 * 60 * 1000),
|
|
14
|
+
secure: cookiesSecure,
|
|
15
|
+
sameSite: 'lax'
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default refreshTokenModule;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function logout (router, client, encryptor, logoutPath, afterLogoutPath) {
|
|
2
|
+
router.get(logoutPath, async (req, res) => {
|
|
3
|
+
let idToken = '';
|
|
4
|
+
if (req.cookies['x-session'])
|
|
5
|
+
res.cookie('x-session', 'Removing...', { maxAge: 0 });
|
|
6
|
+
if (req.cookies['x-session-sso'])
|
|
7
|
+
res.cookie('x-session-sso', 'Removing...', { maxAge: 0 });
|
|
8
|
+
if (req.cookies['x-session-id']) {
|
|
9
|
+
idToken = await encryptor.decrypt(req.cookies['x-session-id']);
|
|
10
|
+
res.cookie('x-session-id', 'Removing...', { maxAge: 0 });
|
|
11
|
+
}
|
|
12
|
+
res.redirect(client.endSessionUrl({
|
|
13
|
+
|
|
14
|
+
// redirect_uri for keycloak
|
|
15
|
+
redirect_uri: `${req.protocol}://${req.headers.host}${afterLogoutPath}`,
|
|
16
|
+
|
|
17
|
+
// id_token_hint and post_logout_redirect_uri for okta
|
|
18
|
+
id_token_hint: idToken,
|
|
19
|
+
post_logout_redirect_uri: `${req.protocol}://${req.headers.host}${afterLogoutPath}`
|
|
20
|
+
|
|
21
|
+
}));
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default logout;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { tokenDecoderModule as decodeToken, userMapper as userMapperDefault } from '@carisls/sso-core';
|
|
2
|
+
|
|
3
|
+
function user (router, encryptor, userMapper) {
|
|
4
|
+
router.all('*', (req, res, next) => {
|
|
5
|
+
// Check if cookie is there
|
|
6
|
+
if (!req.cookies['x-session'] && !req.token) {
|
|
7
|
+
next();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const accessTokenDecoder = req.token ? Promise.resolve(req.token) : encryptor.decrypt(req.cookies['x-session']);
|
|
12
|
+
const idTokenDecoder = req.idToken ? Promise.resolve(req.idToken) : encryptor.decrypt(req.cookies['x-session-id']) ?? Promise.resolve();
|
|
13
|
+
|
|
14
|
+
Promise.all([accessTokenDecoder, idTokenDecoder])
|
|
15
|
+
.then(([tokenEnc, idTokenEnc]) => {
|
|
16
|
+
req.token = tokenEnc;
|
|
17
|
+
req.idToken = idTokenEnc;
|
|
18
|
+
const token = tokenEnc ? decodeToken.data(tokenEnc) : null;
|
|
19
|
+
const idToken = idTokenEnc ? decodeToken.data(idTokenEnc) : null;
|
|
20
|
+
req.user = userMapper ? userMapper(token, idToken) : userMapperDefault(token, idToken);
|
|
21
|
+
})
|
|
22
|
+
.catch(() => {
|
|
23
|
+
res.cookie('x-session', 'Removing...', { maxAge: 0, httpOnly: true, secure: req.protocol === 'https', sameSite: 'lax' });
|
|
24
|
+
})
|
|
25
|
+
.finally(() => {
|
|
26
|
+
next();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default user;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { postForm } from '@carisls/sso-core';
|
|
2
|
+
|
|
3
|
+
function codeRequest (client) {
|
|
4
|
+
const clientId = client.metadata.client_id;
|
|
5
|
+
const clientSecret = client.metadata.client_secret || undefined;
|
|
6
|
+
const url = client.issuer.token_endpoint;
|
|
7
|
+
|
|
8
|
+
return (code, redirectUri) => postForm(url, {
|
|
9
|
+
client_id: clientId,
|
|
10
|
+
client_secret: clientSecret,
|
|
11
|
+
grant_type: 'authorization_code',
|
|
12
|
+
code,
|
|
13
|
+
redirect_uri: redirectUri
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default codeRequest;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import cookieParserModule from '../shared/cookies/cookieParser.js';
|
|
2
|
+
|
|
3
|
+
import loginModule from './login.js';
|
|
4
|
+
import ssoModule from './sso.js';
|
|
5
|
+
import logoutModule from '../shared/logout.js';
|
|
6
|
+
import refreshRequestModule from './refreshRequest.js';
|
|
7
|
+
import refreshTokenModule2 from '../shared/cookies/refreshToken.js';
|
|
8
|
+
|
|
9
|
+
import accessTokenModule2 from '../shared/cookies/accessToken.js';
|
|
10
|
+
|
|
11
|
+
import idTokenModule2 from '../shared/cookies/idToken.js';
|
|
12
|
+
import codeRequestModule2 from './codeRequest.js';
|
|
13
|
+
|
|
14
|
+
import userModule from '../shared/user.js';
|
|
15
|
+
|
|
16
|
+
function index (router, clientId, clientSecret, issuerSelector, encryptor, userMapper, paths, expOffset = 0) {
|
|
17
|
+
// Add cookieParser
|
|
18
|
+
cookieParserModule(router);
|
|
19
|
+
|
|
20
|
+
// Issuer Selector
|
|
21
|
+
const keycloakIssuer = issuerSelector.providers[Object.keys(issuerSelector.providers)[0]].issuer;
|
|
22
|
+
const tokenValidator = issuerSelector.providers[Object.keys(issuerSelector.providers)[0]].validator;
|
|
23
|
+
|
|
24
|
+
// Instantiate client
|
|
25
|
+
const client = new keycloakIssuer.Client({
|
|
26
|
+
client_id: clientId,
|
|
27
|
+
client_secret: clientSecret,
|
|
28
|
+
response_types: ['code']
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Instantiate token modules
|
|
32
|
+
const accessTokenModule = accessTokenModule2(tokenValidator, encryptor, expOffset);
|
|
33
|
+
const refreshTokenModule = refreshTokenModule2(tokenValidator, encryptor);
|
|
34
|
+
const idTokenModule = idTokenModule2(encryptor); // id_token must be saved for Okta logout
|
|
35
|
+
|
|
36
|
+
const codeRequestModule = codeRequestModule2(client);
|
|
37
|
+
|
|
38
|
+
// Send to SSO login
|
|
39
|
+
loginModule(router,
|
|
40
|
+
client,
|
|
41
|
+
paths?.login || '/login',
|
|
42
|
+
paths?.sso || '/sso',
|
|
43
|
+
paths?.afterLogin || '/'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Handle return from standard SSO
|
|
47
|
+
ssoModule(router, client,
|
|
48
|
+
paths?.sso || '/sso',
|
|
49
|
+
codeRequestModule,
|
|
50
|
+
accessTokenModule,
|
|
51
|
+
refreshTokenModule,
|
|
52
|
+
idTokenModule);
|
|
53
|
+
|
|
54
|
+
// Remove session
|
|
55
|
+
logoutModule(router,
|
|
56
|
+
client,
|
|
57
|
+
encryptor,
|
|
58
|
+
paths?.logout || '/logout',
|
|
59
|
+
paths?.afterLogout || '/'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Check if session needs/can be refreshed
|
|
63
|
+
refreshRequestModule(router,
|
|
64
|
+
client,
|
|
65
|
+
encryptor,
|
|
66
|
+
accessTokenModule,
|
|
67
|
+
refreshTokenModule,
|
|
68
|
+
idTokenModule);
|
|
69
|
+
|
|
70
|
+
// Inject user from session cookie if present
|
|
71
|
+
userModule(router, encryptor, userMapper);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default index;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function login (router, client, loginPath, ssoPath, afterLoginPath) {
|
|
2
|
+
router.get(loginPath, (req, res) => {
|
|
3
|
+
// Set redirectTo
|
|
4
|
+
const redirectTo = req.query.ReturnUrl || afterLoginPath;
|
|
5
|
+
|
|
6
|
+
// Standard flow initiates directly
|
|
7
|
+
res.redirect(client.authorizationUrl({
|
|
8
|
+
redirect_uri: `${req.protocol}://${req.headers.host}${ssoPath}`,
|
|
9
|
+
state: redirectTo ? Buffer.from(redirectTo).toString('hex') : undefined,
|
|
10
|
+
scope: 'openid profile email offline_access groups'
|
|
11
|
+
}));
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default login;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { postForm } from '@carisls/sso-core';
|
|
2
|
+
|
|
3
|
+
function refreshRequest (router, client, encryptor, accessTokenModule, refreshTokenModule, idTokenModule) {
|
|
4
|
+
const clientId = client.metadata.client_id;
|
|
5
|
+
const clientSecret = client.metadata.client_secret || undefined;
|
|
6
|
+
const url = client.issuer.token_endpoint;
|
|
7
|
+
|
|
8
|
+
router.all('*', (req, res, next) => {
|
|
9
|
+
// Check if SSO cookie is there
|
|
10
|
+
if (req.cookies['x-session']?.length > 36 || req.token || !req.cookies['x-session-sso']) {
|
|
11
|
+
next();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
encryptor
|
|
16
|
+
.decrypt(req.cookies['x-session-sso'])
|
|
17
|
+
.then(token => postForm(url, {
|
|
18
|
+
client_id: clientId,
|
|
19
|
+
client_secret: clientSecret,
|
|
20
|
+
grant_type: 'refresh_token',
|
|
21
|
+
refresh_token: token
|
|
22
|
+
}))
|
|
23
|
+
.then(tokens => Promise.all([
|
|
24
|
+
accessTokenModule(res, tokens.access_token, req.protocol === 'https'),
|
|
25
|
+
idTokenModule(res, tokens.id_token, req.protocol === 'https'),
|
|
26
|
+
refreshTokenModule(res, tokens.refresh_token, req.protocol === 'https')
|
|
27
|
+
]))
|
|
28
|
+
.then(([{ token }, { idToken }]) => {
|
|
29
|
+
req.token = token;
|
|
30
|
+
req.idToken = idToken;
|
|
31
|
+
})
|
|
32
|
+
.catch((err) => {
|
|
33
|
+
console.error(err);
|
|
34
|
+
res.cookie('x-session-sso', 'Removing...', {
|
|
35
|
+
maxAge: 0,
|
|
36
|
+
httpOnly: true,
|
|
37
|
+
secure: req.protocol === 'https',
|
|
38
|
+
sameSite: 'lax'
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
.finally(() => {
|
|
42
|
+
next();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default refreshRequest;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { HttpError } from '@carisls/sso-core';
|
|
2
|
+
|
|
3
|
+
function sso (router, client, ssoPath, codeRequestModule, accessTokenModule, refreshTokenModule, idTokenModule) {
|
|
4
|
+
router.get(ssoPath, (req, res, next) => {
|
|
5
|
+
if (!req.query.code) {
|
|
6
|
+
next(new HttpError(500, req.query.error));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
codeRequestModule(req.query.code, `${req.protocol}://${req.headers.host}${ssoPath}`)
|
|
11
|
+
.then((tokens) => Promise.all([
|
|
12
|
+
accessTokenModule(res, tokens.access_token, req.protocol === 'https'),
|
|
13
|
+
refreshTokenModule(res, tokens.refresh_token, req.protocol === 'https'),
|
|
14
|
+
idTokenModule(res, tokens.id_token, req.protocol === 'https')
|
|
15
|
+
]))
|
|
16
|
+
.then(() => {
|
|
17
|
+
res.redirect(req.query.state ? Buffer.from(req.query.state, 'hex').toString('utf8') : '/');
|
|
18
|
+
})
|
|
19
|
+
.catch((err) => {
|
|
20
|
+
next(err);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default sso;
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@carisls/sso-standard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A middleware implementing standard flow SSO",
|
|
5
|
+
"main": "cjs/index.cjs",
|
|
6
|
+
"module": "esm/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"sso",
|
|
13
|
+
"openid",
|
|
14
|
+
"okta",
|
|
15
|
+
"keycloak"
|
|
16
|
+
],
|
|
17
|
+
"author": "Mihovil Strujic <mstrujic@carisls.com>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@carisls/sso-core": "^1.0.0",
|
|
21
|
+
"cookie-parser": "^1.4.6",
|
|
22
|
+
"express": "^4.19.2"
|
|
23
|
+
}
|
|
24
|
+
}
|