@ministryofjustice/hmpps-prisoner-auth 0.0.1
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/README.md +124 -0
- package/dist/index.cjs.js +182 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.esm.js +175 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/main/index.d.ts +3 -0
- package/dist/main/launchpadAuthStrategy.d.ts +11 -0
- package/dist/main/launchpadUser.d.ts +17 -0
- package/dist/main/launchpadUser.test.d.ts +1 -0
- package/dist/main/prisonerAuth.d.ts +97 -0
- package/dist/main/prisonerAuth.test.d.ts +1 -0
- package/dist/main/prisonerAuthStrategy.d.ts +12 -0
- package/dist/main/prisonerAuthStrategy.test.d.ts +1 -0
- package/dist/main/testUtils.d.ts +9 -0
- package/dist/main/timeSpans.d.ts +21 -0
- package/dist/main/timeSpans.test.d.ts +1 -0
- package/dist/main/tokenRefresh.d.ts +3 -0
- package/dist/main/tokens.d.ts +40 -0
- package/package.json +47 -0
- package/src/main/index.ts +3 -0
- package/src/main/launchpadUser.test.ts +41 -0
- package/src/main/launchpadUser.ts +42 -0
- package/src/main/prisonerAuth.test.ts +117 -0
- package/src/main/prisonerAuth.ts +174 -0
- package/src/main/testUtils.ts +10 -0
- package/src/main/timeSpans.test.ts +129 -0
- package/src/main/timeSpans.ts +74 -0
- package/src/main/tokens.ts +65 -0
- package/src/misc/install.diff +154 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Prisoner Auth
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
### Quick installation
|
|
6
|
+
|
|
7
|
+
The following steps will give an opinionated out of the box setup for applications where a launchpad user will be the main user in the system.
|
|
8
|
+
|
|
9
|
+
1. Add the package `@ministryofjustice/hmpps-prisoner-auth` to your `package.json`
|
|
10
|
+
```
|
|
11
|
+
$ npm add --save @ministryofjustice/hmpps-prisoner-auth
|
|
12
|
+
$ npm i
|
|
13
|
+
```
|
|
14
|
+
2. Copy the contents of [src/misc/install.diff](src/misc/install.diff) to your local folder
|
|
15
|
+
```
|
|
16
|
+
$ curl https://raw.githubusercontent.com/ministryofjustice/hmpps-prisoner-facing-typescript-lib/refs/heads/main/packages/prisoner-auth/src/misc/install.diff > install.diff
|
|
17
|
+
```
|
|
18
|
+
3. Apply the diff to carry out all the required changes
|
|
19
|
+
```
|
|
20
|
+
$ git apply install.diff
|
|
21
|
+
```
|
|
22
|
+
4. Make sure to look through the applied changes and remove the diff file before committing.
|
|
23
|
+
|
|
24
|
+
### Manual installation
|
|
25
|
+
|
|
26
|
+
1. Add the package `@ministryofjustice/hmpps-prisoner-auth` to your `package.json`
|
|
27
|
+
```
|
|
28
|
+
$ npm add --save @ministryofjustice/hmpps-prisoner-auth
|
|
29
|
+
$ npm i
|
|
30
|
+
```
|
|
31
|
+
2. Modify `interfaces/hmppsUser.ts` to make the following additions:
|
|
32
|
+
```
|
|
33
|
+
# add prisoner-auth to the AuthSource type
|
|
34
|
+
export type AuthSource = 'nomis' | 'delius' | 'external' | 'azuread' | 'prisoner-auth'
|
|
35
|
+
|
|
36
|
+
# add LaunchpadUser to the HmppsUser type
|
|
37
|
+
export type HmppsUser = PrisonUser | ProbationUser | ExternalUser | AzureADUser | LaunchpadUser
|
|
38
|
+
```
|
|
39
|
+
These changes will help smooth over alot of assumptions made in the typescript template about the type of fields the user will have.
|
|
40
|
+
|
|
41
|
+
3. Modify `server/middleware/setupAuthentication.ts` to add prisoner auth as the passport strategy (see below for the full list of config options):
|
|
42
|
+
```
|
|
43
|
+
passport.use(
|
|
44
|
+
'prisoner-auth',
|
|
45
|
+
prisonerAuthStrategy(
|
|
46
|
+
{
|
|
47
|
+
launchpadAuthUrl: 'https://launchpad.instance.etc' ...
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
Note: it is recommended to give the strategy a name ('prisoner-auth' here) for later referral with passport.authenticate middleware.
|
|
53
|
+
|
|
54
|
+
4. Modify `server/middleware/setupAuthentication.ts` to remove any hmpps auth specific code and add in prisoner auth setup. The following code snippet is intended as a guide:
|
|
55
|
+
```
|
|
56
|
+
export default function setupAuthentication() {
|
|
57
|
+
const router = Router()
|
|
58
|
+
|
|
59
|
+
router.use(passport.initialize())
|
|
60
|
+
router.use(passport.session())
|
|
61
|
+
router.use(flash())
|
|
62
|
+
|
|
63
|
+
router.get('/autherror', (req, res) => {
|
|
64
|
+
res.status(401)
|
|
65
|
+
return res.render('autherror')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
router.get('/sign-in', passport.authenticate('prisoner-auth'))
|
|
69
|
+
|
|
70
|
+
router.get('/sign-in/callback', (req, res, next) =>
|
|
71
|
+
passport.authenticate('prisoner-auth', {
|
|
72
|
+
successReturnToOrRedirect: req.session.returnTo || '/',
|
|
73
|
+
failureRedirect: '/autherror',
|
|
74
|
+
})(req, res, next),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
router.use('/sign-out', (req, res, next) => {
|
|
78
|
+
if (req.user) {
|
|
79
|
+
req.logout(err => {
|
|
80
|
+
if (err) return next(err)
|
|
81
|
+
return req.session.destroy(() => res.redirect('/'))
|
|
82
|
+
})
|
|
83
|
+
} else res.redirect('/')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
router.use(async (req, res, next) => {
|
|
87
|
+
if (!req.isAuthenticated()) {
|
|
88
|
+
req.session.returnTo = req.originalUrl
|
|
89
|
+
return res.redirect('/sign-in')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return prisonerAuth
|
|
93
|
+
.validateAndRefreshUser(req.user as LaunchpadUser)
|
|
94
|
+
.then(user => {
|
|
95
|
+
req.user = user
|
|
96
|
+
next()
|
|
97
|
+
})
|
|
98
|
+
.catch(() => res.redirect('/autherror'))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
router.use((req, res, next) => {
|
|
102
|
+
res.locals.user = req.user as HmppsUser
|
|
103
|
+
next()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return router
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
5. You may wish to remove the following middleware from `server/app.ts` as they were not designed for use with `LaunchpadUser`:
|
|
110
|
+
- `authorisationMiddleware()` - this checks the users `ROLE_` which a launchpad user will not have.
|
|
111
|
+
- `setUpCurrentUser()` - this sets up the `res.locals.user` fields which a launchpad will have already (`name`, `displayName` etc).
|
|
112
|
+
|
|
113
|
+
### Configuration Options
|
|
114
|
+
|
|
115
|
+
| Option | Description |
|
|
116
|
+
| - | - |
|
|
117
|
+
| launchpadAuthUrl | The full URL of the launchpad auth instance you wish to authenticate against |
|
|
118
|
+
| clientID | The client ID |
|
|
119
|
+
| clientSecret | The client secret |
|
|
120
|
+
| callbackURL | *Optional, The URL or path you wish launchpad to call back to in the auth flow. Defaults to `/signin/callback` |
|
|
121
|
+
| scope | *Optional, One or more OpenIdConnect Scopes you require for the application. Defaults to `['user.basic.read', 'user.establishment.read', 'user.booking.read']` |
|
|
122
|
+
| tokenMinimumLifespan | The least amount of time a token must have left before it is considered expired. Expressed as a TimeSpan. eg. `minutes(5)` or `nothing()`. |
|
|
123
|
+
|
|
124
|
+
See `PrisonerAuthOptions` for full list and documentation for these options.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
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 { establishment, name, given_name: givenName, family_name: familyName, sub } = tokenFromJwt(idToken);
|
|
29
|
+
const authSource = 'prisoner-auth';
|
|
30
|
+
return {
|
|
31
|
+
authSource,
|
|
32
|
+
idToken,
|
|
33
|
+
refreshToken,
|
|
34
|
+
accessToken,
|
|
35
|
+
establishment,
|
|
36
|
+
name,
|
|
37
|
+
givenName,
|
|
38
|
+
familyName,
|
|
39
|
+
// The following fields are for compatibility with HmppsUser only
|
|
40
|
+
token: idToken,
|
|
41
|
+
username: name,
|
|
42
|
+
userId: sub,
|
|
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
|
+
class PrisonerAuth {
|
|
103
|
+
launchpadAuthUrl;
|
|
104
|
+
authorizationUrl;
|
|
105
|
+
tokenUrl;
|
|
106
|
+
callbackUrl;
|
|
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.clientId = options.clientId;
|
|
119
|
+
this.clientSecret = options.clientSecret;
|
|
120
|
+
this.scope = options.scope ?? ['user.basic.read', 'user.establishment.read', 'user.booking.read'];
|
|
121
|
+
this.nonce = options.nonce ?? true;
|
|
122
|
+
this.tokenMinimumLifespan = options.tokenMinimumLifespan;
|
|
123
|
+
this.tokenFetcher = options.tokenFetcher ?? tokenFetcher;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
*
|
|
127
|
+
* @returns OpenIDConnectStrategy ready for use with passport
|
|
128
|
+
*/
|
|
129
|
+
passportStrategy() {
|
|
130
|
+
return new OpenIDConnectStrategy({
|
|
131
|
+
issuer: this.launchpadAuthUrl,
|
|
132
|
+
authorizationURL: this.authorizationUrl,
|
|
133
|
+
tokenURL: this.tokenUrl,
|
|
134
|
+
callbackURL: this.callbackUrl,
|
|
135
|
+
clientID: this.clientId,
|
|
136
|
+
clientSecret: this.clientSecret,
|
|
137
|
+
scope: this.scope,
|
|
138
|
+
userInfoURL: '',
|
|
139
|
+
skipUserProfile: true,
|
|
140
|
+
nonce: this.nonce ? 'true' : undefined,
|
|
141
|
+
customHeaders: { authorization: this.authorizationToken() },
|
|
142
|
+
}, ((_issuer, _profile, _context, idToken, accessToken, refreshToken, done) => done(null, userFromTokens({ idToken, accessToken, refreshToken }))));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Checks the expiry of a given LaunchpadUser idToken and refreshToken.
|
|
146
|
+
* - If the idToken has expired it will attempt to refresh it using the refreshToken.
|
|
147
|
+
* - If the refreshToken has expired it will throw an Error.
|
|
148
|
+
*
|
|
149
|
+
* 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.
|
|
150
|
+
*
|
|
151
|
+
* @param user - The LaunchpadUser instance to check token expiry on.
|
|
152
|
+
* @returns A LaunchpadUser is always returned, either the given user or a user with refreshed tokens.
|
|
153
|
+
* @throws Error is thrown if the refreshToken has expired, the user must sign in again from scratch.
|
|
154
|
+
*/
|
|
155
|
+
async validateAndRefreshUser(user) {
|
|
156
|
+
const idToken = tokenFromJwt(user.idToken);
|
|
157
|
+
const refreshToken = tokenFromJwt(user.refreshToken);
|
|
158
|
+
const tokenExpiryTime = timeFromNow(this.tokenMinimumLifespan);
|
|
159
|
+
// Id Token still valid, return the current user
|
|
160
|
+
if (seconds(idToken.exp).isGreaterThanOrEqualTo(tokenExpiryTime)) {
|
|
161
|
+
return user;
|
|
162
|
+
}
|
|
163
|
+
// Refresh token expired, cannot continue
|
|
164
|
+
if (seconds(refreshToken.exp).isLessThan(tokenExpiryTime)) {
|
|
165
|
+
throw new Error('Refresh token expired');
|
|
166
|
+
}
|
|
167
|
+
// Refresh the user tokens
|
|
168
|
+
const newTokens = await this.tokenFetcher(user.refreshToken, this.tokenUrl, this.authorizationToken());
|
|
169
|
+
return userFromTokens(newTokens);
|
|
170
|
+
}
|
|
171
|
+
authorizationToken() {
|
|
172
|
+
return oauthClientToken(this.clientId, this.clientSecret);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
exports.PrisonerAuth = PrisonerAuth;
|
|
177
|
+
exports.TimeSpan = TimeSpan;
|
|
178
|
+
exports.milliseconds = milliseconds;
|
|
179
|
+
exports.minutes = minutes;
|
|
180
|
+
exports.nothing = nothing;
|
|
181
|
+
exports.seconds = seconds;
|
|
182
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import OpenIDConnectStrategy from 'passport-openidconnect';
|
|
2
|
+
|
|
3
|
+
type IdToken = {
|
|
4
|
+
name: string;
|
|
5
|
+
given_name: string;
|
|
6
|
+
family_name: string;
|
|
7
|
+
nonce?: string;
|
|
8
|
+
iat: number;
|
|
9
|
+
aud: string;
|
|
10
|
+
sub: string;
|
|
11
|
+
exp: number;
|
|
12
|
+
booking: {
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
establishment: {
|
|
16
|
+
agency_id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
display_name: string;
|
|
19
|
+
youth: boolean;
|
|
20
|
+
};
|
|
21
|
+
iss: string;
|
|
22
|
+
};
|
|
23
|
+
type RawTokens = {
|
|
24
|
+
idToken: string;
|
|
25
|
+
refreshToken: string;
|
|
26
|
+
accessToken: string;
|
|
27
|
+
};
|
|
28
|
+
type Scope = 'user.basic.read' | 'user.establishment.read' | 'user.booking.read' | 'user.clients.read' | 'user.clients.delete';
|
|
29
|
+
type TokenFetcher = (refreshToken: string, tokenUrl: string, authorizationHeader: string) => Promise<RawTokens>;
|
|
30
|
+
|
|
31
|
+
type LaunchpadUser = {
|
|
32
|
+
authSource: 'prisoner-auth';
|
|
33
|
+
idToken: string;
|
|
34
|
+
refreshToken: string;
|
|
35
|
+
accessToken: string;
|
|
36
|
+
establishment: IdToken['establishment'];
|
|
37
|
+
name: string;
|
|
38
|
+
givenName: string;
|
|
39
|
+
familyName: string;
|
|
40
|
+
token: string;
|
|
41
|
+
username: string;
|
|
42
|
+
userId: string;
|
|
43
|
+
displayName: string;
|
|
44
|
+
userRoles: string[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
declare const minutes: (numMinutes: number) => TimeSpan;
|
|
48
|
+
declare const seconds: (numSeconds: number) => TimeSpan;
|
|
49
|
+
declare const milliseconds: (numMilliseconds: number) => TimeSpan;
|
|
50
|
+
declare const nothing: () => TimeSpan;
|
|
51
|
+
declare class TimeSpan {
|
|
52
|
+
readonly minutes: number;
|
|
53
|
+
readonly seconds: number;
|
|
54
|
+
readonly milliseconds: number;
|
|
55
|
+
private constructor();
|
|
56
|
+
static minutes(numMinutes: number): TimeSpan;
|
|
57
|
+
static seconds(numSeconds: number): TimeSpan;
|
|
58
|
+
static milliseconds(numMilliseconds: number): TimeSpan;
|
|
59
|
+
static nothing(): TimeSpan;
|
|
60
|
+
isGreaterThan(t: TimeSpan): boolean;
|
|
61
|
+
isGreaterThanOrEqualTo(t: TimeSpan): boolean;
|
|
62
|
+
isLessThan(t: TimeSpan): boolean;
|
|
63
|
+
isLessThanOrEqualTo(t: TimeSpan): boolean;
|
|
64
|
+
isEqualTo(t: TimeSpan): boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type PrisonerAuthOptions = {
|
|
68
|
+
/** The full URL of the Launchpad Auth instance */
|
|
69
|
+
launchpadAuthUrl: string;
|
|
70
|
+
/**
|
|
71
|
+
* Optional: The full URL of the authorisation path on the Launchpad Auth instance.
|
|
72
|
+
*
|
|
73
|
+
* If omitted, authorizationUrl will default to $launchpadAuthUrl/v1/oauth2/authorize
|
|
74
|
+
*/
|
|
75
|
+
authorizationUrl?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Optional: The full URL of the token path on the Launchpad Auth instance.
|
|
78
|
+
*
|
|
79
|
+
* If omitted, tokenUrl will default to $launchpadAuthUrl/v1/oauth2/token
|
|
80
|
+
*/
|
|
81
|
+
tokenUrl?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Optional: The URL or path that Launchpad Auth will be told to call back to with the tokens.
|
|
84
|
+
*
|
|
85
|
+
* If omitted, callbackUrl will default to /sign-in/callback
|
|
86
|
+
*/
|
|
87
|
+
callbackUrl?: string;
|
|
88
|
+
/** The Client ID of this application to send to Launchpad Auth */
|
|
89
|
+
clientId: string;
|
|
90
|
+
/** The Client Secret of this application to send to Launchpad Auth */
|
|
91
|
+
clientSecret: string;
|
|
92
|
+
/**
|
|
93
|
+
* Optional: The scope or scopes to request on behalf of the user.
|
|
94
|
+
*
|
|
95
|
+
* If omitted, scope will default to user.basic.read, user.establishment.read and user.booking.read
|
|
96
|
+
*/
|
|
97
|
+
scope?: Scope | Scope[];
|
|
98
|
+
/**
|
|
99
|
+
* Optional: Whether or not to use nonce, not recommended to be disabled except for integration tests
|
|
100
|
+
*
|
|
101
|
+
* If ommitted. defaults to true
|
|
102
|
+
*/
|
|
103
|
+
nonce?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* A time span specifying the least amount of time a token must have left before it is considered expired.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
*
|
|
109
|
+
* Token will be considered expired when its exp value is in the past.
|
|
110
|
+
* ```ts
|
|
111
|
+
* nothing()
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* Token will be considered expired unless its exp value is **AT LEAST 5** minutes in the future from time of checking.
|
|
115
|
+
* ```ts
|
|
116
|
+
* minutes(5)
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @see TimeSpan
|
|
120
|
+
*/
|
|
121
|
+
tokenMinimumLifespan: TimeSpan;
|
|
122
|
+
/**
|
|
123
|
+
* Optional: Token fetcher implementation. Intended for unit testing PrisonerAuth
|
|
124
|
+
*
|
|
125
|
+
* @see TokenFetcher
|
|
126
|
+
*/
|
|
127
|
+
tokenFetcher?: TokenFetcher;
|
|
128
|
+
};
|
|
129
|
+
declare class PrisonerAuth {
|
|
130
|
+
readonly launchpadAuthUrl: string;
|
|
131
|
+
readonly authorizationUrl: string;
|
|
132
|
+
readonly tokenUrl: string;
|
|
133
|
+
readonly callbackUrl: string;
|
|
134
|
+
readonly clientId: string;
|
|
135
|
+
readonly clientSecret: string;
|
|
136
|
+
readonly scope: Scope | Scope[];
|
|
137
|
+
readonly nonce: boolean;
|
|
138
|
+
readonly tokenMinimumLifespan: TimeSpan;
|
|
139
|
+
readonly tokenFetcher: TokenFetcher;
|
|
140
|
+
constructor(options: PrisonerAuthOptions);
|
|
141
|
+
/**
|
|
142
|
+
*
|
|
143
|
+
* @returns OpenIDConnectStrategy ready for use with passport
|
|
144
|
+
*/
|
|
145
|
+
passportStrategy(): OpenIDConnectStrategy;
|
|
146
|
+
/**
|
|
147
|
+
* Checks the expiry of a given LaunchpadUser idToken and refreshToken.
|
|
148
|
+
* - If the idToken has expired it will attempt to refresh it using the refreshToken.
|
|
149
|
+
* - If the refreshToken has expired it will throw an Error.
|
|
150
|
+
*
|
|
151
|
+
* 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.
|
|
152
|
+
*
|
|
153
|
+
* @param user - The LaunchpadUser instance to check token expiry on.
|
|
154
|
+
* @returns A LaunchpadUser is always returned, either the given user or a user with refreshed tokens.
|
|
155
|
+
* @throws Error is thrown if the refreshToken has expired, the user must sign in again from scratch.
|
|
156
|
+
*/
|
|
157
|
+
validateAndRefreshUser(user: LaunchpadUser): Promise<LaunchpadUser>;
|
|
158
|
+
private authorizationToken;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { PrisonerAuth, TimeSpan, milliseconds, minutes, nothing, seconds };
|
|
162
|
+
export type { LaunchpadUser, PrisonerAuthOptions };
|
|
@@ -0,0 +1,175 @@
|
|
|
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 { establishment, name, given_name: givenName, family_name: familyName, sub } = tokenFromJwt(idToken);
|
|
27
|
+
const authSource = 'prisoner-auth';
|
|
28
|
+
return {
|
|
29
|
+
authSource,
|
|
30
|
+
idToken,
|
|
31
|
+
refreshToken,
|
|
32
|
+
accessToken,
|
|
33
|
+
establishment,
|
|
34
|
+
name,
|
|
35
|
+
givenName,
|
|
36
|
+
familyName,
|
|
37
|
+
// The following fields are for compatibility with HmppsUser only
|
|
38
|
+
token: idToken,
|
|
39
|
+
username: name,
|
|
40
|
+
userId: sub,
|
|
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
|
+
class PrisonerAuth {
|
|
101
|
+
launchpadAuthUrl;
|
|
102
|
+
authorizationUrl;
|
|
103
|
+
tokenUrl;
|
|
104
|
+
callbackUrl;
|
|
105
|
+
clientId;
|
|
106
|
+
clientSecret;
|
|
107
|
+
scope;
|
|
108
|
+
nonce;
|
|
109
|
+
tokenMinimumLifespan;
|
|
110
|
+
tokenFetcher;
|
|
111
|
+
constructor(options) {
|
|
112
|
+
this.launchpadAuthUrl = options.launchpadAuthUrl;
|
|
113
|
+
this.authorizationUrl = options.authorizationUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/authorize`;
|
|
114
|
+
this.tokenUrl = options.tokenUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/token`;
|
|
115
|
+
this.callbackUrl = options.callbackUrl ?? '/sign-in/callback';
|
|
116
|
+
this.clientId = options.clientId;
|
|
117
|
+
this.clientSecret = options.clientSecret;
|
|
118
|
+
this.scope = options.scope ?? ['user.basic.read', 'user.establishment.read', 'user.booking.read'];
|
|
119
|
+
this.nonce = options.nonce ?? true;
|
|
120
|
+
this.tokenMinimumLifespan = options.tokenMinimumLifespan;
|
|
121
|
+
this.tokenFetcher = options.tokenFetcher ?? tokenFetcher;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
*
|
|
125
|
+
* @returns OpenIDConnectStrategy ready for use with passport
|
|
126
|
+
*/
|
|
127
|
+
passportStrategy() {
|
|
128
|
+
return new OpenIDConnectStrategy({
|
|
129
|
+
issuer: this.launchpadAuthUrl,
|
|
130
|
+
authorizationURL: this.authorizationUrl,
|
|
131
|
+
tokenURL: this.tokenUrl,
|
|
132
|
+
callbackURL: this.callbackUrl,
|
|
133
|
+
clientID: this.clientId,
|
|
134
|
+
clientSecret: this.clientSecret,
|
|
135
|
+
scope: this.scope,
|
|
136
|
+
userInfoURL: '',
|
|
137
|
+
skipUserProfile: true,
|
|
138
|
+
nonce: this.nonce ? 'true' : undefined,
|
|
139
|
+
customHeaders: { authorization: this.authorizationToken() },
|
|
140
|
+
}, ((_issuer, _profile, _context, idToken, accessToken, refreshToken, done) => done(null, userFromTokens({ idToken, accessToken, refreshToken }))));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Checks the expiry of a given LaunchpadUser idToken and refreshToken.
|
|
144
|
+
* - If the idToken has expired it will attempt to refresh it using the refreshToken.
|
|
145
|
+
* - If the refreshToken has expired it will throw an Error.
|
|
146
|
+
*
|
|
147
|
+
* 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.
|
|
148
|
+
*
|
|
149
|
+
* @param user - The LaunchpadUser instance to check token expiry on.
|
|
150
|
+
* @returns A LaunchpadUser is always returned, either the given user or a user with refreshed tokens.
|
|
151
|
+
* @throws Error is thrown if the refreshToken has expired, the user must sign in again from scratch.
|
|
152
|
+
*/
|
|
153
|
+
async validateAndRefreshUser(user) {
|
|
154
|
+
const idToken = tokenFromJwt(user.idToken);
|
|
155
|
+
const refreshToken = tokenFromJwt(user.refreshToken);
|
|
156
|
+
const tokenExpiryTime = timeFromNow(this.tokenMinimumLifespan);
|
|
157
|
+
// Id Token still valid, return the current user
|
|
158
|
+
if (seconds(idToken.exp).isGreaterThanOrEqualTo(tokenExpiryTime)) {
|
|
159
|
+
return user;
|
|
160
|
+
}
|
|
161
|
+
// Refresh token expired, cannot continue
|
|
162
|
+
if (seconds(refreshToken.exp).isLessThan(tokenExpiryTime)) {
|
|
163
|
+
throw new Error('Refresh token expired');
|
|
164
|
+
}
|
|
165
|
+
// Refresh the user tokens
|
|
166
|
+
const newTokens = await this.tokenFetcher(user.refreshToken, this.tokenUrl, this.authorizationToken());
|
|
167
|
+
return userFromTokens(newTokens);
|
|
168
|
+
}
|
|
169
|
+
authorizationToken() {
|
|
170
|
+
return oauthClientToken(this.clientId, this.clientSecret);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { PrisonerAuth, TimeSpan, milliseconds, minutes, nothing, seconds };
|
|
175
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import OpenIDConnectStrategy from 'passport-openidconnect';
|
|
2
|
+
type Scope = 'user.basic.read' | 'user.establishment.read' | 'user.booking.read';
|
|
3
|
+
type LaunchpadAuthStrategyOptions = {
|
|
4
|
+
launchpadUrl: string;
|
|
5
|
+
clientID: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
callbackURL?: string;
|
|
8
|
+
scope?: Scope | Scope[];
|
|
9
|
+
};
|
|
10
|
+
export declare function launchpadAuthStrategy(options: LaunchpadAuthStrategyOptions): OpenIDConnectStrategy;
|
|
11
|
+
export {};
|