@boxyhq/saml-jackson 0.2.3-beta.238 → 0.2.3-beta.243
Sign up to get free protection for your applications and to get access to all the features.
- package/Dockerfile +3 -3
- 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 +80 -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 +5 -4
package/Dockerfile
CHANGED
@@ -4,7 +4,7 @@ FROM node:16.13.1-alpine3.14 AS deps
|
|
4
4
|
RUN apk add --no-cache libc6-compat
|
5
5
|
WORKDIR /app
|
6
6
|
COPY src/ src/
|
7
|
-
COPY package.json package-lock.json ./
|
7
|
+
COPY package.json package-lock.json tsconfig*.json ./
|
8
8
|
RUN npm ci --only=production
|
9
9
|
|
10
10
|
# Production image, copy all the files and run next
|
@@ -17,7 +17,7 @@ ENV NODE_ENV production
|
|
17
17
|
RUN addgroup -g 1001 -S nodejs
|
18
18
|
RUN adduser -S nodejs -u 1001
|
19
19
|
|
20
|
-
COPY --from=deps /app/
|
20
|
+
COPY --from=deps /app/dist ./dist
|
21
21
|
COPY --from=deps /app/node_modules ./node_modules
|
22
22
|
COPY --from=deps /app/package.json ./package.json
|
23
23
|
|
@@ -26,4 +26,4 @@ USER nodejs
|
|
26
26
|
EXPOSE 5000
|
27
27
|
EXPOSE 6000
|
28
28
|
|
29
|
-
CMD [ "node", "
|
29
|
+
CMD [ "node", "dist/jackson.js" ]
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { IdPConfig, ISAMLConfig, OAuth } from '../typings';
|
2
|
+
export declare class SAMLConfig implements ISAMLConfig {
|
3
|
+
private configStore;
|
4
|
+
constructor({ configStore }: {
|
5
|
+
configStore: any;
|
6
|
+
});
|
7
|
+
private _validateIdPConfig;
|
8
|
+
create(body: IdPConfig): Promise<OAuth>;
|
9
|
+
get(body: {
|
10
|
+
clientID: string;
|
11
|
+
tenant: string;
|
12
|
+
product: string;
|
13
|
+
}): Promise<Partial<OAuth>>;
|
14
|
+
delete(body: {
|
15
|
+
clientID: string;
|
16
|
+
clientSecret: string;
|
17
|
+
tenant: string;
|
18
|
+
product: string;
|
19
|
+
}): Promise<void>;
|
20
|
+
config(body: IdPConfig): Promise<OAuth>;
|
21
|
+
getConfig(body: {
|
22
|
+
clientID: string;
|
23
|
+
tenant: string;
|
24
|
+
product: string;
|
25
|
+
}): Promise<Partial<OAuth>>;
|
26
|
+
deleteConfig(body: {
|
27
|
+
clientID: string;
|
28
|
+
clientSecret: string;
|
29
|
+
tenant: string;
|
30
|
+
product: string;
|
31
|
+
}): Promise<void>;
|
32
|
+
}
|
@@ -0,0 +1,193 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
5
|
+
}) : (function(o, m, k, k2) {
|
6
|
+
if (k2 === undefined) k2 = k;
|
7
|
+
o[k2] = m[k];
|
8
|
+
}));
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
11
|
+
}) : function(o, v) {
|
12
|
+
o["default"] = v;
|
13
|
+
});
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
15
|
+
if (mod && mod.__esModule) return mod;
|
16
|
+
var result = {};
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
18
|
+
__setModuleDefault(result, mod);
|
19
|
+
return result;
|
20
|
+
};
|
21
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
22
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
23
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
24
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
25
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
26
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
28
|
+
});
|
29
|
+
};
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
32
|
+
};
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
34
|
+
exports.SAMLConfig = void 0;
|
35
|
+
const crypto_1 = __importDefault(require("crypto"));
|
36
|
+
const dbutils = __importStar(require("../db/utils"));
|
37
|
+
const saml_1 = __importDefault(require("../saml/saml"));
|
38
|
+
const x509_1 = __importDefault(require("../saml/x509"));
|
39
|
+
const error_1 = require("./error");
|
40
|
+
const utils_1 = require("./utils");
|
41
|
+
class SAMLConfig {
|
42
|
+
constructor({ configStore }) {
|
43
|
+
this.configStore = configStore;
|
44
|
+
}
|
45
|
+
_validateIdPConfig(body) {
|
46
|
+
const { rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product } = body;
|
47
|
+
if (!rawMetadata) {
|
48
|
+
throw new error_1.JacksonError('Please provide rawMetadata', 400);
|
49
|
+
}
|
50
|
+
if (!defaultRedirectUrl) {
|
51
|
+
throw new error_1.JacksonError('Please provide a defaultRedirectUrl', 400);
|
52
|
+
}
|
53
|
+
if (!redirectUrl) {
|
54
|
+
throw new error_1.JacksonError('Please provide redirectUrl', 400);
|
55
|
+
}
|
56
|
+
if (!tenant) {
|
57
|
+
throw new error_1.JacksonError('Please provide tenant', 400);
|
58
|
+
}
|
59
|
+
if (!product) {
|
60
|
+
throw new error_1.JacksonError('Please provide product', 400);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
create(body) {
|
64
|
+
return __awaiter(this, void 0, void 0, function* () {
|
65
|
+
const { rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product } = body;
|
66
|
+
this._validateIdPConfig(body);
|
67
|
+
const idpMetadata = yield saml_1.default.parseMetadataAsync(rawMetadata);
|
68
|
+
// extract provider
|
69
|
+
let providerName = extractHostName(idpMetadata.entityID);
|
70
|
+
if (!providerName) {
|
71
|
+
providerName = extractHostName(idpMetadata.sso.redirectUrl || idpMetadata.sso.postUrl);
|
72
|
+
}
|
73
|
+
idpMetadata.provider = providerName ? providerName : 'Unknown';
|
74
|
+
const clientID = dbutils.keyDigest(dbutils.keyFromParts(tenant, product, idpMetadata.entityID));
|
75
|
+
let clientSecret;
|
76
|
+
const exists = yield this.configStore.get(clientID);
|
77
|
+
if (exists) {
|
78
|
+
clientSecret = exists.clientSecret;
|
79
|
+
}
|
80
|
+
else {
|
81
|
+
clientSecret = crypto_1.default.randomBytes(24).toString('hex');
|
82
|
+
}
|
83
|
+
const certs = yield x509_1.default.generate();
|
84
|
+
if (!certs) {
|
85
|
+
throw new Error('Error generating x59 certs');
|
86
|
+
}
|
87
|
+
yield this.configStore.put(clientID, {
|
88
|
+
idpMetadata,
|
89
|
+
defaultRedirectUrl,
|
90
|
+
redirectUrl: JSON.parse(redirectUrl),
|
91
|
+
tenant,
|
92
|
+
product,
|
93
|
+
clientID,
|
94
|
+
clientSecret,
|
95
|
+
certs,
|
96
|
+
}, {
|
97
|
+
// secondary index on entityID
|
98
|
+
name: utils_1.IndexNames.EntityID,
|
99
|
+
value: idpMetadata.entityID,
|
100
|
+
}, {
|
101
|
+
// secondary index on tenant + product
|
102
|
+
name: utils_1.IndexNames.TenantProduct,
|
103
|
+
value: dbutils.keyFromParts(tenant, product),
|
104
|
+
});
|
105
|
+
return {
|
106
|
+
client_id: clientID,
|
107
|
+
client_secret: clientSecret,
|
108
|
+
provider: idpMetadata.provider,
|
109
|
+
};
|
110
|
+
});
|
111
|
+
}
|
112
|
+
get(body) {
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
114
|
+
const { clientID, tenant, product } = body;
|
115
|
+
if (clientID) {
|
116
|
+
const samlConfig = yield this.configStore.get(clientID);
|
117
|
+
return samlConfig ? { provider: samlConfig.idpMetadata.provider } : {};
|
118
|
+
}
|
119
|
+
if (tenant && product) {
|
120
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
121
|
+
name: utils_1.IndexNames.TenantProduct,
|
122
|
+
value: dbutils.keyFromParts(tenant, product),
|
123
|
+
});
|
124
|
+
if (!samlConfigs || !samlConfigs.length) {
|
125
|
+
return {};
|
126
|
+
}
|
127
|
+
return { provider: samlConfigs[0].idpMetadata.provider };
|
128
|
+
}
|
129
|
+
throw new error_1.JacksonError('Please provide `clientID` or `tenant` and `product`.', 400);
|
130
|
+
});
|
131
|
+
}
|
132
|
+
delete(body) {
|
133
|
+
return __awaiter(this, void 0, void 0, function* () {
|
134
|
+
const { clientID, clientSecret, tenant, product } = body;
|
135
|
+
if (clientID && clientSecret) {
|
136
|
+
const samlConfig = yield this.configStore.get(clientID);
|
137
|
+
if (!samlConfig) {
|
138
|
+
return;
|
139
|
+
}
|
140
|
+
if (samlConfig.clientSecret === clientSecret) {
|
141
|
+
yield this.configStore.delete(clientID);
|
142
|
+
}
|
143
|
+
else {
|
144
|
+
throw new error_1.JacksonError('clientSecret mismatch.', 400);
|
145
|
+
}
|
146
|
+
return;
|
147
|
+
}
|
148
|
+
if (tenant && product) {
|
149
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
150
|
+
name: utils_1.IndexNames.TenantProduct,
|
151
|
+
value: dbutils.keyFromParts(tenant, product),
|
152
|
+
});
|
153
|
+
if (!samlConfigs || !samlConfigs.length) {
|
154
|
+
return;
|
155
|
+
}
|
156
|
+
for (const conf of samlConfigs) {
|
157
|
+
yield this.configStore.delete(conf.clientID);
|
158
|
+
}
|
159
|
+
return;
|
160
|
+
}
|
161
|
+
throw new error_1.JacksonError('Please provide `clientID` and `clientSecret` or `tenant` and `product`.', 400);
|
162
|
+
});
|
163
|
+
}
|
164
|
+
// Ensure backward compatibility
|
165
|
+
config(body) {
|
166
|
+
return __awaiter(this, void 0, void 0, function* () {
|
167
|
+
return this.create(body);
|
168
|
+
});
|
169
|
+
}
|
170
|
+
getConfig(body) {
|
171
|
+
return __awaiter(this, void 0, void 0, function* () {
|
172
|
+
return this.get(body);
|
173
|
+
});
|
174
|
+
}
|
175
|
+
deleteConfig(body) {
|
176
|
+
return __awaiter(this, void 0, void 0, function* () {
|
177
|
+
return this.delete(body);
|
178
|
+
});
|
179
|
+
}
|
180
|
+
}
|
181
|
+
exports.SAMLConfig = SAMLConfig;
|
182
|
+
const extractHostName = (url) => {
|
183
|
+
try {
|
184
|
+
const pUrl = new URL(url);
|
185
|
+
if (pUrl.hostname.startsWith('www.')) {
|
186
|
+
return pUrl.hostname.substring(4);
|
187
|
+
}
|
188
|
+
return pUrl.hostname;
|
189
|
+
}
|
190
|
+
catch (err) {
|
191
|
+
return null;
|
192
|
+
}
|
193
|
+
};
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.JacksonError = void 0;
|
4
|
+
class JacksonError extends Error {
|
5
|
+
constructor(message, statusCode = 500) {
|
6
|
+
super(message);
|
7
|
+
this.name = this.constructor.name;
|
8
|
+
this.statusCode = statusCode;
|
9
|
+
Error.captureStackTrace(this, this.constructor);
|
10
|
+
}
|
11
|
+
}
|
12
|
+
exports.JacksonError = JacksonError;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const redirect: (redirectUrl: string, redirectUrls: string[]) => boolean;
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.redirect = void 0;
|
4
|
+
const redirect = (redirectUrl, redirectUrls) => {
|
5
|
+
const url = new URL(redirectUrl);
|
6
|
+
for (const idx in redirectUrls) {
|
7
|
+
const rUrl = new URL(redirectUrls[idx]);
|
8
|
+
// TODO: Check pathname, for now pathname is ignored
|
9
|
+
if (rUrl.protocol === url.protocol &&
|
10
|
+
rUrl.hostname === url.hostname &&
|
11
|
+
rUrl.port === url.port) {
|
12
|
+
return true;
|
13
|
+
}
|
14
|
+
}
|
15
|
+
return false;
|
16
|
+
};
|
17
|
+
exports.redirect = redirect;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.encode = exports.transformBase64 = void 0;
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
8
|
+
const transformBase64 = (input) => {
|
9
|
+
return input.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
10
|
+
};
|
11
|
+
exports.transformBase64 = transformBase64;
|
12
|
+
const encode = (code_challenge) => {
|
13
|
+
return (0, exports.transformBase64)(crypto_1.default.createHash('sha256').update(code_challenge).digest('base64'));
|
14
|
+
};
|
15
|
+
exports.encode = encode;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const success: (redirectUrl: string, params: Record<string, string>) => string;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.success = void 0;
|
4
|
+
const success = (redirectUrl, params) => {
|
5
|
+
const url = new URL(redirectUrl);
|
6
|
+
for (const [key, value] of Object.entries(params)) {
|
7
|
+
url.searchParams.set(key, value);
|
8
|
+
}
|
9
|
+
return url.href;
|
10
|
+
};
|
11
|
+
exports.success = success;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { IOAuthController, OAuthReqBody, OAuthTokenReq, OAuthTokenRes, Profile, SAMLResponsePayload } from '../typings';
|
2
|
+
export declare class OAuthController implements IOAuthController {
|
3
|
+
private configStore;
|
4
|
+
private sessionStore;
|
5
|
+
private codeStore;
|
6
|
+
private tokenStore;
|
7
|
+
private opts;
|
8
|
+
constructor({ configStore, sessionStore, codeStore, tokenStore, opts }: {
|
9
|
+
configStore: any;
|
10
|
+
sessionStore: any;
|
11
|
+
codeStore: any;
|
12
|
+
tokenStore: any;
|
13
|
+
opts: any;
|
14
|
+
});
|
15
|
+
authorize(body: OAuthReqBody): Promise<{
|
16
|
+
redirect_url: string;
|
17
|
+
}>;
|
18
|
+
samlResponse(body: SAMLResponsePayload): Promise<{
|
19
|
+
redirect_url: string;
|
20
|
+
}>;
|
21
|
+
token(body: OAuthTokenReq): Promise<OAuthTokenRes>;
|
22
|
+
userInfo(token: string): Promise<Profile>;
|
23
|
+
}
|
@@ -0,0 +1,263 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
5
|
+
}) : (function(o, m, k, k2) {
|
6
|
+
if (k2 === undefined) k2 = k;
|
7
|
+
o[k2] = m[k];
|
8
|
+
}));
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
11
|
+
}) : function(o, v) {
|
12
|
+
o["default"] = v;
|
13
|
+
});
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
15
|
+
if (mod && mod.__esModule) return mod;
|
16
|
+
var result = {};
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
18
|
+
__setModuleDefault(result, mod);
|
19
|
+
return result;
|
20
|
+
};
|
21
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
22
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
23
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
24
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
25
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
26
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
28
|
+
});
|
29
|
+
};
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
32
|
+
};
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
34
|
+
exports.OAuthController = void 0;
|
35
|
+
const crypto_1 = __importDefault(require("crypto"));
|
36
|
+
const dbutils = __importStar(require("../db/utils"));
|
37
|
+
const saml_1 = __importDefault(require("../saml/saml"));
|
38
|
+
const error_1 = require("./error");
|
39
|
+
const allowed = __importStar(require("./oauth/allowed"));
|
40
|
+
const codeVerifier = __importStar(require("./oauth/code-verifier"));
|
41
|
+
const redirect = __importStar(require("./oauth/redirect"));
|
42
|
+
const utils_1 = require("./utils");
|
43
|
+
const relayStatePrefix = 'boxyhq_jackson_';
|
44
|
+
function getEncodedClientId(client_id) {
|
45
|
+
try {
|
46
|
+
const sp = new URLSearchParams(client_id);
|
47
|
+
const tenant = sp.get('tenant');
|
48
|
+
const product = sp.get('product');
|
49
|
+
if (tenant && product) {
|
50
|
+
return {
|
51
|
+
tenant: sp.get('tenant'),
|
52
|
+
product: sp.get('product'),
|
53
|
+
};
|
54
|
+
}
|
55
|
+
return null;
|
56
|
+
}
|
57
|
+
catch (err) {
|
58
|
+
return null;
|
59
|
+
}
|
60
|
+
}
|
61
|
+
class OAuthController {
|
62
|
+
constructor({ configStore, sessionStore, codeStore, tokenStore, opts }) {
|
63
|
+
this.configStore = configStore;
|
64
|
+
this.sessionStore = sessionStore;
|
65
|
+
this.codeStore = codeStore;
|
66
|
+
this.tokenStore = tokenStore;
|
67
|
+
this.opts = opts;
|
68
|
+
}
|
69
|
+
authorize(body) {
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
71
|
+
const { response_type = 'code', client_id, redirect_uri, state, tenant, product, code_challenge, code_challenge_method = '',
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
73
|
+
provider = 'saml', } = body;
|
74
|
+
if (!redirect_uri) {
|
75
|
+
throw new error_1.JacksonError('Please specify a redirect URL.', 400);
|
76
|
+
}
|
77
|
+
if (!state) {
|
78
|
+
throw new error_1.JacksonError('Please specify a state to safeguard against XSRF attacks.', 400);
|
79
|
+
}
|
80
|
+
let samlConfig;
|
81
|
+
if (tenant && product) {
|
82
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
83
|
+
name: utils_1.IndexNames.TenantProduct,
|
84
|
+
value: dbutils.keyFromParts(tenant, product),
|
85
|
+
});
|
86
|
+
if (!samlConfigs || samlConfigs.length === 0) {
|
87
|
+
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
88
|
+
}
|
89
|
+
// TODO: Support multiple matches
|
90
|
+
samlConfig = samlConfigs[0];
|
91
|
+
}
|
92
|
+
else if (client_id &&
|
93
|
+
client_id !== '' &&
|
94
|
+
client_id !== 'undefined' &&
|
95
|
+
client_id !== 'null') {
|
96
|
+
// if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
|
97
|
+
const sp = getEncodedClientId(client_id);
|
98
|
+
if (sp === null || sp === void 0 ? void 0 : sp.tenant) {
|
99
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
100
|
+
name: utils_1.IndexNames.TenantProduct,
|
101
|
+
value: dbutils.keyFromParts(sp.tenant, sp.product || ''),
|
102
|
+
});
|
103
|
+
if (!samlConfigs || samlConfigs.length === 0) {
|
104
|
+
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
105
|
+
}
|
106
|
+
// TODO: Support multiple matches
|
107
|
+
samlConfig = samlConfigs[0];
|
108
|
+
}
|
109
|
+
else {
|
110
|
+
samlConfig = yield this.configStore.get(client_id);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
else {
|
114
|
+
throw new error_1.JacksonError('You need to specify client_id or tenant & product', 403);
|
115
|
+
}
|
116
|
+
if (!samlConfig) {
|
117
|
+
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
118
|
+
}
|
119
|
+
if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
|
120
|
+
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
121
|
+
}
|
122
|
+
const samlReq = saml_1.default.request({
|
123
|
+
entityID: this.opts.samlAudience,
|
124
|
+
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
|
125
|
+
signingKey: samlConfig.certs.privateKey,
|
126
|
+
});
|
127
|
+
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
128
|
+
yield this.sessionStore.put(sessionId, {
|
129
|
+
id: samlReq.id,
|
130
|
+
redirect_uri,
|
131
|
+
response_type,
|
132
|
+
state,
|
133
|
+
code_challenge,
|
134
|
+
code_challenge_method,
|
135
|
+
});
|
136
|
+
const redirectUrl = redirect.success(samlConfig.idpMetadata.sso.redirectUrl, {
|
137
|
+
RelayState: relayStatePrefix + sessionId,
|
138
|
+
SAMLRequest: Buffer.from(samlReq.request).toString('base64'),
|
139
|
+
});
|
140
|
+
return { redirect_url: redirectUrl };
|
141
|
+
});
|
142
|
+
}
|
143
|
+
samlResponse(body) {
|
144
|
+
return __awaiter(this, void 0, void 0, function* () {
|
145
|
+
const { SAMLResponse } = body; // RelayState will contain the sessionId from earlier quasi-oauth flow
|
146
|
+
let RelayState = body.RelayState || '';
|
147
|
+
if (!this.opts.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
|
148
|
+
// IDP is disabled so block the request
|
149
|
+
throw new error_1.JacksonError('IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.', 403);
|
150
|
+
}
|
151
|
+
if (!RelayState.startsWith(relayStatePrefix)) {
|
152
|
+
RelayState = '';
|
153
|
+
}
|
154
|
+
RelayState = RelayState.replace(relayStatePrefix, '');
|
155
|
+
const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
|
156
|
+
const parsedResp = yield saml_1.default.parseAsync(rawResponse);
|
157
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
158
|
+
name: utils_1.IndexNames.EntityID,
|
159
|
+
value: parsedResp === null || parsedResp === void 0 ? void 0 : parsedResp.issuer,
|
160
|
+
});
|
161
|
+
if (!samlConfigs || samlConfigs.length === 0) {
|
162
|
+
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
163
|
+
}
|
164
|
+
// TODO: Support multiple matches
|
165
|
+
const samlConfig = samlConfigs[0];
|
166
|
+
let session;
|
167
|
+
if (RelayState !== '') {
|
168
|
+
session = yield this.sessionStore.get(RelayState);
|
169
|
+
if (!session) {
|
170
|
+
throw new error_1.JacksonError('Unable to validate state from the origin request.', 403);
|
171
|
+
}
|
172
|
+
}
|
173
|
+
const validateOpts = {
|
174
|
+
thumbprint: samlConfig.idpMetadata.thumbprint,
|
175
|
+
audience: this.opts.samlAudience,
|
176
|
+
};
|
177
|
+
if (session && session.id) {
|
178
|
+
validateOpts.inResponseTo = session.id;
|
179
|
+
}
|
180
|
+
const profile = yield saml_1.default.validateAsync(rawResponse, validateOpts);
|
181
|
+
// store details against a code
|
182
|
+
const code = crypto_1.default.randomBytes(20).toString('hex');
|
183
|
+
const codeVal = {
|
184
|
+
profile,
|
185
|
+
clientID: samlConfig.clientID,
|
186
|
+
clientSecret: samlConfig.clientSecret,
|
187
|
+
};
|
188
|
+
if (session) {
|
189
|
+
codeVal.session = session;
|
190
|
+
}
|
191
|
+
yield this.codeStore.put(code, codeVal);
|
192
|
+
if (session &&
|
193
|
+
session.redirect_uri &&
|
194
|
+
!allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
|
195
|
+
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
196
|
+
}
|
197
|
+
const params = {
|
198
|
+
code,
|
199
|
+
};
|
200
|
+
if (session && session.state) {
|
201
|
+
params.state = session.state;
|
202
|
+
}
|
203
|
+
const redirectUrl = redirect.success((session && session.redirect_uri) || samlConfig.defaultRedirectUrl, params);
|
204
|
+
return { redirect_url: redirectUrl };
|
205
|
+
});
|
206
|
+
}
|
207
|
+
token(body) {
|
208
|
+
return __awaiter(this, void 0, void 0, function* () {
|
209
|
+
const { client_id, client_secret, code_verifier, code, grant_type = 'authorization_code', } = body;
|
210
|
+
if (grant_type !== 'authorization_code') {
|
211
|
+
throw new error_1.JacksonError('Unsupported grant_type', 400);
|
212
|
+
}
|
213
|
+
if (!code) {
|
214
|
+
throw new error_1.JacksonError('Please specify code', 400);
|
215
|
+
}
|
216
|
+
const codeVal = yield this.codeStore.get(code);
|
217
|
+
if (!codeVal || !codeVal.profile) {
|
218
|
+
throw new error_1.JacksonError('Invalid code', 403);
|
219
|
+
}
|
220
|
+
if (client_id && client_secret) {
|
221
|
+
// check if we have an encoded client_id
|
222
|
+
if (client_id !== 'dummy' && client_secret !== 'dummy') {
|
223
|
+
const sp = getEncodedClientId(client_id);
|
224
|
+
if (!sp) {
|
225
|
+
// OAuth flow
|
226
|
+
if (client_id !== codeVal.clientID ||
|
227
|
+
client_secret !== codeVal.clientSecret) {
|
228
|
+
throw new error_1.JacksonError('Invalid client_id or client_secret', 401);
|
229
|
+
}
|
230
|
+
}
|
231
|
+
}
|
232
|
+
}
|
233
|
+
else if (code_verifier) {
|
234
|
+
// PKCE flow
|
235
|
+
let cv = code_verifier;
|
236
|
+
if (codeVal.session.code_challenge_method.toLowerCase() === 's256') {
|
237
|
+
cv = codeVerifier.encode(code_verifier);
|
238
|
+
}
|
239
|
+
if (codeVal.session.code_challenge !== cv) {
|
240
|
+
throw new error_1.JacksonError('Invalid code_verifier', 401);
|
241
|
+
}
|
242
|
+
}
|
243
|
+
else if (codeVal && codeVal.session) {
|
244
|
+
throw new error_1.JacksonError('Please specify client_secret or code_verifier', 401);
|
245
|
+
}
|
246
|
+
// store details against a token
|
247
|
+
const token = crypto_1.default.randomBytes(20).toString('hex');
|
248
|
+
yield this.tokenStore.put(token, codeVal.profile);
|
249
|
+
return {
|
250
|
+
access_token: token,
|
251
|
+
token_type: 'bearer',
|
252
|
+
expires_in: this.opts.db.ttl,
|
253
|
+
};
|
254
|
+
});
|
255
|
+
}
|
256
|
+
userInfo(token) {
|
257
|
+
return __awaiter(this, void 0, void 0, function* () {
|
258
|
+
const { claims } = yield this.tokenStore.get(token);
|
259
|
+
return claims;
|
260
|
+
});
|
261
|
+
}
|
262
|
+
}
|
263
|
+
exports.OAuthController = OAuthController;
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.IndexNames = exports.extractAuthToken = void 0;
|
4
|
+
const extractAuthToken = (req) => {
|
5
|
+
const authHeader = req.get('authorization');
|
6
|
+
const parts = (authHeader || '').split(' ');
|
7
|
+
if (parts.length > 1) {
|
8
|
+
return parts[1];
|
9
|
+
}
|
10
|
+
return null;
|
11
|
+
};
|
12
|
+
exports.extractAuthToken = extractAuthToken;
|
13
|
+
var IndexNames;
|
14
|
+
(function (IndexNames) {
|
15
|
+
IndexNames["EntityID"] = "entityID";
|
16
|
+
IndexNames["TenantProduct"] = "tenantProduct";
|
17
|
+
})(IndexNames = exports.IndexNames || (exports.IndexNames = {}));
|
package/dist/db/db.d.ts
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
import { DatabaseDriver, DatabaseOption, EncryptionKey, Index, Storable } from '../typings';
|
2
|
+
declare class DB implements DatabaseDriver {
|
3
|
+
private db;
|
4
|
+
private encryptionKey;
|
5
|
+
constructor(db: DatabaseDriver, encryptionKey: EncryptionKey);
|
6
|
+
get(namespace: string, key: string): Promise<unknown>;
|
7
|
+
getByIndex(namespace: string, idx: Index): Promise<unknown[]>;
|
8
|
+
put(namespace: string, key: string, val: unknown, ttl?: number, ...indexes: Index[]): Promise<unknown>;
|
9
|
+
delete(namespace: string, key: string): Promise<unknown>;
|
10
|
+
store(namespace: string, ttl?: number): Storable;
|
11
|
+
}
|
12
|
+
declare const _default: {
|
13
|
+
new: (options: DatabaseOption) => Promise<DB>;
|
14
|
+
};
|
15
|
+
export = _default;
|