@boxyhq/saml-jackson 0.2.3 → 0.3.0-beta.247
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/Dockerfile +9 -7
- package/README.md +1 -2
- package/dist/controller/api.d.ts +32 -0
- package/dist/controller/api.js +193 -0
- package/dist/controller/error.d.ts +5 -0
- package/dist/controller/error.js +12 -0
- package/dist/controller/oauth/allowed.d.ts +1 -0
- package/dist/controller/oauth/allowed.js +17 -0
- package/dist/controller/oauth/code-verifier.d.ts +2 -0
- package/dist/controller/oauth/code-verifier.js +15 -0
- package/dist/controller/oauth/redirect.d.ts +1 -0
- package/dist/controller/oauth/redirect.js +11 -0
- package/dist/controller/oauth.d.ts +23 -0
- package/dist/controller/oauth.js +263 -0
- package/dist/controller/utils.d.ts +6 -0
- package/dist/controller/utils.js +17 -0
- package/dist/db/db.d.ts +15 -0
- package/dist/db/db.js +107 -0
- package/dist/db/encrypter.d.ts +3 -0
- package/dist/db/encrypter.js +29 -0
- package/dist/db/mem.d.ts +20 -0
- package/dist/db/mem.js +128 -0
- package/dist/db/mongo.d.ts +17 -0
- package/dist/db/mongo.js +106 -0
- package/dist/db/redis.d.ts +15 -0
- package/dist/db/redis.js +107 -0
- package/dist/db/sql/entity/JacksonIndex.d.ts +7 -0
- package/dist/db/sql/entity/JacksonIndex.js +41 -0
- package/dist/db/sql/entity/JacksonStore.d.ts +6 -0
- package/dist/db/sql/entity/JacksonStore.js +42 -0
- package/dist/db/sql/entity/JacksonTTL.d.ts +4 -0
- package/dist/db/sql/entity/JacksonTTL.js +29 -0
- package/dist/db/sql/sql.d.ts +20 -0
- package/dist/db/sql/sql.js +174 -0
- package/dist/db/store.d.ts +5 -0
- package/dist/db/store.js +68 -0
- package/dist/db/utils.d.ts +7 -0
- package/dist/db/utils.js +29 -0
- package/dist/env.d.ts +22 -0
- package/dist/env.js +35 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +82 -0
- package/dist/jackson.d.ts +1 -0
- package/dist/jackson.js +153 -0
- package/dist/read-config.d.ts +3 -0
- package/dist/read-config.js +50 -0
- package/dist/saml/claims.d.ts +6 -0
- package/dist/saml/claims.js +35 -0
- package/dist/saml/saml.d.ts +11 -0
- package/dist/saml/saml.js +200 -0
- package/dist/saml/x509.d.ts +7 -0
- package/dist/saml/x509.js +69 -0
- package/dist/typings.d.ts +137 -0
- package/dist/typings.js +2 -0
- package/package.json +41 -21
- package/.dockerignore +0 -2
- package/.eslintrc.js +0 -13
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/config.yml +0 -5
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -43
- package/.github/pull_request_template.md +0 -31
- package/.github/workflows/codesee-arch-diagram.yml +0 -81
- package/.github/workflows/main.yml +0 -123
- package/_dev/docker-compose.yml +0 -37
- package/map.js +0 -1
- package/prettier.config.js +0 -4
- package/src/controller/api.js +0 -167
- package/src/controller/error.js +0 -12
- package/src/controller/oauth/allowed.js +0 -19
- package/src/controller/oauth/code-verifier.js +0 -16
- package/src/controller/oauth/redirect.js +0 -18
- package/src/controller/oauth.js +0 -321
- package/src/controller/utils.js +0 -19
- package/src/db/db.js +0 -81
- package/src/db/db.test.js +0 -302
- package/src/db/encrypter.js +0 -36
- package/src/db/mem.js +0 -111
- package/src/db/mongo.js +0 -89
- package/src/db/redis.js +0 -88
- package/src/db/sql/entity/JacksonIndex.js +0 -42
- package/src/db/sql/entity/JacksonStore.js +0 -42
- package/src/db/sql/entity/JacksonTTL.js +0 -23
- package/src/db/sql/model/JacksonIndex.js +0 -9
- package/src/db/sql/model/JacksonStore.js +0 -10
- package/src/db/sql/model/JacksonTTL.js +0 -8
- package/src/db/sql/sql.js +0 -153
- package/src/db/store.js +0 -42
- package/src/db/utils.js +0 -30
- package/src/env.js +0 -39
- package/src/index.js +0 -67
- package/src/jackson.js +0 -161
- package/src/read-config.js +0 -24
- package/src/saml/claims.js +0 -40
- package/src/saml/saml.js +0 -223
- package/src/saml/x509.js +0 -48
- package/src/test/api.test.js +0 -186
- package/src/test/data/metadata/boxyhq.js +0 -6
- package/src/test/data/metadata/boxyhq.xml +0 -30
- package/src/test/data/saml_response +0 -1
- package/src/test/oauth.test.js +0 -342
package/src/env.js
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
const hostUrl = process.env.HOST_URL || 'localhost';
|
2
|
-
const hostPort = (process.env.HOST_PORT || '5000') * 1;
|
3
|
-
const externalUrl =
|
4
|
-
process.env.EXTERNAL_URL || 'http://' + hostUrl + ':' + hostPort;
|
5
|
-
const samlPath = process.env.SAML_PATH || '/oauth/saml';
|
6
|
-
|
7
|
-
const internalHostUrl = process.env.INTERNAL_HOST_URL || 'localhost';
|
8
|
-
const internalHostPort = (process.env.INTERNAL_HOST_PORT || '6000') * 1;
|
9
|
-
|
10
|
-
const apiKeys = (process.env.JACKSON_API_KEYS || '').split(',');
|
11
|
-
|
12
|
-
const samlAudience = process.env.SAML_AUDIENCE;
|
13
|
-
const preLoadedConfig = process.env.PRE_LOADED_CONFIG;
|
14
|
-
|
15
|
-
const idpEnabled = process.env.IDP_ENABLED;
|
16
|
-
const db = {
|
17
|
-
engine: process.env.DB_ENGINE,
|
18
|
-
url: process.env.DB_URL,
|
19
|
-
type: process.env.DB_TYPE,
|
20
|
-
ttl: process.env.DB_TTL,
|
21
|
-
encryptionKey: process.env.DB_ENCRYPTION_KEY,
|
22
|
-
};
|
23
|
-
|
24
|
-
module.exports = {
|
25
|
-
hostUrl,
|
26
|
-
hostPort,
|
27
|
-
externalUrl,
|
28
|
-
samlPath,
|
29
|
-
samlAudience,
|
30
|
-
preLoadedConfig,
|
31
|
-
internalHostUrl,
|
32
|
-
internalHostPort,
|
33
|
-
apiKeys,
|
34
|
-
idpEnabled,
|
35
|
-
db,
|
36
|
-
useInternalServer: !(
|
37
|
-
hostUrl === internalHostUrl && hostPort === internalHostPort
|
38
|
-
),
|
39
|
-
};
|
package/src/index.js
DELETED
@@ -1,67 +0,0 @@
|
|
1
|
-
const DB = require('./db/db.js');
|
2
|
-
const readConfig = require('./read-config.js');
|
3
|
-
|
4
|
-
const defaultOpts = (opts) => {
|
5
|
-
const newOpts = {
|
6
|
-
...opts,
|
7
|
-
};
|
8
|
-
|
9
|
-
if (!newOpts.externalUrl) {
|
10
|
-
throw new Error('externalUrl is required');
|
11
|
-
}
|
12
|
-
if (!newOpts.samlPath) {
|
13
|
-
throw new Error('samlPath is required');
|
14
|
-
}
|
15
|
-
|
16
|
-
newOpts.samlAudience = newOpts.samlAudience || 'https://saml.boxyhq.com';
|
17
|
-
newOpts.preLoadedConfig = newOpts.preLoadedConfig || ''; // path to folder containing static SAML config that will be preloaded. This is useful for self-hosted deployments that only have to support a single tenant (or small number of known tenants).
|
18
|
-
newOpts.idpEnabled = newOpts.idpEnabled === true;
|
19
|
-
newOpts.db = newOpts.db || {};
|
20
|
-
newOpts.db.engine = newOpts.db.engine || 'sql'; // Supported values: redis, sql, mongo, mem. Keep comment in sync with db.js
|
21
|
-
newOpts.db.url =
|
22
|
-
newOpts.db.url || 'postgresql://postgres:postgres@localhost:5432/postgres';
|
23
|
-
newOpts.db.type = newOpts.db.type || 'postgres'; // Only needed if DB_ENGINE is sql. Supported values: postgres, cockroachdb, mysql, mariadb
|
24
|
-
newOpts.db.ttl = (newOpts.db.ttl || 300) * 1; // TTL for the code, session and token stores (in seconds)
|
25
|
-
newOpts.db.cleanupLimit = (newOpts.db.cleanupLimit || 1000) * 1; // Limit cleanup of TTL entries to this many items at a time
|
26
|
-
|
27
|
-
return newOpts;
|
28
|
-
};
|
29
|
-
|
30
|
-
module.exports = async function (opts) {
|
31
|
-
opts = defaultOpts(opts);
|
32
|
-
|
33
|
-
const db = await DB.new(opts.db);
|
34
|
-
const configStore = db.store('saml:config');
|
35
|
-
const sessionStore = db.store('oauth:session', opts.db.ttl);
|
36
|
-
const codeStore = db.store('oauth:code', opts.db.ttl);
|
37
|
-
const tokenStore = db.store('oauth:token', opts.db.ttl);
|
38
|
-
|
39
|
-
const apiController = require('./controller/api.js')({ configStore });
|
40
|
-
const oauthController = require('./controller/oauth.js')({
|
41
|
-
configStore,
|
42
|
-
sessionStore,
|
43
|
-
codeStore,
|
44
|
-
tokenStore,
|
45
|
-
opts,
|
46
|
-
});
|
47
|
-
// write pre-loaded config if present
|
48
|
-
if (opts.preLoadedConfig && opts.preLoadedConfig.length > 0) {
|
49
|
-
const configs = await readConfig(opts.preLoadedConfig);
|
50
|
-
|
51
|
-
for (const config of configs) {
|
52
|
-
await apiController.config(config);
|
53
|
-
console.log(
|
54
|
-
`loaded config for tenant "${config.tenant}" and product "${config.product}"`
|
55
|
-
);
|
56
|
-
}
|
57
|
-
}
|
58
|
-
|
59
|
-
const type =
|
60
|
-
opts.db.engine === 'sql' && opts.db.type ? ' Type: ' + opts.db.type : '';
|
61
|
-
console.log(`Using engine: ${opts.db.engine}.${type}`);
|
62
|
-
|
63
|
-
return {
|
64
|
-
apiController,
|
65
|
-
oauthController,
|
66
|
-
};
|
67
|
-
};
|
package/src/jackson.js
DELETED
@@ -1,161 +0,0 @@
|
|
1
|
-
const express = require('express');
|
2
|
-
const cors = require('cors');
|
3
|
-
|
4
|
-
const env = require('./env.js');
|
5
|
-
const { extractAuthToken } = require('./controller/utils.js');
|
6
|
-
|
7
|
-
let apiController;
|
8
|
-
let oauthController;
|
9
|
-
|
10
|
-
const oauthPath = '/oauth';
|
11
|
-
const apiPath = '/api/v1/saml';
|
12
|
-
|
13
|
-
const app = express();
|
14
|
-
|
15
|
-
app.use(express.json());
|
16
|
-
app.use(express.urlencoded({ extended: true }));
|
17
|
-
|
18
|
-
app.get(oauthPath + '/authorize', async (req, res) => {
|
19
|
-
try {
|
20
|
-
const { redirect_url } = await oauthController.authorize(req.query);
|
21
|
-
|
22
|
-
res.redirect(redirect_url);
|
23
|
-
} catch (err) {
|
24
|
-
const { message, statusCode = 500 } = err;
|
25
|
-
|
26
|
-
res.status(statusCode).send(message);
|
27
|
-
}
|
28
|
-
});
|
29
|
-
|
30
|
-
app.post(env.samlPath, async (req, res) => {
|
31
|
-
try {
|
32
|
-
const { redirect_url } = await oauthController.samlResponse(req.body);
|
33
|
-
|
34
|
-
res.redirect(redirect_url);
|
35
|
-
} catch (err) {
|
36
|
-
const { message, statusCode = 500 } = err;
|
37
|
-
|
38
|
-
res.status(statusCode).send(message);
|
39
|
-
}
|
40
|
-
});
|
41
|
-
|
42
|
-
app.post(oauthPath + '/token', cors(), async (req, res) => {
|
43
|
-
try {
|
44
|
-
const result = await oauthController.token(req.body);
|
45
|
-
|
46
|
-
res.json(result);
|
47
|
-
} catch (err) {
|
48
|
-
const { message, statusCode = 500 } = err;
|
49
|
-
|
50
|
-
res.status(statusCode).send(message);
|
51
|
-
}
|
52
|
-
});
|
53
|
-
|
54
|
-
app.get(oauthPath + '/userinfo', async (req, res) => {
|
55
|
-
try {
|
56
|
-
let token = extractAuthToken(req);
|
57
|
-
|
58
|
-
// check for query param
|
59
|
-
if (!token) {
|
60
|
-
token = req.query.access_token;
|
61
|
-
}
|
62
|
-
|
63
|
-
if (!token) {
|
64
|
-
res.status(401).json({ message: 'Unauthorized' });
|
65
|
-
}
|
66
|
-
|
67
|
-
const profile = await oauthController.userInfo(token);
|
68
|
-
|
69
|
-
res.json(profile);
|
70
|
-
} catch (err) {
|
71
|
-
const { message, statusCode = 500 } = err;
|
72
|
-
|
73
|
-
res.status(statusCode).json({ message });
|
74
|
-
}
|
75
|
-
});
|
76
|
-
|
77
|
-
const server = app.listen(env.hostPort, async () => {
|
78
|
-
console.log(
|
79
|
-
`🚀 The path of the righteous server: http://${env.hostUrl}:${env.hostPort}`
|
80
|
-
);
|
81
|
-
|
82
|
-
const ret = await require('./index.js')(env);
|
83
|
-
apiController = ret.apiController;
|
84
|
-
oauthController = ret.oauthController;
|
85
|
-
});
|
86
|
-
|
87
|
-
// Internal routes, recommended not to expose this to the public interface though it would be guarded by API key(s)
|
88
|
-
let internalApp = app;
|
89
|
-
|
90
|
-
if (env.useInternalServer) {
|
91
|
-
internalApp = express();
|
92
|
-
|
93
|
-
internalApp.use(express.json());
|
94
|
-
internalApp.use(express.urlencoded({ extended: true }));
|
95
|
-
}
|
96
|
-
|
97
|
-
const validateApiKey = (token) => {
|
98
|
-
return env.apiKeys.includes(token);
|
99
|
-
};
|
100
|
-
|
101
|
-
internalApp.post(apiPath + '/config', async (req, res) => {
|
102
|
-
try {
|
103
|
-
const apiKey = extractAuthToken(req);
|
104
|
-
if (!validateApiKey(apiKey)) {
|
105
|
-
res.status(401).send('Unauthorized');
|
106
|
-
return;
|
107
|
-
}
|
108
|
-
|
109
|
-
res.json(await apiController.config(req.body));
|
110
|
-
} catch (err) {
|
111
|
-
res.status(500).json({
|
112
|
-
error: err.message,
|
113
|
-
});
|
114
|
-
}
|
115
|
-
});
|
116
|
-
|
117
|
-
internalApp.get(apiPath + '/config', async (req, res) => {
|
118
|
-
try {
|
119
|
-
const apiKey = extractAuthToken(req);
|
120
|
-
if (!validateApiKey(apiKey)) {
|
121
|
-
res.status(401).send('Unauthorized');
|
122
|
-
return;
|
123
|
-
}
|
124
|
-
|
125
|
-
res.json(await apiController.getConfig(req.query));
|
126
|
-
} catch (err) {
|
127
|
-
res.status(500).json({
|
128
|
-
error: err.message,
|
129
|
-
});
|
130
|
-
}
|
131
|
-
});
|
132
|
-
|
133
|
-
internalApp.delete(apiPath + '/config', async (req, res) => {
|
134
|
-
try {
|
135
|
-
const apiKey = extractAuthToken(req);
|
136
|
-
if (!validateApiKey(apiKey)) {
|
137
|
-
res.status(401).send('Unauthorized');
|
138
|
-
return;
|
139
|
-
}
|
140
|
-
await apiController.deleteConfig(req.body);
|
141
|
-
res.status(200).end();
|
142
|
-
} catch (err) {
|
143
|
-
res.status(500).json({
|
144
|
-
error: err.message,
|
145
|
-
});
|
146
|
-
}
|
147
|
-
});
|
148
|
-
|
149
|
-
let internalServer = server;
|
150
|
-
if (env.useInternalServer) {
|
151
|
-
internalServer = internalApp.listen(env.internalHostPort, async () => {
|
152
|
-
console.log(
|
153
|
-
`🚀 The path of the righteous internal server: http://${env.internalHostUrl}:${env.internalHostPort}`
|
154
|
-
);
|
155
|
-
});
|
156
|
-
}
|
157
|
-
|
158
|
-
module.exports = {
|
159
|
-
server,
|
160
|
-
internalServer,
|
161
|
-
};
|
package/src/read-config.js
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
const fs = require('fs');
|
2
|
-
const path = require('path');
|
3
|
-
|
4
|
-
module.exports = async function (preLoadedConfig) {
|
5
|
-
if (preLoadedConfig.startsWith('./')) {
|
6
|
-
preLoadedConfig = path.resolve(process.cwd(), preLoadedConfig);
|
7
|
-
}
|
8
|
-
const files = await fs.promises.readdir(preLoadedConfig);
|
9
|
-
const configs = [];
|
10
|
-
for (let idx in files) {
|
11
|
-
const file = files[idx];
|
12
|
-
if (file.endsWith('.js')) {
|
13
|
-
const config = require(path.join(preLoadedConfig, file));
|
14
|
-
const rawMetadata = await fs.promises.readFile(
|
15
|
-
path.join(preLoadedConfig, path.parse(file).name + '.xml'),
|
16
|
-
'utf8'
|
17
|
-
);
|
18
|
-
config.rawMetadata = rawMetadata;
|
19
|
-
configs.push(config);
|
20
|
-
}
|
21
|
-
}
|
22
|
-
|
23
|
-
return configs;
|
24
|
-
};
|
package/src/saml/claims.js
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
const mapping = [
|
2
|
-
{
|
3
|
-
attribute: 'id',
|
4
|
-
schema:
|
5
|
-
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier',
|
6
|
-
},
|
7
|
-
{
|
8
|
-
attribute: 'email',
|
9
|
-
schema:
|
10
|
-
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
|
11
|
-
},
|
12
|
-
{
|
13
|
-
attribute: 'firstName',
|
14
|
-
schema: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
|
15
|
-
},
|
16
|
-
{
|
17
|
-
attribute: 'lastName',
|
18
|
-
schema: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
|
19
|
-
},
|
20
|
-
];
|
21
|
-
|
22
|
-
const map = (claims) => {
|
23
|
-
const profile = {
|
24
|
-
raw: claims,
|
25
|
-
};
|
26
|
-
|
27
|
-
mapping.forEach((m) => {
|
28
|
-
if (claims[m.attribute]) {
|
29
|
-
profile[m.attribute] = claims[m.attribute];
|
30
|
-
} else if (claims[m.schema]) {
|
31
|
-
profile[m.attribute] = claims[m.schema];
|
32
|
-
}
|
33
|
-
});
|
34
|
-
|
35
|
-
return profile;
|
36
|
-
};
|
37
|
-
|
38
|
-
module.exports = {
|
39
|
-
map,
|
40
|
-
};
|
package/src/saml/saml.js
DELETED
@@ -1,223 +0,0 @@
|
|
1
|
-
const saml = require('@boxyhq/saml20');
|
2
|
-
const xml2js = require('xml2js');
|
3
|
-
const rambda = require('rambda');
|
4
|
-
const thumbprint = require('thumbprint');
|
5
|
-
const xmlbuilder = require('xmlbuilder');
|
6
|
-
const crypto = require('crypto');
|
7
|
-
const xmlcrypto = require('xml-crypto');
|
8
|
-
const claims = require('./claims');
|
9
|
-
|
10
|
-
const idPrefix = '_';
|
11
|
-
const authnXPath =
|
12
|
-
'/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]';
|
13
|
-
const issuerXPath =
|
14
|
-
'/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:assertion"]';
|
15
|
-
|
16
|
-
const signRequest = (xml, signingKey) => {
|
17
|
-
if (!xml) {
|
18
|
-
throw new Error('Please specify xml');
|
19
|
-
}
|
20
|
-
if (!signingKey) {
|
21
|
-
throw new Error('Please specify signingKey');
|
22
|
-
}
|
23
|
-
|
24
|
-
const sig = new xmlcrypto.SignedXml();
|
25
|
-
sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
|
26
|
-
sig.signingKey = signingKey;
|
27
|
-
sig.addReference(
|
28
|
-
authnXPath,
|
29
|
-
[
|
30
|
-
'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
|
31
|
-
'http://www.w3.org/2001/10/xml-exc-c14n#',
|
32
|
-
],
|
33
|
-
'http://www.w3.org/2001/04/xmlenc#sha256'
|
34
|
-
);
|
35
|
-
sig.computeSignature(xml, {
|
36
|
-
location: { reference: authnXPath + issuerXPath, action: 'after' },
|
37
|
-
});
|
38
|
-
|
39
|
-
return sig.getSignedXml();
|
40
|
-
};
|
41
|
-
|
42
|
-
module.exports = {
|
43
|
-
request: ({
|
44
|
-
ssoUrl,
|
45
|
-
entityID,
|
46
|
-
callbackUrl,
|
47
|
-
isPassive = false,
|
48
|
-
forceAuthn = false,
|
49
|
-
identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
50
|
-
providerName = 'BoxyHQ',
|
51
|
-
signingKey,
|
52
|
-
}) => {
|
53
|
-
const id = idPrefix + crypto.randomBytes(10).toString('hex');
|
54
|
-
const date = new Date().toISOString();
|
55
|
-
|
56
|
-
const samlReq = {
|
57
|
-
'samlp:AuthnRequest': {
|
58
|
-
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
59
|
-
'@ID': id,
|
60
|
-
'@Version': '2.0',
|
61
|
-
'@IssueInstant': date,
|
62
|
-
'@ProtocolBinding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
63
|
-
'@Destination': ssoUrl,
|
64
|
-
'saml:Issuer': {
|
65
|
-
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
|
66
|
-
'#text': entityID,
|
67
|
-
},
|
68
|
-
},
|
69
|
-
};
|
70
|
-
|
71
|
-
if (isPassive) samlReq['samlp:AuthnRequest']['@IsPassive'] = true;
|
72
|
-
|
73
|
-
if (forceAuthn) {
|
74
|
-
samlReq['samlp:AuthnRequest']['@ForceAuthn'] = true;
|
75
|
-
}
|
76
|
-
|
77
|
-
samlReq['samlp:AuthnRequest']['@AssertionConsumerServiceURL'] = callbackUrl;
|
78
|
-
|
79
|
-
samlReq['samlp:AuthnRequest']['samlp:NameIDPolicy'] = {
|
80
|
-
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
81
|
-
'@Format': identifierFormat,
|
82
|
-
'@AllowCreate': 'true',
|
83
|
-
};
|
84
|
-
|
85
|
-
if (providerName != null) {
|
86
|
-
samlReq['samlp:AuthnRequest']['@ProviderName'] = providerName;
|
87
|
-
}
|
88
|
-
|
89
|
-
let xml = xmlbuilder.create(samlReq).end({});
|
90
|
-
if (signingKey) {
|
91
|
-
xml = signRequest(xml, signingKey);
|
92
|
-
}
|
93
|
-
|
94
|
-
return {
|
95
|
-
id,
|
96
|
-
request: xml,
|
97
|
-
};
|
98
|
-
},
|
99
|
-
|
100
|
-
parseAsync: async (rawAssertion) => {
|
101
|
-
return new Promise((resolve, reject) => {
|
102
|
-
saml.parse(rawAssertion, function onParseAsync(err, profile) {
|
103
|
-
if (err) {
|
104
|
-
reject(err);
|
105
|
-
return;
|
106
|
-
}
|
107
|
-
|
108
|
-
resolve(profile);
|
109
|
-
});
|
110
|
-
});
|
111
|
-
},
|
112
|
-
|
113
|
-
validateAsync: async (rawAssertion, options) => {
|
114
|
-
return new Promise((resolve, reject) => {
|
115
|
-
saml.validate(
|
116
|
-
rawAssertion,
|
117
|
-
options,
|
118
|
-
function onValidateAsync(err, profile) {
|
119
|
-
if (err) {
|
120
|
-
reject(err);
|
121
|
-
return;
|
122
|
-
}
|
123
|
-
|
124
|
-
if (profile && profile.claims) {
|
125
|
-
// we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
|
126
|
-
profile.claims = claims.map(profile.claims);
|
127
|
-
|
128
|
-
// some providers don't return the id in the assertion, we set it to a sha256 hash of the email
|
129
|
-
if (!profile.claims.id) {
|
130
|
-
profile.claims.id = crypto
|
131
|
-
.createHash('sha256')
|
132
|
-
.update(profile.claims.email)
|
133
|
-
.digest('hex');
|
134
|
-
}
|
135
|
-
}
|
136
|
-
|
137
|
-
resolve(profile);
|
138
|
-
}
|
139
|
-
);
|
140
|
-
});
|
141
|
-
},
|
142
|
-
|
143
|
-
parseMetadataAsync: async (idpMeta) => {
|
144
|
-
return new Promise((resolve, reject) => {
|
145
|
-
xml2js.parseString(
|
146
|
-
idpMeta,
|
147
|
-
{ tagNameProcessors: [xml2js.processors.stripPrefix] },
|
148
|
-
(err, res) => {
|
149
|
-
if (err) {
|
150
|
-
reject(err);
|
151
|
-
return;
|
152
|
-
}
|
153
|
-
|
154
|
-
const entityID = rambda.path('EntityDescriptor.$.entityID', res);
|
155
|
-
let X509Certificate = null;
|
156
|
-
let ssoPostUrl = null;
|
157
|
-
let ssoRedirectUrl = null;
|
158
|
-
let loginType = 'idp';
|
159
|
-
|
160
|
-
let ssoDes = rambda.pathOr(
|
161
|
-
null,
|
162
|
-
'EntityDescriptor.IDPSSODescriptor',
|
163
|
-
res
|
164
|
-
);
|
165
|
-
if (!ssoDes) {
|
166
|
-
ssoDes = rambda.pathOr([], 'EntityDescriptor.SPSSODescriptor', res);
|
167
|
-
if (!ssoDes) {
|
168
|
-
loginType = 'sp';
|
169
|
-
}
|
170
|
-
}
|
171
|
-
|
172
|
-
for (const ssoDesRec of ssoDes) {
|
173
|
-
const keyDes = ssoDesRec['KeyDescriptor'];
|
174
|
-
for (const keyDesRec of keyDes) {
|
175
|
-
if (keyDesRec['$'] && keyDesRec['$'].use === 'signing') {
|
176
|
-
const ki = keyDesRec['KeyInfo'][0];
|
177
|
-
const cd = ki['X509Data'][0];
|
178
|
-
X509Certificate = cd['X509Certificate'][0];
|
179
|
-
}
|
180
|
-
}
|
181
|
-
|
182
|
-
const ssoSvc =
|
183
|
-
ssoDesRec['SingleSignOnService'] ||
|
184
|
-
ssoDesRec['AssertionConsumerService'] ||
|
185
|
-
[];
|
186
|
-
for (const ssoSvcRec of ssoSvc) {
|
187
|
-
if (
|
188
|
-
rambda.pathOr('', '$.Binding', ssoSvcRec).endsWith('HTTP-POST')
|
189
|
-
) {
|
190
|
-
ssoPostUrl = rambda.path('$.Location', ssoSvcRec);
|
191
|
-
} else if (
|
192
|
-
rambda
|
193
|
-
.pathOr('', '$.Binding', ssoSvcRec)
|
194
|
-
.endsWith('HTTP-Redirect')
|
195
|
-
) {
|
196
|
-
ssoRedirectUrl = rambda.path('$.Location', ssoSvcRec);
|
197
|
-
}
|
198
|
-
}
|
199
|
-
}
|
200
|
-
|
201
|
-
const ret = {
|
202
|
-
sso: {},
|
203
|
-
};
|
204
|
-
if (entityID) {
|
205
|
-
ret.entityID = entityID;
|
206
|
-
}
|
207
|
-
if (X509Certificate) {
|
208
|
-
ret.thumbprint = thumbprint.calculate(X509Certificate);
|
209
|
-
}
|
210
|
-
if (ssoPostUrl) {
|
211
|
-
ret.sso.postUrl = ssoPostUrl;
|
212
|
-
}
|
213
|
-
if (ssoRedirectUrl) {
|
214
|
-
ret.sso.redirectUrl = ssoRedirectUrl;
|
215
|
-
}
|
216
|
-
ret.loginType = loginType;
|
217
|
-
|
218
|
-
resolve(ret);
|
219
|
-
}
|
220
|
-
);
|
221
|
-
});
|
222
|
-
},
|
223
|
-
};
|
package/src/saml/x509.js
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
const x509 = require('@peculiar/x509');
|
2
|
-
const { Crypto } = require('@peculiar/webcrypto');
|
3
|
-
|
4
|
-
const crypto = new Crypto();
|
5
|
-
x509.cryptoProvider.set(crypto);
|
6
|
-
|
7
|
-
const alg = {
|
8
|
-
name: 'RSASSA-PKCS1-v1_5',
|
9
|
-
hash: 'SHA-256',
|
10
|
-
publicExponent: new Uint8Array([1, 0, 1]),
|
11
|
-
modulusLength: 2048,
|
12
|
-
};
|
13
|
-
|
14
|
-
const generate = async () => {
|
15
|
-
const keys = await crypto.subtle.generateKey(alg, true, ['sign', 'verify']);
|
16
|
-
|
17
|
-
const extensions = [
|
18
|
-
new x509.BasicConstraintsExtension(false, undefined, true),
|
19
|
-
];
|
20
|
-
|
21
|
-
extensions.push(
|
22
|
-
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true)
|
23
|
-
);
|
24
|
-
extensions.push(
|
25
|
-
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
|
26
|
-
);
|
27
|
-
|
28
|
-
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
29
|
-
serialNumber: '01',
|
30
|
-
name: 'CN=BoxyHQ Jackson',
|
31
|
-
notBefore: new Date(),
|
32
|
-
notAfter: new Date('3021/01/01'), // TODO: set shorter expiry and rotate ceritifcates
|
33
|
-
signingAlgorithm: alg,
|
34
|
-
keys: keys,
|
35
|
-
extensions,
|
36
|
-
});
|
37
|
-
|
38
|
-
const pkcs8 = await crypto.subtle.exportKey('pkcs8', keys.privateKey);
|
39
|
-
|
40
|
-
return {
|
41
|
-
publicKey: cert.toString('pem'),
|
42
|
-
privateKey: x509.PemConverter.encode(pkcs8, 'private key'),
|
43
|
-
};
|
44
|
-
};
|
45
|
-
|
46
|
-
module.exports = {
|
47
|
-
generate,
|
48
|
-
};
|