/
strategy.ts
199 lines (174 loc) · 5.75 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
import { Strategy as PassportStrategy } from "passport-strategy";
import * as saml from "./saml";
import * as url from "url";
import {
AuthenticateOptions,
AuthorizeOptions,
RequestWithUser,
SamlConfig,
StrategyOptions,
VerifyWithoutRequest,
VerifyWithRequest,
} from "./types";
import { Profile } from "./types";
export abstract class AbstractStrategy extends PassportStrategy {
name: string;
_verify: VerifyWithRequest | VerifyWithoutRequest;
_saml: saml.SAML | undefined;
_passReqToCallback?: boolean;
constructor(options: SamlConfig, verify: VerifyWithRequest);
constructor(options: SamlConfig, verify: VerifyWithoutRequest);
constructor(options: SamlConfig, verify: never) {
super();
if (typeof options === "function") {
throw new Error("Mandatory SAML options missing");
}
if (!verify) {
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._verify = verify;
if ((this.constructor as typeof Strategy).newSamlProviderOnConstruct) {
this._saml = new saml.SAML(options);
}
this._passReqToCallback = !!options.passReqToCallback;
}
authenticate(req: RequestWithUser, 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 = ({
profile,
loggedOut,
}: {
profile?: Profile | null;
loggedOut?: boolean;
}) => {
if (loggedOut) {
req.logout();
if (profile) {
if (this._saml == null) {
throw new Error("Can't get logout response URL without a SAML provider defined.");
}
req.samlLogoutRequest = profile;
return this._saml.getLogoutResponseUrl(req, options, redirectIfSuccess);
}
return this.pass();
}
const verified = (
err: Error | null,
user?: Record<string, unknown>,
info?: Record<string, unknown>
) => {
if (err) {
return this.error(err);
}
if (!user) {
return this.fail(info, 401);
}
this.success(user, info);
};
if (this._passReqToCallback) {
(this._verify as VerifyWithRequest)(req, profile, verified);
} else {
(this._verify as VerifyWithoutRequest)(profile, verified);
}
};
const redirectIfSuccess = (err: Error | null, url?: string | null) => {
if (err) {
this.error(err);
} else {
this.redirect(url!);
}
};
if (req.query && (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 && req.body.SAMLResponse) {
this._saml
.validatePostResponseAsync(req.body)
.then(validateCallback)
.catch((err) => this.error(err));
} else if (req.body && 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.");
}
if (this._saml.options.authnRequestBinding === "HTTP-POST") {
const data = await this._saml.getAuthorizeFormAsync(req);
const res = req.res!;
res.send(data);
} else {
// Defaults to HTTP-Redirect
this.redirect(await this._saml.getAuthorizeUrlAsync(req, options));
}
} catch (err) {
this.error(err);
}
},
"logout-request": async () => {
if (this._saml == null) {
throw new Error("Can't process logout request without a SAML provider defined.");
}
try {
this.redirect(await this._saml.getLogoutUrlAsync(req, options));
} catch (err) {
this.error(err);
}
},
}[options.samlFallback];
if (typeof requestHandler !== "function") {
return this.fail(401);
}
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.");
}
this._saml
.getLogoutUrlAsync(req, {})
.then((url) => callback(null, url))
.catch((err) => callback(err));
}
protected _generateServiceProviderMetadata(
decryptionCert: string | null,
signingCert?: 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 reduntant, but helps with testing
error(err: Error): void {
super.error(err);
}
}
export class Strategy extends AbstractStrategy {
static readonly newSamlProviderOnConstruct = true;
generateServiceProviderMetadata(
decryptionCert: string | null,
signingCert?: string | null
): string {
return this._generateServiceProviderMetadata(decryptionCert, signingCert);
}
}