@boxyhq/saml-jackson 0.2.3-beta.210 → 0.2.3-beta.222
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/.eslintrc.js +13 -0
- package/package.json +2 -2
- package/prettier.config.js +4 -0
- package/src/controller/api.ts +225 -0
- package/src/controller/error.ts +13 -0
- package/src/controller/oauth/allowed.ts +22 -0
- package/src/controller/oauth/code-verifier.ts +11 -0
- package/src/controller/oauth/redirect.ts +12 -0
- package/src/controller/oauth.ts +337 -0
- package/src/controller/utils.ts +17 -0
- package/src/db/db.ts +100 -0
- package/src/db/encrypter.ts +38 -0
- package/src/db/mem.ts +128 -0
- package/src/db/mongo.ts +110 -0
- package/src/db/redis.ts +103 -0
- package/src/db/sql/entity/JacksonIndex.ts +44 -0
- package/src/db/sql/entity/JacksonStore.ts +43 -0
- package/src/db/sql/entity/JacksonTTL.ts +17 -0
- package/src/db/sql/model/JacksonIndex.ts +3 -0
- package/src/db/sql/model/JacksonStore.ts +8 -0
- package/src/db/sql/sql.ts +184 -0
- package/src/db/store.ts +49 -0
- package/src/db/utils.ts +26 -0
- package/src/env.ts +42 -0
- package/src/index.ts +79 -0
- package/src/jackson.ts +171 -0
- package/src/read-config.ts +29 -0
- package/src/saml/claims.ts +41 -0
- package/src/saml/saml.ts +234 -0
- package/src/saml/x509.ts +51 -0
- package/src/test/api.test.ts +271 -0
- package/src/test/data/metadata/boxyhq.js +6 -0
- package/src/test/data/metadata/boxyhq.xml +30 -0
- package/src/test/data/saml_response +1 -0
- package/src/test/db.test.ts +313 -0
- package/src/test/oauth.test.ts +353 -0
- package/src/typings.ts +167 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +26 -0
- package/.nyc_output/36a3e9e1-42eb-468d-a9ec-8d206fedcd3e.json +0 -1
- package/.nyc_output/8c0af85a-b807-45bb-8331-20c3aabe15df.json +0 -1
- package/.nyc_output/ad148b90-7401-4df2-959f-3fdcf81a06ec.json +0 -1
- package/.nyc_output/processinfo/36a3e9e1-42eb-468d-a9ec-8d206fedcd3e.json +0 -1
- package/.nyc_output/processinfo/8c0af85a-b807-45bb-8331-20c3aabe15df.json +0 -1
- package/.nyc_output/processinfo/ad148b90-7401-4df2-959f-3fdcf81a06ec.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
@@ -0,0 +1,184 @@
|
|
1
|
+
/*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
|
2
|
+
|
3
|
+
require('reflect-metadata');
|
4
|
+
|
5
|
+
import { DatabaseDriver, DatabaseOption, Index } from 'saml-jackson';
|
6
|
+
import { Connection, createConnection } from 'typeorm';
|
7
|
+
import * as dbutils from '../utils';
|
8
|
+
import { JacksonIndex } from './model/JacksonIndex';
|
9
|
+
import { JacksonStore } from './model/JacksonStore';
|
10
|
+
import { JacksonTTL } from './entity/JacksonTTL';
|
11
|
+
|
12
|
+
import JacksonStoreEntity from './entity/JacksonStore';
|
13
|
+
import JacksonIndexEntity from './entity/JacksonIndex';
|
14
|
+
import { JacksonTTL as JacksonTTLEntity } from './entity/JacksonTTL';
|
15
|
+
|
16
|
+
class Sql implements DatabaseDriver {
|
17
|
+
private options: DatabaseOption;
|
18
|
+
private connection!: Connection;
|
19
|
+
private storeRepository; //!: typeorm.Repository<JacksonStore>;
|
20
|
+
private indexRepository; //!: typeorm.Repository<JacksonIndex>;
|
21
|
+
private ttlRepository; //!: typeorm.Repository<JacksonTTL>;
|
22
|
+
private ttlCleanup;
|
23
|
+
private timerId;
|
24
|
+
|
25
|
+
constructor(options: DatabaseOption) {
|
26
|
+
this.options = options;
|
27
|
+
}
|
28
|
+
|
29
|
+
async init(): Promise<Sql> {
|
30
|
+
while (true) {
|
31
|
+
try {
|
32
|
+
// TODO: Fix it
|
33
|
+
// @ts-ignore
|
34
|
+
this.connection = await createConnection({
|
35
|
+
name: this.options.type + Math.floor(Math.random() * 100000),
|
36
|
+
type: this.options.type,
|
37
|
+
url: this.options.url,
|
38
|
+
synchronize: true,
|
39
|
+
migrationsTableName: '_jackson_migrations',
|
40
|
+
logging: false,
|
41
|
+
entities: [
|
42
|
+
JacksonStoreEntity(this.options.type),
|
43
|
+
JacksonIndexEntity,
|
44
|
+
JacksonTTLEntity,
|
45
|
+
],
|
46
|
+
});
|
47
|
+
|
48
|
+
break;
|
49
|
+
} catch (err) {
|
50
|
+
console.error(`error connecting to ${this.options.type} db: ${err}`);
|
51
|
+
await dbutils.sleep(1000);
|
52
|
+
continue;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
this.storeRepository = this.connection.getRepository(JacksonStore);
|
57
|
+
this.indexRepository = this.connection.getRepository(JacksonIndex);
|
58
|
+
this.ttlRepository = this.connection.getRepository(JacksonTTL);
|
59
|
+
|
60
|
+
if (this.options.ttl && this.options.cleanupLimit) {
|
61
|
+
this.ttlCleanup = async () => {
|
62
|
+
const now = Date.now();
|
63
|
+
|
64
|
+
while (true) {
|
65
|
+
const ids = await this.ttlRepository
|
66
|
+
.createQueryBuilder('jackson_ttl')
|
67
|
+
.limit(this.options.cleanupLimit)
|
68
|
+
.where('jackson_ttl.expiresAt <= :expiresAt', {
|
69
|
+
expiresAt: now,
|
70
|
+
})
|
71
|
+
.getMany();
|
72
|
+
|
73
|
+
if (ids.length <= 0) {
|
74
|
+
break;
|
75
|
+
}
|
76
|
+
|
77
|
+
const delIds = ids.map((id) => {
|
78
|
+
return id.key;
|
79
|
+
});
|
80
|
+
|
81
|
+
await this.storeRepository.remove(ids);
|
82
|
+
await this.ttlRepository.delete(delIds);
|
83
|
+
}
|
84
|
+
|
85
|
+
this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
|
86
|
+
};
|
87
|
+
|
88
|
+
this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
|
89
|
+
} else {
|
90
|
+
console.log(
|
91
|
+
'Warning: ttl cleanup not enabled, set both "ttl" and "cleanupLimit" options to enable it!'
|
92
|
+
);
|
93
|
+
}
|
94
|
+
|
95
|
+
return this;
|
96
|
+
}
|
97
|
+
|
98
|
+
async get(namespace: string, key: string): Promise<any> {
|
99
|
+
let res = await this.storeRepository.findOne({
|
100
|
+
key: dbutils.key(namespace, key),
|
101
|
+
});
|
102
|
+
|
103
|
+
if (res && res.value) {
|
104
|
+
return {
|
105
|
+
value: res.value,
|
106
|
+
iv: res.iv,
|
107
|
+
tag: res.tag,
|
108
|
+
};
|
109
|
+
}
|
110
|
+
|
111
|
+
return null;
|
112
|
+
}
|
113
|
+
|
114
|
+
async getByIndex(namespace: string, idx: Index): Promise<any> {
|
115
|
+
const res = await this.indexRepository.find({
|
116
|
+
key: dbutils.keyForIndex(namespace, idx),
|
117
|
+
});
|
118
|
+
|
119
|
+
const ret: string[] = [];
|
120
|
+
|
121
|
+
if (res) {
|
122
|
+
res.forEach((r) => {
|
123
|
+
// @ts-ignore
|
124
|
+
ret.push({
|
125
|
+
value: r.store.value,
|
126
|
+
iv: r.store.iv,
|
127
|
+
tag: r.store.tag,
|
128
|
+
});
|
129
|
+
});
|
130
|
+
}
|
131
|
+
|
132
|
+
return ret;
|
133
|
+
}
|
134
|
+
|
135
|
+
async put(
|
136
|
+
namespace: string,
|
137
|
+
key: string,
|
138
|
+
val: string,
|
139
|
+
ttl: number = 0,
|
140
|
+
...indexes: any[]
|
141
|
+
): Promise<void> {
|
142
|
+
await this.connection.transaction(async (transactionalEntityManager) => {
|
143
|
+
const dbKey = dbutils.key(namespace, key);
|
144
|
+
|
145
|
+
// @ts-ignore
|
146
|
+
const store = new JacksonStore(dbKey, val.value, val.iv, val.tag);
|
147
|
+
|
148
|
+
await transactionalEntityManager.save(store);
|
149
|
+
|
150
|
+
if (ttl) {
|
151
|
+
const ttlRec = new JacksonTTL();
|
152
|
+
ttlRec.key = dbKey;
|
153
|
+
ttlRec.expiresAt = Date.now() + ttl * 1000;
|
154
|
+
await transactionalEntityManager.save(ttlRec);
|
155
|
+
}
|
156
|
+
|
157
|
+
// no ttl support for secondary indexes
|
158
|
+
for (const idx of indexes || []) {
|
159
|
+
const key = dbutils.keyForIndex(namespace, idx);
|
160
|
+
const rec = await this.indexRepository.findOne({
|
161
|
+
key,
|
162
|
+
storeKey: store.key,
|
163
|
+
});
|
164
|
+
if (!rec) {
|
165
|
+
await transactionalEntityManager.save(
|
166
|
+
new JacksonIndex(0, key, store)
|
167
|
+
);
|
168
|
+
}
|
169
|
+
}
|
170
|
+
});
|
171
|
+
}
|
172
|
+
|
173
|
+
async delete(namespace: string, key: string): Promise<any> {
|
174
|
+
return await this.storeRepository.remove({
|
175
|
+
key: dbutils.key(namespace, key),
|
176
|
+
});
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
export default {
|
181
|
+
new: async (options: DatabaseOption): Promise<Sql> => {
|
182
|
+
return await new Sql(options).init();
|
183
|
+
},
|
184
|
+
};
|
package/src/db/store.ts
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
import { Index, Storable } from 'saml-jackson';
|
2
|
+
import * as dbutils from './utils';
|
3
|
+
|
4
|
+
class Store implements Storable {
|
5
|
+
private namespace: string;
|
6
|
+
private db: any;
|
7
|
+
private ttl: number;
|
8
|
+
|
9
|
+
constructor(namespace: string, db: any, ttl: number = 0) {
|
10
|
+
this.namespace = namespace;
|
11
|
+
this.db = db;
|
12
|
+
this.ttl = ttl;
|
13
|
+
}
|
14
|
+
|
15
|
+
async get(key: string): Promise<any> {
|
16
|
+
return await this.db.get(this.namespace, dbutils.keyDigest(key));
|
17
|
+
}
|
18
|
+
|
19
|
+
async getByIndex(idx: Index): Promise<any> {
|
20
|
+
idx.value = dbutils.keyDigest(idx.value);
|
21
|
+
|
22
|
+
return await this.db.getByIndex(this.namespace, idx);
|
23
|
+
}
|
24
|
+
|
25
|
+
async put(key: string, val: any, ...indexes: Index[]): Promise<any> {
|
26
|
+
indexes = (indexes || []).map((idx) => {
|
27
|
+
idx.value = dbutils.keyDigest(idx.value);
|
28
|
+
return idx;
|
29
|
+
});
|
30
|
+
|
31
|
+
return await this.db.put(
|
32
|
+
this.namespace,
|
33
|
+
dbutils.keyDigest(key),
|
34
|
+
val,
|
35
|
+
this.ttl,
|
36
|
+
...indexes
|
37
|
+
);
|
38
|
+
}
|
39
|
+
|
40
|
+
async delete(key: string): Promise<any> {
|
41
|
+
return await this.db.delete(this.namespace, dbutils.keyDigest(key));
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
export default {
|
46
|
+
new: (namespace: string, db: any, ttl: number = 0): Storable => {
|
47
|
+
return new Store(namespace, db, ttl);
|
48
|
+
},
|
49
|
+
};
|
package/src/db/utils.ts
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
import Ripemd160 from 'ripemd160';
|
2
|
+
import { Index } from 'saml-jackson';
|
3
|
+
|
4
|
+
export const key = (namespace: string, k: string): string => {
|
5
|
+
return namespace + ':' + k;
|
6
|
+
};
|
7
|
+
|
8
|
+
export const keyForIndex = (namespace: string, idx: Index): string => {
|
9
|
+
return key(key(namespace, idx.name), idx.value);
|
10
|
+
};
|
11
|
+
|
12
|
+
export const keyDigest = (k: string): string => {
|
13
|
+
return new Ripemd160().update(k).digest('hex');
|
14
|
+
};
|
15
|
+
|
16
|
+
export const keyFromParts = (...parts: string[]): string => {
|
17
|
+
// TODO: pick a better strategy, keys can collide now
|
18
|
+
|
19
|
+
return parts.join(':');
|
20
|
+
};
|
21
|
+
|
22
|
+
export const sleep = (ms: number): Promise<void> => {
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
24
|
+
};
|
25
|
+
|
26
|
+
export const indexPrefix = '_index';
|
package/src/env.ts
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
const hostUrl = process.env.HOST_URL || 'localhost';
|
2
|
+
const hostPort = +(process.env.HOST_PORT || '5000');
|
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');
|
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
|
+
|
17
|
+
const db = {
|
18
|
+
engine: process.env.DB_ENGINE,
|
19
|
+
url: process.env.DB_URL,
|
20
|
+
type: process.env.DB_TYPE,
|
21
|
+
ttl: process.env.DB_TTL,
|
22
|
+
encryptionKey: process.env.DB_ENCRYPTION_KEY,
|
23
|
+
};
|
24
|
+
|
25
|
+
const env = {
|
26
|
+
hostUrl,
|
27
|
+
hostPort,
|
28
|
+
externalUrl,
|
29
|
+
samlPath,
|
30
|
+
samlAudience,
|
31
|
+
preLoadedConfig,
|
32
|
+
internalHostUrl,
|
33
|
+
internalHostPort,
|
34
|
+
apiKeys,
|
35
|
+
idpEnabled,
|
36
|
+
db,
|
37
|
+
useInternalServer: !(
|
38
|
+
hostUrl === internalHostUrl && hostPort === internalHostPort
|
39
|
+
),
|
40
|
+
};
|
41
|
+
|
42
|
+
export default env;
|
package/src/index.ts
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
import { JacksonOption } from 'saml-jackson';
|
2
|
+
import { SAMLConfig } from './controller/api';
|
3
|
+
import { OAuthController } from './controller/oauth';
|
4
|
+
import DB from './db/db';
|
5
|
+
import readConfig from './read-config';
|
6
|
+
|
7
|
+
const defaultOpts = (opts: JacksonOption): JacksonOption => {
|
8
|
+
const newOpts = {
|
9
|
+
...opts,
|
10
|
+
};
|
11
|
+
|
12
|
+
if (!newOpts.externalUrl) {
|
13
|
+
throw new Error('externalUrl is required');
|
14
|
+
}
|
15
|
+
|
16
|
+
if (!newOpts.samlPath) {
|
17
|
+
throw new Error('samlPath is required');
|
18
|
+
}
|
19
|
+
|
20
|
+
newOpts.samlAudience = newOpts.samlAudience || 'https://saml.boxyhq.com';
|
21
|
+
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).
|
22
|
+
newOpts.idpEnabled = newOpts.idpEnabled === true;
|
23
|
+
|
24
|
+
newOpts.db = newOpts.db || {};
|
25
|
+
newOpts.db.engine = newOpts.db.engine || 'sql';
|
26
|
+
newOpts.db.url =
|
27
|
+
newOpts.db.url || 'postgresql://postgres:postgres@localhost:5432/postgres';
|
28
|
+
newOpts.db.type = newOpts.db.type || 'postgres'; // Only needed if DB_ENGINE is sql.
|
29
|
+
newOpts.db.ttl = (newOpts.db.ttl || 300) * 1; // TTL for the code, session and token stores (in seconds)
|
30
|
+
newOpts.db.cleanupLimit = (newOpts.db.cleanupLimit || 1000) * 1; // Limit cleanup of TTL entries to this many items at a time
|
31
|
+
|
32
|
+
return newOpts;
|
33
|
+
};
|
34
|
+
|
35
|
+
export default async function controllers(
|
36
|
+
opts: JacksonOption
|
37
|
+
): Promise<{ apiController: SAMLConfig; oauthController: OAuthController }> {
|
38
|
+
opts = defaultOpts(opts);
|
39
|
+
|
40
|
+
const db = await DB.new(opts.db);
|
41
|
+
|
42
|
+
const configStore = db.store('saml:config');
|
43
|
+
const sessionStore = db.store('oauth:session', opts.db.ttl);
|
44
|
+
const codeStore = db.store('oauth:code', opts.db.ttl);
|
45
|
+
const tokenStore = db.store('oauth:token', opts.db.ttl);
|
46
|
+
|
47
|
+
const apiController = new SAMLConfig({ configStore });
|
48
|
+
|
49
|
+
const oauthController = new OAuthController({
|
50
|
+
configStore,
|
51
|
+
sessionStore,
|
52
|
+
codeStore,
|
53
|
+
tokenStore,
|
54
|
+
opts,
|
55
|
+
});
|
56
|
+
|
57
|
+
// write pre-loaded config if present
|
58
|
+
if (opts.preLoadedConfig && opts.preLoadedConfig.length > 0) {
|
59
|
+
const configs = await readConfig(opts.preLoadedConfig);
|
60
|
+
|
61
|
+
for (const config of configs) {
|
62
|
+
await apiController.config(config);
|
63
|
+
|
64
|
+
console.log(
|
65
|
+
`loaded config for tenant "${config.tenant}" and product "${config.product}"`
|
66
|
+
);
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
const type =
|
71
|
+
opts.db.engine === 'sql' && opts.db.type ? ' Type: ' + opts.db.type : '';
|
72
|
+
|
73
|
+
console.log(`Using engine: ${opts.db.engine}.${type}`);
|
74
|
+
|
75
|
+
return {
|
76
|
+
apiController,
|
77
|
+
oauthController,
|
78
|
+
};
|
79
|
+
}
|
package/src/jackson.ts
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
import cors from 'cors';
|
2
|
+
import express from 'express';
|
3
|
+
import { JacksonError } from './controller/error';
|
4
|
+
import { extractAuthToken } from './controller/utils';
|
5
|
+
import env from './env';
|
6
|
+
import jackson from './index';
|
7
|
+
|
8
|
+
let apiController;
|
9
|
+
let oauthController;
|
10
|
+
|
11
|
+
const oauthPath = '/oauth';
|
12
|
+
const apiPath = '/api/v1/saml';
|
13
|
+
|
14
|
+
const app = express();
|
15
|
+
|
16
|
+
app.use(express.json());
|
17
|
+
app.use(express.urlencoded({ extended: true }));
|
18
|
+
|
19
|
+
app.get(oauthPath + '/authorize', async (req, res) => {
|
20
|
+
try {
|
21
|
+
const { redirect_url } = await oauthController.authorize(req.query);
|
22
|
+
|
23
|
+
res.redirect(redirect_url);
|
24
|
+
} catch (err) {
|
25
|
+
const { message, statusCode = 500 } = err as JacksonError;
|
26
|
+
|
27
|
+
res.status(statusCode).send(message);
|
28
|
+
}
|
29
|
+
});
|
30
|
+
|
31
|
+
app.post(env.samlPath, async (req, res) => {
|
32
|
+
try {
|
33
|
+
const { redirect_url } = await oauthController.samlResponse(req.body);
|
34
|
+
|
35
|
+
res.redirect(redirect_url);
|
36
|
+
} catch (err) {
|
37
|
+
const { message, statusCode = 500 } = err as JacksonError;
|
38
|
+
|
39
|
+
res.status(statusCode).send(message);
|
40
|
+
}
|
41
|
+
});
|
42
|
+
|
43
|
+
app.post(oauthPath + '/token', cors(), async (req, res) => {
|
44
|
+
try {
|
45
|
+
const result = await oauthController.token(req.body);
|
46
|
+
|
47
|
+
res.json(result);
|
48
|
+
} catch (err) {
|
49
|
+
const { message, statusCode = 500 } = err as JacksonError;
|
50
|
+
|
51
|
+
res.status(statusCode).send(message);
|
52
|
+
}
|
53
|
+
});
|
54
|
+
|
55
|
+
app.get(oauthPath + '/userinfo', async (req, res) => {
|
56
|
+
try {
|
57
|
+
let token = extractAuthToken(req);
|
58
|
+
|
59
|
+
// check for query param
|
60
|
+
if (!token) {
|
61
|
+
token = req.query.access_token;
|
62
|
+
}
|
63
|
+
|
64
|
+
if (!token) {
|
65
|
+
res.status(401).json({ message: 'Unauthorized' });
|
66
|
+
}
|
67
|
+
|
68
|
+
const profile = await oauthController.userInfo(token);
|
69
|
+
|
70
|
+
res.json(profile);
|
71
|
+
} catch (err) {
|
72
|
+
const { message, statusCode = 500 } = err as JacksonError;
|
73
|
+
|
74
|
+
res.status(statusCode).json({ message });
|
75
|
+
}
|
76
|
+
});
|
77
|
+
|
78
|
+
const server = app.listen(env.hostPort, async () => {
|
79
|
+
console.log(
|
80
|
+
`🚀 The path of the righteous server: http://${env.hostUrl}:${env.hostPort}`
|
81
|
+
);
|
82
|
+
|
83
|
+
// TODO: Fix it
|
84
|
+
// @ts-ignore
|
85
|
+
const ctrlrModule = await jackson(env);
|
86
|
+
|
87
|
+
apiController = ctrlrModule.apiController;
|
88
|
+
oauthController = ctrlrModule.oauthController;
|
89
|
+
});
|
90
|
+
|
91
|
+
// Internal routes, recommended not to expose this to the public interface though it would be guarded by API key(s)
|
92
|
+
let internalApp = app;
|
93
|
+
|
94
|
+
if (env.useInternalServer) {
|
95
|
+
internalApp = express();
|
96
|
+
|
97
|
+
internalApp.use(express.json());
|
98
|
+
internalApp.use(express.urlencoded({ extended: true }));
|
99
|
+
}
|
100
|
+
|
101
|
+
const validateApiKey = (token) => {
|
102
|
+
return env.apiKeys.includes(token);
|
103
|
+
};
|
104
|
+
|
105
|
+
internalApp.post(apiPath + '/config', async (req, res) => {
|
106
|
+
try {
|
107
|
+
const apiKey = extractAuthToken(req);
|
108
|
+
if (!validateApiKey(apiKey)) {
|
109
|
+
res.status(401).send('Unauthorized');
|
110
|
+
return;
|
111
|
+
}
|
112
|
+
|
113
|
+
res.json(await apiController.config(req.body));
|
114
|
+
} catch (err) {
|
115
|
+
const { message } = err as JacksonError;
|
116
|
+
|
117
|
+
res.status(500).json({
|
118
|
+
error: message,
|
119
|
+
});
|
120
|
+
}
|
121
|
+
});
|
122
|
+
|
123
|
+
internalApp.get(apiPath + '/config', async (req, res) => {
|
124
|
+
try {
|
125
|
+
const apiKey = extractAuthToken(req);
|
126
|
+
if (!validateApiKey(apiKey)) {
|
127
|
+
res.status(401).send('Unauthorized');
|
128
|
+
return;
|
129
|
+
}
|
130
|
+
|
131
|
+
res.json(await apiController.getConfig(req.query));
|
132
|
+
} catch (err) {
|
133
|
+
const { message } = err as JacksonError;
|
134
|
+
|
135
|
+
res.status(500).json({
|
136
|
+
error: message,
|
137
|
+
});
|
138
|
+
}
|
139
|
+
});
|
140
|
+
|
141
|
+
internalApp.delete(apiPath + '/config', async (req, res) => {
|
142
|
+
try {
|
143
|
+
const apiKey = extractAuthToken(req);
|
144
|
+
if (!validateApiKey(apiKey)) {
|
145
|
+
res.status(401).send('Unauthorized');
|
146
|
+
return;
|
147
|
+
}
|
148
|
+
await apiController.deleteConfig(req.body);
|
149
|
+
res.status(200).end();
|
150
|
+
} catch (err) {
|
151
|
+
const { message } = err as JacksonError;
|
152
|
+
|
153
|
+
res.status(500).json({
|
154
|
+
error: message,
|
155
|
+
});
|
156
|
+
}
|
157
|
+
});
|
158
|
+
|
159
|
+
let internalServer = server;
|
160
|
+
if (env.useInternalServer) {
|
161
|
+
internalServer = internalApp.listen(env.internalHostPort, async () => {
|
162
|
+
console.log(
|
163
|
+
`🚀 The path of the righteous internal server: http://${env.internalHostUrl}:${env.internalHostPort}`
|
164
|
+
);
|
165
|
+
});
|
166
|
+
}
|
167
|
+
|
168
|
+
module.exports = {
|
169
|
+
server,
|
170
|
+
internalServer,
|
171
|
+
};
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import * as fs from 'fs';
|
2
|
+
import * as path from 'path';
|
3
|
+
import { IdPConfig } from 'saml-jackson';
|
4
|
+
|
5
|
+
const readConfig = async (preLoadedConfig: string): Promise<IdPConfig[]> => {
|
6
|
+
if (preLoadedConfig.startsWith('./')) {
|
7
|
+
preLoadedConfig = path.resolve(process.cwd(), preLoadedConfig);
|
8
|
+
}
|
9
|
+
|
10
|
+
const files = await fs.promises.readdir(preLoadedConfig);
|
11
|
+
const configs: IdPConfig[] = [];
|
12
|
+
|
13
|
+
for (let idx in files) {
|
14
|
+
const file = files[idx];
|
15
|
+
if (file.endsWith('.js')) {
|
16
|
+
const config = require(path.join(preLoadedConfig, file)) as IdPConfig;
|
17
|
+
const rawMetadata = await fs.promises.readFile(
|
18
|
+
path.join(preLoadedConfig, path.parse(file).name + '.xml'),
|
19
|
+
'utf8'
|
20
|
+
);
|
21
|
+
config.rawMetadata = rawMetadata;
|
22
|
+
configs.push(config);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
return configs;
|
27
|
+
};
|
28
|
+
|
29
|
+
export default readConfig;
|
@@ -0,0 +1,41 @@
|
|
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
|
+
] as const;
|
21
|
+
|
22
|
+
type attributes = typeof mapping[number]['attribute'];
|
23
|
+
type schemas = typeof mapping[number]['schema'];
|
24
|
+
|
25
|
+
const map = (claims: Record<attributes | schemas, unknown>) => {
|
26
|
+
const profile = {
|
27
|
+
raw: claims,
|
28
|
+
};
|
29
|
+
|
30
|
+
mapping.forEach((m) => {
|
31
|
+
if (claims[m.attribute]) {
|
32
|
+
profile[m.attribute] = claims[m.attribute];
|
33
|
+
} else if (claims[m.schema]) {
|
34
|
+
profile[m.attribute] = claims[m.schema];
|
35
|
+
}
|
36
|
+
});
|
37
|
+
|
38
|
+
return profile;
|
39
|
+
};
|
40
|
+
|
41
|
+
export default { map };
|