-
Notifications
You must be signed in to change notification settings - Fork 476
/
Copy pathstrategy.ts
274 lines (247 loc) · 8.97 KB
/
strategy.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
import { Strategy as PassportStrategy } from "passport-strategy";
import { strict as assert } from "assert";
import * as url from "url";
import { Profile, SAML, SamlConfig } from ".";
import {
AuthenticateOptions,
RequestWithUser,
User,
VerifiedCallback,
VerifyWithoutRequest,
VerifyWithRequest,
} from "./types";
import { Request } from "express";
export abstract class AbstractStrategy extends PassportStrategy {
static readonly newSamlProviderOnConstruct: boolean;
name: string;
_signonVerify: VerifyWithRequest | VerifyWithoutRequest;
_logoutVerify: VerifyWithRequest | VerifyWithoutRequest;
_saml: SAML | undefined;
_passReqToCallback?: boolean;
constructor(
options: SamlConfig,
signonVerify: VerifyWithRequest,
logoutVerify: VerifyWithRequest
);
constructor(
options: SamlConfig,
signonVerify: VerifyWithoutRequest,
logoutVerify: VerifyWithoutRequest
);
constructor(options: SamlConfig, signonVerify: never, logoutVerify: never) {
super();
if (typeof options === "function") {
throw new Error("Mandatory SAML options missing");
}
if (!signonVerify || typeof signonVerify != "function") {
throw new Error("SAML authentication strategy requires a verify function");
}
// Customizing the name can be useful to support multiple SAML configurations at the same time.
// Unlike other options, this one gets deleted instead of passed along.
if (options.name) {
this.name = options.name;
} else {
this.name = "saml";
}
this._signonVerify = signonVerify;
this._logoutVerify = logoutVerify;
if ((this.constructor as typeof Strategy).newSamlProviderOnConstruct) {
this._saml = new SAML(options);
}
this._passReqToCallback = !!options.passReqToCallback;
}
authenticate(req: Request, options: AuthenticateOptions): void {
if (this._saml == null) {
throw new Error("Can't get authenticate without a SAML provider defined.");
}
options.samlFallback = options.samlFallback || "login-request";
const validateCallback = async ({
profile,
loggedOut,
}: {
profile: Profile | null;
loggedOut: boolean;
}) => {
if (loggedOut) {
if (profile != null) {
// When logging out a user, use the consumer's `validate` function to check that
// the `profile` associated with the logout request resolves to the same user
// as the `profile` associated with the current session.
const verified = async (logoutUser?: User) => {
let userMatch = true;
try {
// Check to see if we are logging out the user that is currently logged in to craft a proper IdP response
// It is up to the caller to return the same `User` as we have currently recorded as logged in for a successful logout
assert.deepStrictEqual(req.user, logoutUser);
} catch (err) {
userMatch = false;
}
const RelayState = req.query?.RelayState || req.body?.RelayState;
if (this._saml == null) {
return this.error(
new Error("Can't get logout response URL without a SAML provider defined.")
);
} else {
this._saml.getLogoutResponseUrl(
profile,
RelayState,
options,
userMatch,
redirectIfSuccess
);
}
// Log out the current user no matter if we can verify the logged in user === logout requested user
await new Promise((resolve, reject) => {
req.logout((err) => {
if (err) {
return reject(err);
}
resolve(undefined);
});
});
};
let logoutUser: User | undefined;
try {
logoutUser = await new Promise((resolve, reject) => {
const verifedCallback: VerifiedCallback = (err: Error | null, logoutUser?: User) => {
if (err) {
return reject(err);
}
resolve(logoutUser);
};
if (this._passReqToCallback) {
(this._logoutVerify as VerifyWithRequest)(req, profile, verifedCallback);
} else {
(this._logoutVerify as VerifyWithoutRequest)(profile, verifedCallback);
}
});
} catch (err) {
return this.error(err as Error);
}
await verified(logoutUser);
} else {
// If the `profile` object was null, this is just a logout acknowledgment, so we take no action
return this.pass();
}
} else {
const verified = (err: Error | null, user?: User, info?: unknown) => {
if (err) {
return this.error(err);
}
if (!user) {
return this.fail(info, 401);
}
this.success(user, info);
};
if (this._passReqToCallback) {
(this._signonVerify as VerifyWithRequest)(req, profile, verified);
} else {
(this._signonVerify as VerifyWithoutRequest)(profile, verified);
}
}
};
const redirectIfSuccess = (err: Error | null, url?: string) => {
if (err) {
this.error(err);
} else if (url == null) {
this.error(new Error("Invalid logout redirect URL."));
} else {
this.redirect(url);
}
};
if (req.query?.SAMLResponse || req.query?.SAMLRequest) {
const originalQuery = url.parse(req.url).query ?? "";
this._saml
.validateRedirectAsync(req.query, originalQuery)
.then(validateCallback)
.catch((err) => this.error(err));
} else if (req.body?.SAMLResponse) {
this._saml
.validatePostResponseAsync(req.body)
.then(validateCallback)
.catch((err) => this.error(err));
} else if (req.body?.SAMLRequest) {
this._saml
.validatePostRequestAsync(req.body)
.then(validateCallback)
.catch((err) => this.error(err));
} else {
const requestHandler = {
"login-request": async () => {
try {
if (this._saml == null) {
throw new Error("Can't process login request without a SAML provider defined.");
}
const RelayState =
(req.query && req.query.RelayState) || (req.body && req.body.RelayState);
const host = req.headers && req.headers.host;
if (this._saml.options.authnRequestBinding === "HTTP-POST") {
const data = await this._saml.getAuthorizeFormAsync(RelayState, host);
const res = req.res;
res?.send(data);
} else {
// Defaults to HTTP-Redirect
this.redirect(await this._saml.getAuthorizeUrlAsync(RelayState, host, options));
}
} catch (err) {
this.error(err as Error);
}
},
"logout-request": async () => {
if (this._saml == null) {
throw new Error("Can't process logout request without a SAML provider defined.");
}
try {
const RelayState =
(req.query && req.query.RelayState) || (req.body && req.body.RelayState);
// Defaults to HTTP-Redirect
this.redirect(
await this._saml.getLogoutUrlAsync(req.user as Profile, RelayState, options)
);
} catch (err) {
this.error(err as Error);
}
},
}[options.samlFallback];
requestHandler();
}
}
logout(req: RequestWithUser, callback: (err: Error | null, url?: string | null) => void): void {
if (this._saml == null) {
throw new Error("Can't logout without a SAML provider defined.");
}
const RelayState = (req.query && req.query.RelayState) || (req.body && req.body.RelayState);
this._saml
.getLogoutUrlAsync(req.user as Profile, RelayState, {})
.then((url) => callback(null, url))
.catch((err) => callback(err));
}
protected _generateServiceProviderMetadata(
decryptionCert: string | null,
signingCert?: string | string[] | null
): string {
if (this._saml == null) {
throw new Error("Can't generate service provider metadata without a SAML provider defined.");
}
return this._saml.generateServiceProviderMetadata(decryptionCert, signingCert);
}
// This is redundant, but helps with testing
error(err: Error): void {
super.error(err);
}
redirect(url: string, status?: number): void {
super.redirect(url, status);
}
success(user: unknown, info?: unknown): void {
super.success(user, info);
}
}
export class Strategy extends AbstractStrategy {
static readonly newSamlProviderOnConstruct = true;
generateServiceProviderMetadata(
decryptionCert: string | null,
signingCert?: string | string[] | null
): string {
return this._generateServiceProviderMetadata(decryptionCert, signingCert);
}
}