@ministryofjustice/hmpps-prisoner-auth 0.0.11 → 0.0.12
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/dist/index.cjs.js +185 -11
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +180 -11
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -1,14 +1,188 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
var OpenIDConnectStrategy = require('passport-openidconnect');
|
|
4
|
+
var superagent = require('superagent');
|
|
5
|
+
|
|
6
|
+
const tokenFromJwt = (token) => JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
7
|
+
const oauthClientToken = (clientId, clientSecret) => {
|
|
8
|
+
const token = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
9
|
+
return `Basic ${token}`;
|
|
10
|
+
};
|
|
11
|
+
const tokenFetcher = (userRefreshToken, tokenUrl, authorizationHeader) => {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
superagent
|
|
14
|
+
.post(tokenUrl)
|
|
15
|
+
.set('Content-Type', 'application/x-www-form-urlencoded')
|
|
16
|
+
.set('Authorization', authorizationHeader)
|
|
17
|
+
.query({ refresh_token: userRefreshToken, grant_type: 'refresh_token' })
|
|
18
|
+
.end((err, res) => {
|
|
19
|
+
if (err)
|
|
20
|
+
reject(err);
|
|
21
|
+
const { id_token: idToken, access_token: accessToken, refresh_token: refreshToken } = res.body;
|
|
22
|
+
return resolve({ idToken, accessToken, refreshToken });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const userFromTokens = ({ idToken, accessToken, refreshToken }) => {
|
|
28
|
+
const parsedIdToken = tokenFromJwt(idToken);
|
|
29
|
+
const { name, sub, establishment } = parsedIdToken;
|
|
30
|
+
const authSource = 'prisoner-auth';
|
|
31
|
+
const userUuid = undefined;
|
|
32
|
+
return {
|
|
33
|
+
establishment,
|
|
34
|
+
idToken: parsedIdToken,
|
|
35
|
+
refreshToken,
|
|
36
|
+
accessToken,
|
|
37
|
+
authSource,
|
|
38
|
+
name,
|
|
39
|
+
token: idToken,
|
|
40
|
+
username: sub,
|
|
41
|
+
userId: sub,
|
|
42
|
+
userUuid,
|
|
43
|
+
displayName: name,
|
|
44
|
+
userRoles: [],
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// A TimeSpan allows you to give meaning to a number and then read that number back and compare them in different formats
|
|
49
|
+
// ie.
|
|
50
|
+
// seconds(60).minutes === 1
|
|
51
|
+
// seconds(30).seconds === 30
|
|
52
|
+
// minutes(1).seconds === 60
|
|
53
|
+
// milliseconds(1000).seconds === 1
|
|
54
|
+
// milliseconds(2000).isEqualTo(seconds(2)) #=> true
|
|
55
|
+
// minutes(5).
|
|
56
|
+
// This allows a caller to just accept a TimeSpan and then work in the units it cares about
|
|
57
|
+
const minutes = (numMinutes) => TimeSpan.minutes(numMinutes);
|
|
58
|
+
const seconds = (numSeconds) => TimeSpan.seconds(numSeconds);
|
|
59
|
+
const milliseconds = (numMilliseconds) => TimeSpan.milliseconds(numMilliseconds);
|
|
60
|
+
const nothing = () => TimeSpan.nothing();
|
|
61
|
+
// Get a time span in the future
|
|
62
|
+
const timeFromNow = (fromNow) => milliseconds(Date.now() + fromNow.milliseconds);
|
|
63
|
+
// Implementation
|
|
64
|
+
class TimeSpan {
|
|
65
|
+
minutes;
|
|
66
|
+
seconds;
|
|
67
|
+
milliseconds;
|
|
68
|
+
constructor(numMinutes, numSeconds, numMilliseconds) {
|
|
69
|
+
this.minutes = numMinutes;
|
|
70
|
+
this.seconds = numSeconds;
|
|
71
|
+
this.milliseconds = numMilliseconds;
|
|
72
|
+
}
|
|
73
|
+
static minutes(numMinutes) {
|
|
74
|
+
return new TimeSpan(numMinutes, numMinutes * 60, TimeSpan.seconds(numMinutes * 60).milliseconds);
|
|
75
|
+
}
|
|
76
|
+
static seconds(numSeconds) {
|
|
77
|
+
return new TimeSpan(numSeconds / 60, numSeconds, numSeconds * 1000);
|
|
78
|
+
}
|
|
79
|
+
static milliseconds(numMilliseconds) {
|
|
80
|
+
return new TimeSpan(TimeSpan.seconds(numMilliseconds / 1000).minutes, numMilliseconds / 1000, numMilliseconds);
|
|
81
|
+
}
|
|
82
|
+
static nothing() {
|
|
83
|
+
return new TimeSpan(0, 0, 0);
|
|
84
|
+
}
|
|
85
|
+
isGreaterThan(t) {
|
|
86
|
+
return this.milliseconds > t.milliseconds;
|
|
87
|
+
}
|
|
88
|
+
isGreaterThanOrEqualTo(t) {
|
|
89
|
+
return this.milliseconds >= t.milliseconds;
|
|
90
|
+
}
|
|
91
|
+
isLessThan(t) {
|
|
92
|
+
return this.milliseconds < t.milliseconds;
|
|
93
|
+
}
|
|
94
|
+
isLessThanOrEqualTo(t) {
|
|
95
|
+
return this.milliseconds <= t.milliseconds;
|
|
96
|
+
}
|
|
97
|
+
isEqualTo(t) {
|
|
98
|
+
return this.milliseconds === t.milliseconds;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nullOnLoginSuccessCallback = _ => Promise.resolve();
|
|
103
|
+
class PrisonerAuth {
|
|
104
|
+
launchpadAuthUrl;
|
|
105
|
+
authorizationUrl;
|
|
106
|
+
tokenUrl;
|
|
107
|
+
callbackUrl;
|
|
108
|
+
onLoginSuccessCallback;
|
|
109
|
+
clientId;
|
|
110
|
+
clientSecret;
|
|
111
|
+
scope;
|
|
112
|
+
nonce;
|
|
113
|
+
tokenMinimumLifespan;
|
|
114
|
+
tokenFetcher;
|
|
115
|
+
constructor(options) {
|
|
116
|
+
this.launchpadAuthUrl = options.launchpadAuthUrl;
|
|
117
|
+
this.authorizationUrl = options.authorizationUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/authorize`;
|
|
118
|
+
this.tokenUrl = options.tokenUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/token`;
|
|
119
|
+
this.callbackUrl = options.callbackUrl ?? '/sign-in/callback';
|
|
120
|
+
this.onLoginSuccessCallback = options.onLoginSuccessCallback ?? nullOnLoginSuccessCallback;
|
|
121
|
+
this.clientId = options.clientId;
|
|
122
|
+
this.clientSecret = options.clientSecret;
|
|
123
|
+
this.scope = options.scope ?? ['user.basic.read', 'user.establishment.read', 'user.booking.read'];
|
|
124
|
+
this.nonce = options.nonce ?? true;
|
|
125
|
+
this.tokenMinimumLifespan = options.tokenMinimumLifespan;
|
|
126
|
+
this.tokenFetcher = options.tokenFetcher ?? tokenFetcher;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
*
|
|
130
|
+
* @returns OpenIDConnectStrategy ready for use with passport
|
|
131
|
+
*/
|
|
132
|
+
passportStrategy() {
|
|
133
|
+
return new OpenIDConnectStrategy({
|
|
134
|
+
issuer: this.launchpadAuthUrl,
|
|
135
|
+
authorizationURL: this.authorizationUrl,
|
|
136
|
+
tokenURL: this.tokenUrl,
|
|
137
|
+
callbackURL: this.callbackUrl,
|
|
138
|
+
clientID: this.clientId,
|
|
139
|
+
clientSecret: this.clientSecret,
|
|
140
|
+
scope: this.scope,
|
|
141
|
+
userInfoURL: '',
|
|
142
|
+
skipUserProfile: true,
|
|
143
|
+
nonce: this.nonce ? 'true' : undefined,
|
|
144
|
+
customHeaders: { authorization: this.authorizationToken() },
|
|
145
|
+
}, (async (_issuer, _profile, _context, idToken, accessToken, refreshToken, done) => {
|
|
146
|
+
const user = userFromTokens({ idToken, accessToken, refreshToken });
|
|
147
|
+
await this.onLoginSuccessCallback(user);
|
|
148
|
+
done(null, user);
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Checks the expiry of a given LaunchpadUser idToken and refreshToken.
|
|
153
|
+
* - If the idToken has expired it will attempt to refresh it using the refreshToken.
|
|
154
|
+
* - If the refreshToken has expired it will throw an Error.
|
|
155
|
+
*
|
|
156
|
+
* This method will always return a LaunchpadUser so it is suggested to updated your local copy and it will keep the tokens up to date.
|
|
157
|
+
*
|
|
158
|
+
* @param user - The LaunchpadUser instance to check token expiry on.
|
|
159
|
+
* @returns A LaunchpadUser is always returned, either the given user or a user with refreshed tokens.
|
|
160
|
+
* @throws Error is thrown if the refreshToken has expired, the user must sign in again from scratch.
|
|
161
|
+
*/
|
|
162
|
+
async validateAndRefreshUser(user) {
|
|
163
|
+
const refreshToken = tokenFromJwt(user.refreshToken);
|
|
164
|
+
const tokenExpiryTime = timeFromNow(this.tokenMinimumLifespan);
|
|
165
|
+
// Id Token still valid, return the current user
|
|
166
|
+
if (seconds(user.idToken.exp).isGreaterThanOrEqualTo(tokenExpiryTime)) {
|
|
167
|
+
return user;
|
|
168
|
+
}
|
|
169
|
+
// Refresh token expired, cannot continue
|
|
170
|
+
if (seconds(refreshToken.exp).isLessThan(tokenExpiryTime)) {
|
|
171
|
+
throw new Error('Refresh token expired');
|
|
172
|
+
}
|
|
173
|
+
// Refresh the user tokens
|
|
174
|
+
const newTokens = await this.tokenFetcher(user.refreshToken, this.tokenUrl, this.authorizationToken());
|
|
175
|
+
return userFromTokens(newTokens);
|
|
176
|
+
}
|
|
177
|
+
authorizationToken() {
|
|
178
|
+
return oauthClientToken(this.clientId, this.clientSecret);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
exports.PrisonerAuth = PrisonerAuth;
|
|
183
|
+
exports.TimeSpan = TimeSpan;
|
|
184
|
+
exports.milliseconds = milliseconds;
|
|
185
|
+
exports.minutes = minutes;
|
|
186
|
+
exports.nothing = nothing;
|
|
187
|
+
exports.seconds = seconds;
|
|
14
188
|
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":["../src/main/
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/main/tokens.ts","../src/main/launchpadUser.ts","../src/main/timeSpans.ts","../src/main/prisonerAuth.ts"],"sourcesContent":[null,null,null,null],"names":[],"mappings":";;;;;AAsCO,MAAM,YAAY,GAAG,CAAI,KAAa,KAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;AAE/G,MAAM,gBAAgB,GAAG,CAAC,QAAgB,EAAE,YAAoB,KAAY;AACjF,IAAA,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC3E,OAAO,CAAA,MAAA,EAAS,KAAK,CAAA,CAAE;AACzB,CAAC;AAIM,MAAM,YAAY,GAAiB,CAAC,gBAAgB,EAAE,QAAQ,EAAE,mBAAmB,KAAI;IAC5F,OAAO,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,MAAM,KAAI;QAChD;aACG,IAAI,CAAC,QAAQ;AACb,aAAA,GAAG,CAAC,cAAc,EAAE,mCAAmC;AACvD,aAAA,GAAG,CAAC,eAAe,EAAE,mBAAmB;aACxC,KAAK,CAAC,EAAE,aAAa,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE;AACtE,aAAA,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,KAAI;AAChB,YAAA,IAAI,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC;AACpB,YAAA,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,IAAI;YAC9F,OAAO,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AACxD,QAAA,CAAC,CAAC;AACN,IAAA,CAAC,CAAC;AACJ,CAAC;;ACvCM,MAAM,cAAc,GAAG,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAa,KAAmB;AACjG,IAAA,MAAM,aAAa,GAAG,YAAY,CAAU,OAAO,CAAC;IACpD,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,aAAa;IAClD,MAAM,UAAU,GAAG,eAAe;IAClC,MAAM,QAAQ,GAAG,SAAS;IAE1B,OAAO;QACL,aAAa;AACb,QAAA,OAAO,EAAE,aAAa;QACtB,YAAY;QACZ,WAAW;QACX,UAAU;QACV,IAAI;AACJ,QAAA,KAAK,EAAE,OAAO;AACd,QAAA,QAAQ,EAAE,GAAG;AACb,QAAA,MAAM,EAAE,GAAG;QACX,QAAQ;AACR,QAAA,WAAW,EAAE,IAAI;AACjB,QAAA,SAAS,EAAE,EAAE;KACd;AACH,CAAC;;ACzCD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEO,MAAM,OAAO,GAAG,CAAC,UAAkB,KAAe,QAAQ,CAAC,OAAO,CAAC,UAAU;AAE7E,MAAM,OAAO,GAAG,CAAC,UAAkB,KAAe,QAAQ,CAAC,OAAO,CAAC,UAAU;AAE7E,MAAM,YAAY,GAAG,CAAC,eAAuB,KAAe,QAAQ,CAAC,YAAY,CAAC,eAAe;AAEjG,MAAM,OAAO,GAAG,MAAgB,QAAQ,CAAC,OAAO;AAKvD;AACO,MAAM,WAAW,GAAG,CAAC,OAAiB,KAAK,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC;AAEjG;MACa,QAAQ,CAAA;AACV,IAAA,OAAO;AAEP,IAAA,OAAO;AAEP,IAAA,YAAY;AAErB,IAAA,WAAA,CAAoB,UAAkB,EAAE,UAAkB,EAAE,eAAuB,EAAA;AACjF,QAAA,IAAI,CAAC,OAAO,GAAG,UAAU;AACzB,QAAA,IAAI,CAAC,OAAO,GAAG,UAAU;AACzB,QAAA,IAAI,CAAC,YAAY,GAAG,eAAe;IACrC;IAEA,OAAO,OAAO,CAAC,UAAkB,EAAA;QAC/B,OAAO,IAAI,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC,YAAY,CAAC;IAClG;IAEA,OAAO,OAAO,CAAC,UAAkB,EAAA;AAC/B,QAAA,OAAO,IAAI,QAAQ,CAAC,UAAU,GAAG,EAAE,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IACrE;IAEA,OAAO,YAAY,CAAC,eAAuB,EAAA;QACzC,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,EAAE,eAAe,CAAC;IAChH;AAEA,IAAA,OAAO,OAAO,GAAA;QACZ,OAAO,IAAI,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9B;AAEA,IAAA,aAAa,CAAC,CAAW,EAAA;AACvB,QAAA,OAAO,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,YAAY;IAC3C;AAEA,IAAA,sBAAsB,CAAC,CAAW,EAAA;AAChC,QAAA,OAAO,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC,YAAY;IAC5C;AAEA,IAAA,UAAU,CAAC,CAAW,EAAA;AACpB,QAAA,OAAO,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,YAAY;IAC3C;AAEA,IAAA,mBAAmB,CAAC,CAAW,EAAA;AAC7B,QAAA,OAAO,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC,YAAY;IAC5C;AAEA,IAAA,SAAS,CAAC,CAAW,EAAA;AACnB,QAAA,OAAO,IAAI,CAAC,YAAY,KAAK,CAAC,CAAC,YAAY;IAC7C;AACD;;ACOD,MAAM,0BAA0B,GAA2B,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE;AAEnE,MAAO,YAAY,CAAA;AACtB,IAAA,gBAAgB;AAEhB,IAAA,gBAAgB;AAEhB,IAAA,QAAQ;AAER,IAAA,WAAW;AAEX,IAAA,sBAAsB;AAEtB,IAAA,QAAQ;AAER,IAAA,YAAY;AAEZ,IAAA,KAAK;AAEL,IAAA,KAAK;AAEL,IAAA,oBAAoB;AAEpB,IAAA,YAAY;AAErB,IAAA,WAAA,CAAY,OAA4B,EAAA;AACtC,QAAA,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB;AAChD,QAAA,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,CAAA,EAAG,OAAO,CAAC,gBAAgB,CAAA,oBAAA,CAAsB;AACrG,QAAA,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAA,EAAG,OAAO,CAAC,gBAAgB,CAAA,gBAAA,CAAkB;QACjF,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,mBAAmB;QAC7D,IAAI,CAAC,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,IAAI,0BAA0B;AAC1F,QAAA,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ;AAChC,QAAA,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY;AACxC,QAAA,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC,iBAAiB,EAAE,yBAAyB,EAAE,mBAAmB,CAAC;QACjG,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,IAAI;AAClC,QAAA,IAAI,CAAC,oBAAoB,GAAG,OAAO,CAAC,oBAAoB;QACxD,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,YAAY;IAC1D;AAEA;;;AAGG;IACH,gBAAgB,GAAA;QACd,OAAO,IAAI,qBAAqB,CAC9B;YACE,MAAM,EAAE,IAAI,CAAC,gBAAgB;YAC7B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;YACvC,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,KAAK,EAAE,IAAI,CAAC,KAAK;AACjB,YAAA,WAAW,EAAE,EAAE;AACf,YAAA,eAAe,EAAE,IAAI;YACrB,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,MAAM,GAAG,SAAS;YACtC,aAAa,EAAE,EAAE,aAAa,EAAE,IAAI,CAAC,kBAAkB,EAAE,EAAE;AAC5D,SAAA,GACA,OACC,OAAe,EACf,QAAiB,EACjB,QAAgB,EAChB,OAAe,EACf,WAAmB,EACnB,YAAoB,EACpB,IAAoB,KAClB;AACF,YAAA,MAAM,IAAI,GAAG,cAAc,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AACnE,YAAA,MAAM,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC;AACvC,YAAA,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC;QAClB,CAAC,EACF;IACH;AAEA;;;;;;;;;;AAUG;IACH,MAAM,sBAAsB,CAAC,IAAmB,EAAA;QAC9C,MAAM,YAAY,GAAG,YAAY,CAAe,IAAI,CAAC,YAAY,CAAC;QAClE,MAAM,eAAe,GAAG,WAAW,CAAC,IAAI,CAAC,oBAAoB,CAAC;;AAG9D,QAAA,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,sBAAsB,CAAC,eAAe,CAAC,EAAE;AACrE,YAAA,OAAO,IAAI;QACb;;AAGA,QAAA,IAAI,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE;AACzD,YAAA,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC;QAC1C;;QAGA,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,kBAAkB,EAAE,CAAC;AACtG,QAAA,OAAO,cAAc,CAAC,SAAS,CAAC;IAClC;IAEQ,kBAAkB,GAAA;QACxB,OAAO,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC;IAC3D;AACD;;;;;;;"}
|
package/dist/index.esm.js
CHANGED
|
@@ -1,12 +1,181 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import OpenIDConnectStrategy from 'passport-openidconnect';
|
|
2
|
+
import superagent from 'superagent';
|
|
3
|
+
|
|
4
|
+
const tokenFromJwt = (token) => JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
5
|
+
const oauthClientToken = (clientId, clientSecret) => {
|
|
6
|
+
const token = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
7
|
+
return `Basic ${token}`;
|
|
8
|
+
};
|
|
9
|
+
const tokenFetcher = (userRefreshToken, tokenUrl, authorizationHeader) => {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
superagent
|
|
12
|
+
.post(tokenUrl)
|
|
13
|
+
.set('Content-Type', 'application/x-www-form-urlencoded')
|
|
14
|
+
.set('Authorization', authorizationHeader)
|
|
15
|
+
.query({ refresh_token: userRefreshToken, grant_type: 'refresh_token' })
|
|
16
|
+
.end((err, res) => {
|
|
17
|
+
if (err)
|
|
18
|
+
reject(err);
|
|
19
|
+
const { id_token: idToken, access_token: accessToken, refresh_token: refreshToken } = res.body;
|
|
20
|
+
return resolve({ idToken, accessToken, refreshToken });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const userFromTokens = ({ idToken, accessToken, refreshToken }) => {
|
|
26
|
+
const parsedIdToken = tokenFromJwt(idToken);
|
|
27
|
+
const { name, sub, establishment } = parsedIdToken;
|
|
28
|
+
const authSource = 'prisoner-auth';
|
|
29
|
+
const userUuid = undefined;
|
|
30
|
+
return {
|
|
31
|
+
establishment,
|
|
32
|
+
idToken: parsedIdToken,
|
|
33
|
+
refreshToken,
|
|
34
|
+
accessToken,
|
|
35
|
+
authSource,
|
|
36
|
+
name,
|
|
37
|
+
token: idToken,
|
|
38
|
+
username: sub,
|
|
39
|
+
userId: sub,
|
|
40
|
+
userUuid,
|
|
41
|
+
displayName: name,
|
|
42
|
+
userRoles: [],
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// A TimeSpan allows you to give meaning to a number and then read that number back and compare them in different formats
|
|
47
|
+
// ie.
|
|
48
|
+
// seconds(60).minutes === 1
|
|
49
|
+
// seconds(30).seconds === 30
|
|
50
|
+
// minutes(1).seconds === 60
|
|
51
|
+
// milliseconds(1000).seconds === 1
|
|
52
|
+
// milliseconds(2000).isEqualTo(seconds(2)) #=> true
|
|
53
|
+
// minutes(5).
|
|
54
|
+
// This allows a caller to just accept a TimeSpan and then work in the units it cares about
|
|
55
|
+
const minutes = (numMinutes) => TimeSpan.minutes(numMinutes);
|
|
56
|
+
const seconds = (numSeconds) => TimeSpan.seconds(numSeconds);
|
|
57
|
+
const milliseconds = (numMilliseconds) => TimeSpan.milliseconds(numMilliseconds);
|
|
58
|
+
const nothing = () => TimeSpan.nothing();
|
|
59
|
+
// Get a time span in the future
|
|
60
|
+
const timeFromNow = (fromNow) => milliseconds(Date.now() + fromNow.milliseconds);
|
|
61
|
+
// Implementation
|
|
62
|
+
class TimeSpan {
|
|
63
|
+
minutes;
|
|
64
|
+
seconds;
|
|
65
|
+
milliseconds;
|
|
66
|
+
constructor(numMinutes, numSeconds, numMilliseconds) {
|
|
67
|
+
this.minutes = numMinutes;
|
|
68
|
+
this.seconds = numSeconds;
|
|
69
|
+
this.milliseconds = numMilliseconds;
|
|
70
|
+
}
|
|
71
|
+
static minutes(numMinutes) {
|
|
72
|
+
return new TimeSpan(numMinutes, numMinutes * 60, TimeSpan.seconds(numMinutes * 60).milliseconds);
|
|
73
|
+
}
|
|
74
|
+
static seconds(numSeconds) {
|
|
75
|
+
return new TimeSpan(numSeconds / 60, numSeconds, numSeconds * 1000);
|
|
76
|
+
}
|
|
77
|
+
static milliseconds(numMilliseconds) {
|
|
78
|
+
return new TimeSpan(TimeSpan.seconds(numMilliseconds / 1000).minutes, numMilliseconds / 1000, numMilliseconds);
|
|
79
|
+
}
|
|
80
|
+
static nothing() {
|
|
81
|
+
return new TimeSpan(0, 0, 0);
|
|
82
|
+
}
|
|
83
|
+
isGreaterThan(t) {
|
|
84
|
+
return this.milliseconds > t.milliseconds;
|
|
85
|
+
}
|
|
86
|
+
isGreaterThanOrEqualTo(t) {
|
|
87
|
+
return this.milliseconds >= t.milliseconds;
|
|
88
|
+
}
|
|
89
|
+
isLessThan(t) {
|
|
90
|
+
return this.milliseconds < t.milliseconds;
|
|
91
|
+
}
|
|
92
|
+
isLessThanOrEqualTo(t) {
|
|
93
|
+
return this.milliseconds <= t.milliseconds;
|
|
94
|
+
}
|
|
95
|
+
isEqualTo(t) {
|
|
96
|
+
return this.milliseconds === t.milliseconds;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nullOnLoginSuccessCallback = _ => Promise.resolve();
|
|
101
|
+
class PrisonerAuth {
|
|
102
|
+
launchpadAuthUrl;
|
|
103
|
+
authorizationUrl;
|
|
104
|
+
tokenUrl;
|
|
105
|
+
callbackUrl;
|
|
106
|
+
onLoginSuccessCallback;
|
|
107
|
+
clientId;
|
|
108
|
+
clientSecret;
|
|
109
|
+
scope;
|
|
110
|
+
nonce;
|
|
111
|
+
tokenMinimumLifespan;
|
|
112
|
+
tokenFetcher;
|
|
113
|
+
constructor(options) {
|
|
114
|
+
this.launchpadAuthUrl = options.launchpadAuthUrl;
|
|
115
|
+
this.authorizationUrl = options.authorizationUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/authorize`;
|
|
116
|
+
this.tokenUrl = options.tokenUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/token`;
|
|
117
|
+
this.callbackUrl = options.callbackUrl ?? '/sign-in/callback';
|
|
118
|
+
this.onLoginSuccessCallback = options.onLoginSuccessCallback ?? nullOnLoginSuccessCallback;
|
|
119
|
+
this.clientId = options.clientId;
|
|
120
|
+
this.clientSecret = options.clientSecret;
|
|
121
|
+
this.scope = options.scope ?? ['user.basic.read', 'user.establishment.read', 'user.booking.read'];
|
|
122
|
+
this.nonce = options.nonce ?? true;
|
|
123
|
+
this.tokenMinimumLifespan = options.tokenMinimumLifespan;
|
|
124
|
+
this.tokenFetcher = options.tokenFetcher ?? tokenFetcher;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
*
|
|
128
|
+
* @returns OpenIDConnectStrategy ready for use with passport
|
|
129
|
+
*/
|
|
130
|
+
passportStrategy() {
|
|
131
|
+
return new OpenIDConnectStrategy({
|
|
132
|
+
issuer: this.launchpadAuthUrl,
|
|
133
|
+
authorizationURL: this.authorizationUrl,
|
|
134
|
+
tokenURL: this.tokenUrl,
|
|
135
|
+
callbackURL: this.callbackUrl,
|
|
136
|
+
clientID: this.clientId,
|
|
137
|
+
clientSecret: this.clientSecret,
|
|
138
|
+
scope: this.scope,
|
|
139
|
+
userInfoURL: '',
|
|
140
|
+
skipUserProfile: true,
|
|
141
|
+
nonce: this.nonce ? 'true' : undefined,
|
|
142
|
+
customHeaders: { authorization: this.authorizationToken() },
|
|
143
|
+
}, (async (_issuer, _profile, _context, idToken, accessToken, refreshToken, done) => {
|
|
144
|
+
const user = userFromTokens({ idToken, accessToken, refreshToken });
|
|
145
|
+
await this.onLoginSuccessCallback(user);
|
|
146
|
+
done(null, user);
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Checks the expiry of a given LaunchpadUser idToken and refreshToken.
|
|
151
|
+
* - If the idToken has expired it will attempt to refresh it using the refreshToken.
|
|
152
|
+
* - If the refreshToken has expired it will throw an Error.
|
|
153
|
+
*
|
|
154
|
+
* This method will always return a LaunchpadUser so it is suggested to updated your local copy and it will keep the tokens up to date.
|
|
155
|
+
*
|
|
156
|
+
* @param user - The LaunchpadUser instance to check token expiry on.
|
|
157
|
+
* @returns A LaunchpadUser is always returned, either the given user or a user with refreshed tokens.
|
|
158
|
+
* @throws Error is thrown if the refreshToken has expired, the user must sign in again from scratch.
|
|
159
|
+
*/
|
|
160
|
+
async validateAndRefreshUser(user) {
|
|
161
|
+
const refreshToken = tokenFromJwt(user.refreshToken);
|
|
162
|
+
const tokenExpiryTime = timeFromNow(this.tokenMinimumLifespan);
|
|
163
|
+
// Id Token still valid, return the current user
|
|
164
|
+
if (seconds(user.idToken.exp).isGreaterThanOrEqualTo(tokenExpiryTime)) {
|
|
165
|
+
return user;
|
|
166
|
+
}
|
|
167
|
+
// Refresh token expired, cannot continue
|
|
168
|
+
if (seconds(refreshToken.exp).isLessThan(tokenExpiryTime)) {
|
|
169
|
+
throw new Error('Refresh token expired');
|
|
170
|
+
}
|
|
171
|
+
// Refresh the user tokens
|
|
172
|
+
const newTokens = await this.tokenFetcher(user.refreshToken, this.tokenUrl, this.authorizationToken());
|
|
173
|
+
return userFromTokens(newTokens);
|
|
174
|
+
}
|
|
175
|
+
authorizationToken() {
|
|
176
|
+
return oauthClientToken(this.clientId, this.clientSecret);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { PrisonerAuth, TimeSpan, milliseconds, minutes, nothing, seconds };
|
|
12
181
|
//# sourceMappingURL=index.esm.js.map
|
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.js","sources":["../src/main/
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/main/tokens.ts","../src/main/launchpadUser.ts","../src/main/timeSpans.ts","../src/main/prisonerAuth.ts"],"sourcesContent":[null,null,null,null],"names":[],"mappings":"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"}
|