@ministryofjustice/hmpps-prisoner-auth 0.0.2 → 0.0.4
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 +32 -93
- package/dist/index.cjs.js +8 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.esm.js +8 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/main/prisonerAuth.d.ts +6 -0
- package/package.json +5 -4
- package/src/main/index.ts +0 -3
- package/src/main/launchpadUser.test.ts +0 -41
- package/src/main/launchpadUser.ts +0 -42
- package/src/main/prisonerAuth.test.ts +0 -117
- package/src/main/prisonerAuth.ts +0 -174
- package/src/main/testUtils.ts +0 -10
- package/src/main/timeSpans.test.ts +0 -129
- package/src/main/timeSpans.ts +0 -74
- package/src/main/tokens.ts +0 -65
- package/src/misc/install.diff +0 -154
package/README.md
CHANGED
|
@@ -7,29 +7,39 @@
|
|
|
7
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
8
|
|
|
9
9
|
1. Add the package `@ministryofjustice/hmpps-prisoner-auth` to your `package.json`
|
|
10
|
+
```sh
|
|
11
|
+
npm add --save @ministryofjustice/hmpps-prisoner-auth
|
|
12
|
+
npm i
|
|
10
13
|
```
|
|
11
|
-
|
|
12
|
-
$ npm i
|
|
13
|
-
```
|
|
14
|
+
|
|
14
15
|
2. Copy the contents of [src/misc/install.diff](src/misc/install.diff) to your local folder
|
|
16
|
+
```sh
|
|
17
|
+
curl https://raw.githubusercontent.com/ministryofjustice/hmpps-prisoner-facing-typescript-lib/refs/heads/main/packages/prisoner-auth/src/misc/install.diff > install.diff
|
|
15
18
|
```
|
|
16
|
-
|
|
17
|
-
```
|
|
19
|
+
|
|
18
20
|
3. Apply the diff to carry out all the required changes
|
|
21
|
+
```sh
|
|
22
|
+
git apply install.diff
|
|
19
23
|
```
|
|
20
|
-
|
|
21
|
-
```
|
|
24
|
+
|
|
22
25
|
4. Make sure to look through the applied changes and remove the diff file before committing.
|
|
23
26
|
|
|
27
|
+
5. Fill in the configuration options in `server/middleware/setUpPrisonerAuth.ts`:
|
|
28
|
+
|
|
29
|
+
See [PrisonerAuthOptions](https://raw.githubusercontent.com/ministryofjustice/hmpps-prisoner-facing-typescript-lib/refs/heads/main/packages/prisoner-auth/src/main/prisonerAuth.ts) for full documentation on the avaialble options
|
|
30
|
+
|
|
24
31
|
### Manual installation
|
|
25
32
|
|
|
33
|
+
NOTE: see [install.diff](https://raw.githubusercontent.com/ministryofjustice/hmpps-prisoner-facing-typescript-lib/refs/heads/main/packages/prisoner-auth/src/misc/install.diff) for an authorititive list of changes to be made.
|
|
34
|
+
|
|
26
35
|
1. Add the package `@ministryofjustice/hmpps-prisoner-auth` to your `package.json`
|
|
36
|
+
```sh
|
|
37
|
+
npm add --save @ministryofjustice/hmpps-prisoner-auth
|
|
38
|
+
npm i
|
|
27
39
|
```
|
|
28
|
-
|
|
29
|
-
$ npm i
|
|
30
|
-
```
|
|
40
|
+
|
|
31
41
|
2. Modify `interfaces/hmppsUser.ts` to make the following additions:
|
|
32
|
-
```
|
|
42
|
+
```ts
|
|
33
43
|
# add prisoner-auth to the AuthSource type
|
|
34
44
|
export type AuthSource = 'nomis' | 'delius' | 'external' | 'azuread' | 'prisoner-auth'
|
|
35
45
|
|
|
@@ -38,87 +48,16 @@ export type HmppsUser = PrisonUser | ProbationUser | ExternalUser | AzureADUser
|
|
|
38
48
|
```
|
|
39
49
|
These changes will help smooth over alot of assumptions made in the typescript template about the type of fields the user will have.
|
|
40
50
|
|
|
41
|
-
3.
|
|
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.
|
|
51
|
+
3. Add `server/middleware/setUpPrisonerAuth.ts` middlware:
|
|
53
52
|
|
|
54
|
-
|
|
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
|
-
}
|
|
53
|
+
```sh
|
|
54
|
+
curl https://raw.githubusercontent.com/ministryofjustice/hmpps-prisoner-facing-typescript-lib/refs/heads/main/packages/prisoner-auth/src/misc/setUpPrisonerAuth.ts.example > server/middleware/setUpPrisonerAuth.ts
|
|
108
55
|
```
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
56
|
+
Make sure to fill in the appropriate values.
|
|
57
|
+
|
|
58
|
+
See [PrisonerAuthOptions](https://raw.githubusercontent.com/ministryofjustice/hmpps-prisoner-facing-typescript-lib/refs/heads/main/packages/prisoner-auth/src/main/prisonerAuth.ts) for full documentation on the avaialble options
|
|
59
|
+
|
|
60
|
+
4. Modify `server/app.ts`:
|
|
61
|
+
- ADD `setUpPrisonerAuth()` to enable the middlware.
|
|
62
|
+
- REMOVE reference to `authorisationMiddleware()` - this checks the users `ROLE_` which a launchpad user will not have.
|
|
63
|
+
- REMOVE reference to `setUpCurrentUser()` - this sets up the `res.locals.user` fields which a launchpad will have already (`name`, `displayName` etc).
|
package/dist/index.cjs.js
CHANGED
|
@@ -99,11 +99,13 @@ class TimeSpan {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const nullOnLoginSuccessCallback = _ => Promise.resolve();
|
|
102
103
|
class PrisonerAuth {
|
|
103
104
|
launchpadAuthUrl;
|
|
104
105
|
authorizationUrl;
|
|
105
106
|
tokenUrl;
|
|
106
107
|
callbackUrl;
|
|
108
|
+
onLoginSuccessCallback;
|
|
107
109
|
clientId;
|
|
108
110
|
clientSecret;
|
|
109
111
|
scope;
|
|
@@ -115,6 +117,7 @@ class PrisonerAuth {
|
|
|
115
117
|
this.authorizationUrl = options.authorizationUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/authorize`;
|
|
116
118
|
this.tokenUrl = options.tokenUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/token`;
|
|
117
119
|
this.callbackUrl = options.callbackUrl ?? '/sign-in/callback';
|
|
120
|
+
this.onLoginSuccessCallback = options.onLoginSuccessCallback ?? nullOnLoginSuccessCallback;
|
|
118
121
|
this.clientId = options.clientId;
|
|
119
122
|
this.clientSecret = options.clientSecret;
|
|
120
123
|
this.scope = options.scope ?? ['user.basic.read', 'user.establishment.read', 'user.booking.read'];
|
|
@@ -139,7 +142,11 @@ class PrisonerAuth {
|
|
|
139
142
|
skipUserProfile: true,
|
|
140
143
|
nonce: this.nonce ? 'true' : undefined,
|
|
141
144
|
customHeaders: { authorization: this.authorizationToken() },
|
|
142
|
-
}, ((_issuer, _profile, _context, idToken, accessToken, refreshToken, done) =>
|
|
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
|
+
}));
|
|
143
150
|
}
|
|
144
151
|
/**
|
|
145
152
|
* Checks the expiry of a given LaunchpadUser idToken and refreshToken.
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +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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
|
|
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
CHANGED
|
@@ -85,6 +85,10 @@ type PrisonerAuthOptions = {
|
|
|
85
85
|
* If omitted, callbackUrl will default to /sign-in/callback
|
|
86
86
|
*/
|
|
87
87
|
callbackUrl?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Optional: A callback that will be invoked on sucessful login, provides the logged in user as a parameter
|
|
90
|
+
*/
|
|
91
|
+
onLoginSuccessCallback?: OnLoginSuccessCallback;
|
|
88
92
|
/** The Client ID of this application to send to Launchpad Auth */
|
|
89
93
|
clientId: string;
|
|
90
94
|
/** The Client Secret of this application to send to Launchpad Auth */
|
|
@@ -126,11 +130,13 @@ type PrisonerAuthOptions = {
|
|
|
126
130
|
*/
|
|
127
131
|
tokenFetcher?: TokenFetcher;
|
|
128
132
|
};
|
|
133
|
+
type OnLoginSuccessCallback = (user: LaunchpadUser) => Promise<void>;
|
|
129
134
|
declare class PrisonerAuth {
|
|
130
135
|
readonly launchpadAuthUrl: string;
|
|
131
136
|
readonly authorizationUrl: string;
|
|
132
137
|
readonly tokenUrl: string;
|
|
133
138
|
readonly callbackUrl: string;
|
|
139
|
+
readonly onLoginSuccessCallback: OnLoginSuccessCallback;
|
|
134
140
|
readonly clientId: string;
|
|
135
141
|
readonly clientSecret: string;
|
|
136
142
|
readonly scope: Scope | Scope[];
|
package/dist/index.esm.js
CHANGED
|
@@ -97,11 +97,13 @@ class TimeSpan {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
const nullOnLoginSuccessCallback = _ => Promise.resolve();
|
|
100
101
|
class PrisonerAuth {
|
|
101
102
|
launchpadAuthUrl;
|
|
102
103
|
authorizationUrl;
|
|
103
104
|
tokenUrl;
|
|
104
105
|
callbackUrl;
|
|
106
|
+
onLoginSuccessCallback;
|
|
105
107
|
clientId;
|
|
106
108
|
clientSecret;
|
|
107
109
|
scope;
|
|
@@ -113,6 +115,7 @@ class PrisonerAuth {
|
|
|
113
115
|
this.authorizationUrl = options.authorizationUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/authorize`;
|
|
114
116
|
this.tokenUrl = options.tokenUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/token`;
|
|
115
117
|
this.callbackUrl = options.callbackUrl ?? '/sign-in/callback';
|
|
118
|
+
this.onLoginSuccessCallback = options.onLoginSuccessCallback ?? nullOnLoginSuccessCallback;
|
|
116
119
|
this.clientId = options.clientId;
|
|
117
120
|
this.clientSecret = options.clientSecret;
|
|
118
121
|
this.scope = options.scope ?? ['user.basic.read', 'user.establishment.read', 'user.booking.read'];
|
|
@@ -137,7 +140,11 @@ class PrisonerAuth {
|
|
|
137
140
|
skipUserProfile: true,
|
|
138
141
|
nonce: this.nonce ? 'true' : undefined,
|
|
139
142
|
customHeaders: { authorization: this.authorizationToken() },
|
|
140
|
-
}, ((_issuer, _profile, _context, idToken, accessToken, refreshToken, done) =>
|
|
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
|
+
}));
|
|
141
148
|
}
|
|
142
149
|
/**
|
|
143
150
|
* Checks the expiry of a given LaunchpadUser idToken and refreshToken.
|
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +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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
|
|
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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"}
|
|
@@ -23,6 +23,10 @@ export type PrisonerAuthOptions = {
|
|
|
23
23
|
* If omitted, callbackUrl will default to /sign-in/callback
|
|
24
24
|
*/
|
|
25
25
|
callbackUrl?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional: A callback that will be invoked on sucessful login, provides the logged in user as a parameter
|
|
28
|
+
*/
|
|
29
|
+
onLoginSuccessCallback?: OnLoginSuccessCallback;
|
|
26
30
|
/** The Client ID of this application to send to Launchpad Auth */
|
|
27
31
|
clientId: string;
|
|
28
32
|
/** The Client Secret of this application to send to Launchpad Auth */
|
|
@@ -64,11 +68,13 @@ export type PrisonerAuthOptions = {
|
|
|
64
68
|
*/
|
|
65
69
|
tokenFetcher?: TokenFetcher;
|
|
66
70
|
};
|
|
71
|
+
export type OnLoginSuccessCallback = (user: LaunchpadUser) => Promise<void>;
|
|
67
72
|
export default class PrisonerAuth {
|
|
68
73
|
readonly launchpadAuthUrl: string;
|
|
69
74
|
readonly authorizationUrl: string;
|
|
70
75
|
readonly tokenUrl: string;
|
|
71
76
|
readonly callbackUrl: string;
|
|
77
|
+
readonly onLoginSuccessCallback: OnLoginSuccessCallback;
|
|
72
78
|
readonly clientId: string;
|
|
73
79
|
readonly clientSecret: string;
|
|
74
80
|
readonly scope: Scope | Scope[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ministryofjustice/hmpps-prisoner-auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Middleware for providing authentication against Launchpad Auth",
|
|
5
5
|
"author": "hmpps-launchpad-devs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"files": [
|
|
20
20
|
"*.md",
|
|
21
|
-
"dist/**/*"
|
|
22
|
-
"src/**/*"
|
|
21
|
+
"dist/**/*"
|
|
23
22
|
],
|
|
24
23
|
"engines": {
|
|
25
24
|
"node": "24"
|
|
@@ -42,5 +41,7 @@
|
|
|
42
41
|
"passport-openidconnect": "^0.1.2",
|
|
43
42
|
"superagent": "^10.3.0"
|
|
44
43
|
},
|
|
45
|
-
"peerDependencies": {
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"express": "^5.2.1"
|
|
46
|
+
}
|
|
46
47
|
}
|
package/src/main/index.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import jwt from 'jsonwebtoken'
|
|
2
|
-
import { userFromTokens } from './launchpadUser'
|
|
3
|
-
|
|
4
|
-
describe('userFromTokens', () => {
|
|
5
|
-
const aJwtOf = (obj: object): string => jwt.sign(obj, 'secret')
|
|
6
|
-
|
|
7
|
-
const idToken = aJwtOf({
|
|
8
|
-
name: 'Test User',
|
|
9
|
-
given_name: 'Test',
|
|
10
|
-
family_name: 'User',
|
|
11
|
-
establishment: { agency_id: 'CKI' },
|
|
12
|
-
sub: '12345',
|
|
13
|
-
})
|
|
14
|
-
const accessToken = aJwtOf({ hello: 'world' })
|
|
15
|
-
const refreshToken = aJwtOf({ hello: 'world' })
|
|
16
|
-
|
|
17
|
-
it('sets the given tokens on the user for later use (refreshing tokens)', () => {
|
|
18
|
-
const result = userFromTokens({ idToken, accessToken, refreshToken })
|
|
19
|
-
|
|
20
|
-
expect(result.idToken).toEqual(idToken)
|
|
21
|
-
expect(result.accessToken).toEqual(accessToken)
|
|
22
|
-
expect(result.refreshToken).toEqual(refreshToken)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('sets the establishment from the idToken', () => {
|
|
26
|
-
const result = userFromTokens({ idToken, accessToken, refreshToken })
|
|
27
|
-
expect(result.establishment).toEqual({ agency_id: 'CKI' })
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('sets the name fields from the idToken', () => {
|
|
31
|
-
const result = userFromTokens({ idToken, accessToken, refreshToken })
|
|
32
|
-
expect(result.name).toEqual('Test User')
|
|
33
|
-
expect(result.givenName).toEqual('Test')
|
|
34
|
-
expect(result.familyName).toEqual('User')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('sets authSource to give compatibility with the HmppsUser interface', () => {
|
|
38
|
-
const result = userFromTokens({ idToken, accessToken, refreshToken })
|
|
39
|
-
expect(result.authSource).toEqual('prisoner-auth')
|
|
40
|
-
})
|
|
41
|
-
})
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { IdToken, RawTokens, tokenFromJwt } from './tokens'
|
|
2
|
-
|
|
3
|
-
export type LaunchpadUser = {
|
|
4
|
-
authSource: 'prisoner-auth'
|
|
5
|
-
idToken: string
|
|
6
|
-
refreshToken: string
|
|
7
|
-
accessToken: string
|
|
8
|
-
establishment: IdToken['establishment']
|
|
9
|
-
name: string
|
|
10
|
-
givenName: string
|
|
11
|
-
familyName: string
|
|
12
|
-
|
|
13
|
-
// The following fields are for compatibility with HmppsUser only
|
|
14
|
-
token: string
|
|
15
|
-
username: string
|
|
16
|
-
userId: string
|
|
17
|
-
displayName: string
|
|
18
|
-
userRoles: string[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const userFromTokens = ({ idToken, accessToken, refreshToken }: RawTokens): LaunchpadUser => {
|
|
22
|
-
const { establishment, name, given_name: givenName, family_name: familyName, sub } = tokenFromJwt<IdToken>(idToken)
|
|
23
|
-
const authSource = 'prisoner-auth'
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
authSource,
|
|
27
|
-
idToken,
|
|
28
|
-
refreshToken,
|
|
29
|
-
accessToken,
|
|
30
|
-
establishment,
|
|
31
|
-
name,
|
|
32
|
-
givenName,
|
|
33
|
-
familyName,
|
|
34
|
-
|
|
35
|
-
// The following fields are for compatibility with HmppsUser only
|
|
36
|
-
token: idToken,
|
|
37
|
-
username: name,
|
|
38
|
-
userId: sub,
|
|
39
|
-
displayName: name,
|
|
40
|
-
userRoles: [],
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { minutes, timeAgo, timeFromNow } from './timeSpans'
|
|
2
|
-
import PrisonerAuth, { PrisonerAuthOptions } from './prisonerAuth'
|
|
3
|
-
import { LaunchpadUser } from './launchpadUser'
|
|
4
|
-
import { AccessToken, IdToken, RefreshToken, tokenFromJwt } from './tokens'
|
|
5
|
-
import { aJwtOf, aUserWith } from './testUtils'
|
|
6
|
-
|
|
7
|
-
describe('PrisonerAuth', () => {
|
|
8
|
-
const options: PrisonerAuthOptions = {
|
|
9
|
-
launchpadAuthUrl: 'https://launchpad.example.com',
|
|
10
|
-
clientId: 'cf837899-2a3c-4838-aa83-5fb76bd58df1',
|
|
11
|
-
clientSecret: '445f30b2-1010-4bcb-8cba-447b70463bd0',
|
|
12
|
-
tokenMinimumLifespan: minutes(5),
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('setting options', () => {
|
|
16
|
-
it('sets sane defaults for the URL options', () => {
|
|
17
|
-
const prisonerAuth = new PrisonerAuth(options)
|
|
18
|
-
expect(prisonerAuth.authorizationUrl).toEqual('https://launchpad.example.com/v1/oauth2/authorize')
|
|
19
|
-
expect(prisonerAuth.tokenUrl).toEqual('https://launchpad.example.com/v1/oauth2/token')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('allows custom override of the URL options', () => {
|
|
23
|
-
const prisonerAuth = new PrisonerAuth({
|
|
24
|
-
...options,
|
|
25
|
-
authorizationUrl: 'https://auth.com',
|
|
26
|
-
tokenUrl: 'https://token.com',
|
|
27
|
-
})
|
|
28
|
-
expect(prisonerAuth.authorizationUrl).toEqual('https://auth.com')
|
|
29
|
-
expect(prisonerAuth.tokenUrl).toEqual('https://token.com')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('sets the client credentials', () => {
|
|
33
|
-
const prisonerAuth = new PrisonerAuth(options)
|
|
34
|
-
expect(prisonerAuth.clientId).toEqual('cf837899-2a3c-4838-aa83-5fb76bd58df1')
|
|
35
|
-
expect(prisonerAuth.clientSecret).toEqual('445f30b2-1010-4bcb-8cba-447b70463bd0')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('allows overriding of the callbackUrl', () => {
|
|
39
|
-
const prisonerAuth = new PrisonerAuth({ ...options, callbackUrl: '/my/call/back' })
|
|
40
|
-
expect(prisonerAuth.callbackUrl).toEqual('/my/call/back')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('sets a default callbackUrl when none provided', () => {
|
|
44
|
-
const prisonerAuth = new PrisonerAuth(options)
|
|
45
|
-
expect(prisonerAuth.callbackUrl).toEqual('/sign-in/callback')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('allows overriding of the scopes', () => {
|
|
49
|
-
const prisonerAuth = new PrisonerAuth({ ...options, scope: 'user.basic.read' })
|
|
50
|
-
expect(prisonerAuth.scope).toEqual('user.basic.read')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('sets a default scope of all read only scopes when no scopes given', () => {
|
|
54
|
-
const prisonerAuth = new PrisonerAuth(options)
|
|
55
|
-
expect(prisonerAuth.scope).toEqual(['user.basic.read', 'user.establishment.read', 'user.booking.read'])
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
describe('validateAndRefreshUser', () => {
|
|
60
|
-
describe('when the users idToken has not yet expired', () => {
|
|
61
|
-
it('returns the user as it is', async () => {
|
|
62
|
-
const prisonerAuth = new PrisonerAuth(options)
|
|
63
|
-
const user = aUserWith({
|
|
64
|
-
idTokenExp: timeFromNow(minutes(6)).seconds,
|
|
65
|
-
refreshTokenExp: timeFromNow(minutes(6)).seconds,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const result = await prisonerAuth.validateAndRefreshUser(user as LaunchpadUser)
|
|
69
|
-
const idToken = tokenFromJwt<IdToken>(result.idToken)
|
|
70
|
-
|
|
71
|
-
expect(idToken.iat).toEqual(123_456_789)
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
describe('when the users idToken has expired', () => {
|
|
76
|
-
describe('when the users refreshToken has not yet expired', () => {
|
|
77
|
-
it('returns a new set of user tokens for the given user', async () => {
|
|
78
|
-
const prisonerAuth = new PrisonerAuth({
|
|
79
|
-
...options,
|
|
80
|
-
tokenFetcher: jest.fn().mockResolvedValue({
|
|
81
|
-
idToken: aJwtOf({ iat: 987_654_321 }),
|
|
82
|
-
refreshToken: aJwtOf({ iat: 987_654_322 }),
|
|
83
|
-
accessToken: aJwtOf({ iat: 987_654_323 }),
|
|
84
|
-
}),
|
|
85
|
-
})
|
|
86
|
-
const user = aUserWith({
|
|
87
|
-
idTokenExp: timeAgo(minutes(3)).seconds,
|
|
88
|
-
refreshTokenExp: timeFromNow(minutes(6)).seconds,
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const result = await prisonerAuth.validateAndRefreshUser(user as LaunchpadUser)
|
|
92
|
-
const idToken = tokenFromJwt<IdToken>(result.idToken)
|
|
93
|
-
const refreshToken = tokenFromJwt<RefreshToken>(result.refreshToken)
|
|
94
|
-
const accessToken = tokenFromJwt<AccessToken>(result.accessToken)
|
|
95
|
-
|
|
96
|
-
expect(idToken.iat).toEqual(987_654_321)
|
|
97
|
-
expect(refreshToken.iat).toEqual(987_654_322)
|
|
98
|
-
expect(accessToken.iat).toEqual(987_654_323)
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
describe('when the users refreshToken has expired', () => {
|
|
103
|
-
it('throws an error as we cant continue', async () => {
|
|
104
|
-
const prisonerAuth = new PrisonerAuth(options)
|
|
105
|
-
const user = aUserWith({
|
|
106
|
-
idTokenExp: timeAgo(minutes(3)).seconds,
|
|
107
|
-
refreshTokenExp: timeFromNow(minutes(3)).seconds,
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
await expect(async () => {
|
|
111
|
-
await prisonerAuth.validateAndRefreshUser(user as LaunchpadUser)
|
|
112
|
-
}).rejects.toThrow('Refresh token expired')
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
})
|
package/src/main/prisonerAuth.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import OpenIDConnectStrategy, { Profile, VerifyCallback, VerifyFunction } from 'passport-openidconnect'
|
|
2
|
-
import { IdToken, oauthClientToken, RefreshToken, Scope, tokenFromJwt, tokenFetcher, TokenFetcher } from './tokens'
|
|
3
|
-
import { LaunchpadUser, userFromTokens } from './launchpadUser'
|
|
4
|
-
import { seconds, timeFromNow, TimeSpan } from './timeSpans'
|
|
5
|
-
|
|
6
|
-
export type PrisonerAuthOptions = {
|
|
7
|
-
/** The full URL of the Launchpad Auth instance */
|
|
8
|
-
launchpadAuthUrl: string
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Optional: The full URL of the authorisation path on the Launchpad Auth instance.
|
|
12
|
-
*
|
|
13
|
-
* If omitted, authorizationUrl will default to $launchpadAuthUrl/v1/oauth2/authorize
|
|
14
|
-
*/
|
|
15
|
-
authorizationUrl?: string
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Optional: The full URL of the token path on the Launchpad Auth instance.
|
|
19
|
-
*
|
|
20
|
-
* If omitted, tokenUrl will default to $launchpadAuthUrl/v1/oauth2/token
|
|
21
|
-
*/
|
|
22
|
-
tokenUrl?: string
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Optional: The URL or path that Launchpad Auth will be told to call back to with the tokens.
|
|
26
|
-
*
|
|
27
|
-
* If omitted, callbackUrl will default to /sign-in/callback
|
|
28
|
-
*/
|
|
29
|
-
callbackUrl?: string
|
|
30
|
-
|
|
31
|
-
/** The Client ID of this application to send to Launchpad Auth */
|
|
32
|
-
clientId: string
|
|
33
|
-
/** The Client Secret of this application to send to Launchpad Auth */
|
|
34
|
-
clientSecret: string
|
|
35
|
-
/**
|
|
36
|
-
* Optional: The scope or scopes to request on behalf of the user.
|
|
37
|
-
*
|
|
38
|
-
* If omitted, scope will default to user.basic.read, user.establishment.read and user.booking.read
|
|
39
|
-
*/
|
|
40
|
-
scope?: Scope | Scope[]
|
|
41
|
-
/**
|
|
42
|
-
* Optional: Whether or not to use nonce, not recommended to be disabled except for integration tests
|
|
43
|
-
*
|
|
44
|
-
* If ommitted. defaults to true
|
|
45
|
-
*/
|
|
46
|
-
nonce?: boolean
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A time span specifying the least amount of time a token must have left before it is considered expired.
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
*
|
|
53
|
-
* Token will be considered expired when its exp value is in the past.
|
|
54
|
-
* ```ts
|
|
55
|
-
* nothing()
|
|
56
|
-
* ```
|
|
57
|
-
*
|
|
58
|
-
* Token will be considered expired unless its exp value is **AT LEAST 5** minutes in the future from time of checking.
|
|
59
|
-
* ```ts
|
|
60
|
-
* minutes(5)
|
|
61
|
-
* ```
|
|
62
|
-
*
|
|
63
|
-
* @see TimeSpan
|
|
64
|
-
*/
|
|
65
|
-
tokenMinimumLifespan: TimeSpan
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Optional: Token fetcher implementation. Intended for unit testing PrisonerAuth
|
|
69
|
-
*
|
|
70
|
-
* @see TokenFetcher
|
|
71
|
-
*/
|
|
72
|
-
tokenFetcher?: TokenFetcher
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export default class PrisonerAuth {
|
|
76
|
-
readonly launchpadAuthUrl: string
|
|
77
|
-
|
|
78
|
-
readonly authorizationUrl: string
|
|
79
|
-
|
|
80
|
-
readonly tokenUrl: string
|
|
81
|
-
|
|
82
|
-
readonly callbackUrl: string
|
|
83
|
-
|
|
84
|
-
readonly clientId: string
|
|
85
|
-
|
|
86
|
-
readonly clientSecret: string
|
|
87
|
-
|
|
88
|
-
readonly scope: Scope | Scope[]
|
|
89
|
-
|
|
90
|
-
readonly nonce: boolean
|
|
91
|
-
|
|
92
|
-
readonly tokenMinimumLifespan: TimeSpan
|
|
93
|
-
|
|
94
|
-
readonly tokenFetcher: TokenFetcher
|
|
95
|
-
|
|
96
|
-
constructor(options: PrisonerAuthOptions) {
|
|
97
|
-
this.launchpadAuthUrl = options.launchpadAuthUrl
|
|
98
|
-
this.authorizationUrl = options.authorizationUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/authorize`
|
|
99
|
-
this.tokenUrl = options.tokenUrl ?? `${options.launchpadAuthUrl}/v1/oauth2/token`
|
|
100
|
-
this.callbackUrl = options.callbackUrl ?? '/sign-in/callback'
|
|
101
|
-
this.clientId = options.clientId
|
|
102
|
-
this.clientSecret = options.clientSecret
|
|
103
|
-
this.scope = options.scope ?? ['user.basic.read', 'user.establishment.read', 'user.booking.read']
|
|
104
|
-
this.nonce = options.nonce ?? true
|
|
105
|
-
this.tokenMinimumLifespan = options.tokenMinimumLifespan
|
|
106
|
-
this.tokenFetcher = options.tokenFetcher ?? tokenFetcher
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
*
|
|
111
|
-
* @returns OpenIDConnectStrategy ready for use with passport
|
|
112
|
-
*/
|
|
113
|
-
passportStrategy(): OpenIDConnectStrategy {
|
|
114
|
-
return new OpenIDConnectStrategy(
|
|
115
|
-
{
|
|
116
|
-
issuer: this.launchpadAuthUrl,
|
|
117
|
-
authorizationURL: this.authorizationUrl,
|
|
118
|
-
tokenURL: this.tokenUrl,
|
|
119
|
-
callbackURL: this.callbackUrl,
|
|
120
|
-
clientID: this.clientId,
|
|
121
|
-
clientSecret: this.clientSecret,
|
|
122
|
-
scope: this.scope,
|
|
123
|
-
userInfoURL: '',
|
|
124
|
-
skipUserProfile: true,
|
|
125
|
-
nonce: this.nonce ? 'true' : undefined,
|
|
126
|
-
customHeaders: { authorization: this.authorizationToken() },
|
|
127
|
-
},
|
|
128
|
-
((
|
|
129
|
-
_issuer: string,
|
|
130
|
-
_profile: Profile,
|
|
131
|
-
_context: object,
|
|
132
|
-
idToken: string,
|
|
133
|
-
accessToken: string,
|
|
134
|
-
refreshToken: string,
|
|
135
|
-
done: VerifyCallback,
|
|
136
|
-
) => done(null, userFromTokens({ idToken, accessToken, refreshToken }))) as VerifyFunction,
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Checks the expiry of a given LaunchpadUser idToken and refreshToken.
|
|
142
|
-
* - If the idToken has expired it will attempt to refresh it using the refreshToken.
|
|
143
|
-
* - If the refreshToken has expired it will throw an Error.
|
|
144
|
-
*
|
|
145
|
-
* 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.
|
|
146
|
-
*
|
|
147
|
-
* @param user - The LaunchpadUser instance to check token expiry on.
|
|
148
|
-
* @returns A LaunchpadUser is always returned, either the given user or a user with refreshed tokens.
|
|
149
|
-
* @throws Error is thrown if the refreshToken has expired, the user must sign in again from scratch.
|
|
150
|
-
*/
|
|
151
|
-
async validateAndRefreshUser(user: LaunchpadUser): Promise<LaunchpadUser> {
|
|
152
|
-
const idToken = tokenFromJwt<IdToken>(user.idToken)
|
|
153
|
-
const refreshToken = tokenFromJwt<RefreshToken>(user.refreshToken)
|
|
154
|
-
const tokenExpiryTime = timeFromNow(this.tokenMinimumLifespan)
|
|
155
|
-
|
|
156
|
-
// Id Token still valid, return the current user
|
|
157
|
-
if (seconds(idToken.exp).isGreaterThanOrEqualTo(tokenExpiryTime)) {
|
|
158
|
-
return user
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Refresh token expired, cannot continue
|
|
162
|
-
if (seconds(refreshToken.exp).isLessThan(tokenExpiryTime)) {
|
|
163
|
-
throw new Error('Refresh token expired')
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Refresh the user tokens
|
|
167
|
-
const newTokens = await this.tokenFetcher(user.refreshToken, this.tokenUrl, this.authorizationToken())
|
|
168
|
-
return userFromTokens(newTokens)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private authorizationToken(): string {
|
|
172
|
-
return oauthClientToken(this.clientId, this.clientSecret)
|
|
173
|
-
}
|
|
174
|
-
}
|
package/src/main/testUtils.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
-
import jwt from 'jsonwebtoken'
|
|
3
|
-
|
|
4
|
-
export const aJwtOf = (obj: object): string => jwt.sign(obj, 'secret')
|
|
5
|
-
|
|
6
|
-
export const aUserWith = ({ idTokenExp, refreshTokenExp }: { idTokenExp: number; refreshTokenExp: number }) => ({
|
|
7
|
-
idToken: aJwtOf({ exp: idTokenExp, iat: 123_456_789, sub: 'USER_A' }),
|
|
8
|
-
refreshToken: aJwtOf({ exp: refreshTokenExp, iat: 123_456_789, sub: 'USER_A' }),
|
|
9
|
-
accessToken: aJwtOf({ exp: refreshTokenExp, iat: 123_456_789, sub: 'USER_A' }),
|
|
10
|
-
})
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { milliseconds, minutes, seconds, timeAgo, timeFromNow, TimeSpan } from './timeSpans'
|
|
2
|
-
|
|
3
|
-
describe('time spans', () => {
|
|
4
|
-
describe('minutes', () => {
|
|
5
|
-
const cases = [
|
|
6
|
-
[1, 1, 60, 60000],
|
|
7
|
-
[5, 5, 300, 300000],
|
|
8
|
-
[0.5, 0.5, 30, 30000],
|
|
9
|
-
[0, 0, 0, 0],
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
test.each(cases)(
|
|
13
|
-
'Given %p minute(s), minutes is %p, seconds is %p and milliseconds is %p',
|
|
14
|
-
(example, expectedMinutes, expectedSeconds, expectedMilliseconds) => {
|
|
15
|
-
const subject = minutes(example)
|
|
16
|
-
expect(subject.minutes).toEqual(expectedMinutes)
|
|
17
|
-
expect(subject.seconds).toEqual(expectedSeconds)
|
|
18
|
-
expect(subject.milliseconds).toEqual(expectedMilliseconds)
|
|
19
|
-
},
|
|
20
|
-
)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
describe('seconds', () => {
|
|
24
|
-
const cases = [
|
|
25
|
-
[1, 0.01666667, 1, 1000],
|
|
26
|
-
[60, 1, 60, 60000],
|
|
27
|
-
[120, 2, 120, 120000],
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
test.each(cases)(
|
|
31
|
-
'Given %p seconds(s), minutes is %p, seconds is %p and milliseconds is %p',
|
|
32
|
-
(example, expectedMinutes, expectedSeconds, expectedMilliseconds) => {
|
|
33
|
-
const subject = seconds(example)
|
|
34
|
-
expect(subject.minutes).toBeCloseTo(expectedMinutes)
|
|
35
|
-
expect(subject.seconds).toEqual(expectedSeconds)
|
|
36
|
-
expect(subject.milliseconds).toEqual(expectedMilliseconds)
|
|
37
|
-
},
|
|
38
|
-
)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('milliseconds', () => {
|
|
42
|
-
const cases = [
|
|
43
|
-
[1, 0.00001667, 0.001, 1],
|
|
44
|
-
[1000, 0.01666667, 1, 1000],
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
test.each(cases)(
|
|
48
|
-
'Given %p milliseconds(s), minutes is %p, seconds is %p and milliseconds is %p',
|
|
49
|
-
(example, expectedMinutes, expectedSeconds, expectedMilliseconds) => {
|
|
50
|
-
const subject = milliseconds(example)
|
|
51
|
-
expect(subject.minutes).toBeCloseTo(expectedMinutes)
|
|
52
|
-
expect(subject.seconds).toEqual(expectedSeconds)
|
|
53
|
-
expect(subject.milliseconds).toEqual(expectedMilliseconds)
|
|
54
|
-
},
|
|
55
|
-
)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
describe('isEqualTo', () => {
|
|
59
|
-
const cases: [string, TimeSpan, TimeSpan, boolean][] = [
|
|
60
|
-
['60 seconds is equal to 1 minute', seconds(60), minutes(1), true],
|
|
61
|
-
['120 seconds is equal to 1 and a half minutes', seconds(90), minutes(1.5), true],
|
|
62
|
-
['2 and a half seconds is equal to 2500 milliseconds', seconds(2.5), milliseconds(2500), true],
|
|
63
|
-
['120 minutes is equal to 7200 seconds', minutes(120), seconds(7200), true],
|
|
64
|
-
['120 minutes is not equal to 7000 seconds', minutes(120), seconds(7000), false],
|
|
65
|
-
['1 seconds is not equal to 1 minutes', seconds(1), minutes(1), false],
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
test.each(cases)('%s', (testCaseDescription, example1, example2, expectedResult) => {
|
|
69
|
-
expect(example1.isEqualTo(example2)).toBe(expectedResult)
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('isGreaterThan', () => {
|
|
74
|
-
const cases: [string, TimeSpan, TimeSpan, boolean][] = [
|
|
75
|
-
['61 seconds is greater than 1 minute', seconds(61), minutes(1), true],
|
|
76
|
-
['125 seconds is greater than 1 and a half minutes', seconds(121), minutes(1.5), true],
|
|
77
|
-
['60 seconds is not greater than 1 minute', seconds(60), minutes(1), false],
|
|
78
|
-
['1 minute is not greater than 61 seconds', minutes(1), seconds(61), false],
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
test.each(cases)('%s', (testCaseDescription, example1, example2, expectedResult) => {
|
|
82
|
-
expect(example1.isGreaterThan(example2)).toBe(expectedResult)
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
describe('timeAgo', () => {
|
|
87
|
-
const dateNowInMs: number = 1770000000000
|
|
88
|
-
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
Date.now = jest.fn(() => dateNowInMs)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
const cases: [string, number, TimeSpan][] = [
|
|
94
|
-
['1 millisecond', dateNowInMs - 1, milliseconds(1)],
|
|
95
|
-
['1 second', dateNowInMs - 1000, seconds(1)],
|
|
96
|
-
['30 seconds', dateNowInMs - 30000, seconds(30)],
|
|
97
|
-
['2 minutes', dateNowInMs - 120000, minutes(2)],
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
test.each(cases)(
|
|
101
|
-
'Given a current time in milliseconds 1770000000000 and a timespan of %p, the expected past timespan in milliseconds is %p',
|
|
102
|
-
(description, expectedMilliseconds, example) => {
|
|
103
|
-
expect(timeAgo(example).milliseconds).toEqual(expectedMilliseconds)
|
|
104
|
-
},
|
|
105
|
-
)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
describe('timeFromNow', () => {
|
|
109
|
-
const dateNowInMs: number = 1770000000000
|
|
110
|
-
|
|
111
|
-
beforeEach(() => {
|
|
112
|
-
Date.now = jest.fn(() => dateNowInMs)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
const cases: [string, number, TimeSpan][] = [
|
|
116
|
-
['1 millisecond', dateNowInMs + 1, milliseconds(1)],
|
|
117
|
-
['1 second', dateNowInMs + 1000, seconds(1)],
|
|
118
|
-
['30 seconds', dateNowInMs + 30000, seconds(30)],
|
|
119
|
-
['2 minutes', dateNowInMs + 120000, minutes(2)],
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
test.each(cases)(
|
|
123
|
-
'Given a current time in milliseconds 1770000000000 and a timespan of %p, the expected future timespan in milliseconds is %p',
|
|
124
|
-
(description, expectedMilliseconds, example) => {
|
|
125
|
-
expect(timeFromNow(example).milliseconds).toEqual(expectedMilliseconds)
|
|
126
|
-
},
|
|
127
|
-
)
|
|
128
|
-
})
|
|
129
|
-
})
|
package/src/main/timeSpans.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// A TimeSpan allows you to give meaning to a number and then read that number back and compare them in different formats
|
|
2
|
-
// ie.
|
|
3
|
-
// seconds(60).minutes === 1
|
|
4
|
-
// seconds(30).seconds === 30
|
|
5
|
-
// minutes(1).seconds === 60
|
|
6
|
-
// milliseconds(1000).seconds === 1
|
|
7
|
-
// milliseconds(2000).isEqualTo(seconds(2)) #=> true
|
|
8
|
-
// minutes(5).
|
|
9
|
-
// This allows a caller to just accept a TimeSpan and then work in the units it cares about
|
|
10
|
-
|
|
11
|
-
export const minutes = (numMinutes: number): TimeSpan => TimeSpan.minutes(numMinutes)
|
|
12
|
-
|
|
13
|
-
export const seconds = (numSeconds: number): TimeSpan => TimeSpan.seconds(numSeconds)
|
|
14
|
-
|
|
15
|
-
export const milliseconds = (numMilliseconds: number): TimeSpan => TimeSpan.milliseconds(numMilliseconds)
|
|
16
|
-
|
|
17
|
-
export const nothing = (): TimeSpan => TimeSpan.nothing()
|
|
18
|
-
|
|
19
|
-
// Get a time span in the past
|
|
20
|
-
export const timeAgo = (ago: TimeSpan) => milliseconds(Date.now() - ago.milliseconds)
|
|
21
|
-
|
|
22
|
-
// Get a time span in the future
|
|
23
|
-
export const timeFromNow = (fromNow: TimeSpan) => milliseconds(Date.now() + fromNow.milliseconds)
|
|
24
|
-
|
|
25
|
-
// Implementation
|
|
26
|
-
export class TimeSpan {
|
|
27
|
-
readonly minutes: number
|
|
28
|
-
|
|
29
|
-
readonly seconds: number
|
|
30
|
-
|
|
31
|
-
readonly milliseconds: number
|
|
32
|
-
|
|
33
|
-
private constructor(numMinutes: number, numSeconds: number, numMilliseconds: number) {
|
|
34
|
-
this.minutes = numMinutes
|
|
35
|
-
this.seconds = numSeconds
|
|
36
|
-
this.milliseconds = numMilliseconds
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static minutes(numMinutes: number): TimeSpan {
|
|
40
|
-
return new TimeSpan(numMinutes, numMinutes * 60, TimeSpan.seconds(numMinutes * 60).milliseconds)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
static seconds(numSeconds: number): TimeSpan {
|
|
44
|
-
return new TimeSpan(numSeconds / 60, numSeconds, numSeconds * 1000)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
static milliseconds(numMilliseconds: number): TimeSpan {
|
|
48
|
-
return new TimeSpan(TimeSpan.seconds(numMilliseconds / 1000).minutes, numMilliseconds / 1000, numMilliseconds)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
static nothing(): TimeSpan {
|
|
52
|
-
return new TimeSpan(0, 0, 0)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
isGreaterThan(t: TimeSpan): boolean {
|
|
56
|
-
return this.milliseconds > t.milliseconds
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
isGreaterThanOrEqualTo(t: TimeSpan): boolean {
|
|
60
|
-
return this.milliseconds >= t.milliseconds
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
isLessThan(t: TimeSpan): boolean {
|
|
64
|
-
return this.milliseconds < t.milliseconds
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
isLessThanOrEqualTo(t: TimeSpan): boolean {
|
|
68
|
-
return this.milliseconds <= t.milliseconds
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
isEqualTo(t: TimeSpan): boolean {
|
|
72
|
-
return this.milliseconds === t.milliseconds
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/main/tokens.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import superagent from 'superagent'
|
|
2
|
-
|
|
3
|
-
export 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 // exp is in seconds
|
|
12
|
-
booking: { id: string }
|
|
13
|
-
establishment: {
|
|
14
|
-
agency_id: string
|
|
15
|
-
name: string
|
|
16
|
-
display_name: string
|
|
17
|
-
youth: boolean
|
|
18
|
-
}
|
|
19
|
-
iss: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type RefreshToken = {
|
|
23
|
-
jti: string
|
|
24
|
-
ati: string
|
|
25
|
-
iat: number
|
|
26
|
-
aud: string
|
|
27
|
-
sub: string
|
|
28
|
-
exp: number // exp is in seconds
|
|
29
|
-
scopes: Scope[]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type AccessToken = RefreshToken
|
|
33
|
-
|
|
34
|
-
export type RawTokens = { idToken: string; refreshToken: string; accessToken: string }
|
|
35
|
-
|
|
36
|
-
export type Scope =
|
|
37
|
-
| 'user.basic.read'
|
|
38
|
-
| 'user.establishment.read'
|
|
39
|
-
| 'user.booking.read'
|
|
40
|
-
| 'user.clients.read'
|
|
41
|
-
| 'user.clients.delete'
|
|
42
|
-
|
|
43
|
-
export const tokenFromJwt = <T>(token: string): T => JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
|
|
44
|
-
|
|
45
|
-
export const oauthClientToken = (clientId: string, clientSecret: string): string => {
|
|
46
|
-
const token = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
|
|
47
|
-
return `Basic ${token}`
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type TokenFetcher = (refreshToken: string, tokenUrl: string, authorizationHeader: string) => Promise<RawTokens>
|
|
51
|
-
|
|
52
|
-
export const tokenFetcher: TokenFetcher = (userRefreshToken, tokenUrl, authorizationHeader) => {
|
|
53
|
-
return new Promise<RawTokens>((resolve, reject) => {
|
|
54
|
-
superagent
|
|
55
|
-
.post(tokenUrl)
|
|
56
|
-
.set('Content-Type', 'application/x-www-form-urlencoded')
|
|
57
|
-
.set('Authorization', authorizationHeader)
|
|
58
|
-
.query({ refresh_token: userRefreshToken, grant_type: 'refresh_token' })
|
|
59
|
-
.end((err, res) => {
|
|
60
|
-
if (err) reject(err)
|
|
61
|
-
const { id_token: idToken, access_token: accessToken, refresh_token: refreshToken } = res.body
|
|
62
|
-
return resolve({ idToken, accessToken, refreshToken })
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
}
|
package/src/misc/install.diff
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
diff --git a/server/app.ts b/server/app.ts
|
|
2
|
-
index eddaa67..26fb35e 100644
|
|
3
|
-
--- a/server/app.ts
|
|
4
|
-
+++ b/server/app.ts
|
|
5
|
-
@@ -5,11 +5,9 @@ import createError from 'http-errors'
|
|
6
|
-
import nunjucksSetup from './utils/nunjucksSetup'
|
|
7
|
-
import errorHandler from './errorHandler'
|
|
8
|
-
import { appInsightsMiddleware } from './utils/azureAppInsights'
|
|
9
|
-
-import authorisationMiddleware from './middleware/authorisationMiddleware'
|
|
10
|
-
|
|
11
|
-
import setUpAuthentication from './middleware/setUpAuthentication'
|
|
12
|
-
import setUpCsrf from './middleware/setUpCsrf'
|
|
13
|
-
-import setUpCurrentUser from './middleware/setUpCurrentUser'
|
|
14
|
-
import setUpHealthChecks from './middleware/setUpHealthChecks'
|
|
15
|
-
import setUpStaticResources from './middleware/setUpStaticResources'
|
|
16
|
-
import setUpWebRequestParsing from './middleware/setupRequestParsing'
|
|
17
|
-
@@ -34,9 +32,7 @@ export default function createApp(services: Services): express.Application {
|
|
18
|
-
app.use(setUpStaticResources())
|
|
19
|
-
nunjucksSetup(app)
|
|
20
|
-
app.use(setUpAuthentication())
|
|
21
|
-
- app.use(authorisationMiddleware())
|
|
22
|
-
app.use(setUpCsrf())
|
|
23
|
-
- app.use(setUpCurrentUser())
|
|
24
|
-
|
|
25
|
-
app.use(routes(services))
|
|
26
|
-
|
|
27
|
-
diff --git a/server/interfaces/hmppsUser.ts b/server/interfaces/hmppsUser.ts
|
|
28
|
-
index f0a6dd2..8c01550 100644
|
|
29
|
-
--- a/server/interfaces/hmppsUser.ts
|
|
30
|
-
+++ b/server/interfaces/hmppsUser.ts
|
|
31
|
-
@@ -1,4 +1,6 @@
|
|
32
|
-
-export type AuthSource = 'nomis' | 'delius' | 'external' | 'azuread'
|
|
33
|
-
+import { LaunchpadUser } from '@ministryofjustice/hmpps-prisoner-auth'
|
|
34
|
-
+
|
|
35
|
-
+export type AuthSource = 'nomis' | 'delius' | 'external' | 'azuread' | 'prisoner-auth'
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* These are the details that all user types share.
|
|
39
|
-
@@ -57,4 +59,4 @@ export interface AzureADUser extends BaseUser {
|
|
40
|
-
authSource: 'azuread'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
-export type HmppsUser = PrisonUser | ProbationUser | ExternalUser | AzureADUser
|
|
44
|
-
+export type HmppsUser = PrisonUser | ProbationUser | ExternalUser | AzureADUser | LaunchpadUser
|
|
45
|
-
diff --git a/server/middleware/setUpAuthentication.ts b/server/middleware/setUpAuthentication.ts
|
|
46
|
-
index 1979d6e..a954198 100644
|
|
47
|
-
--- a/server/middleware/setUpAuthentication.ts
|
|
48
|
-
+++ b/server/middleware/setUpAuthentication.ts
|
|
49
|
-
@@ -1,12 +1,8 @@
|
|
50
|
-
import passport from 'passport'
|
|
51
|
-
import flash from 'connect-flash'
|
|
52
|
-
import { Router } from 'express'
|
|
53
|
-
-import { Strategy } from 'passport-oauth2'
|
|
54
|
-
-import { VerificationClient, AuthenticatedRequest } from '@ministryofjustice/hmpps-auth-clients'
|
|
55
|
-
-import config from '../config'
|
|
56
|
-
+import { LaunchpadUser, PrisonerAuth, minutes } from '@ministryofjustice/hmpps-prisoner-auth'
|
|
57
|
-
import { HmppsUser } from '../interfaces/hmppsUser'
|
|
58
|
-
-import generateOauthClientToken from '../utils/clientCredentials'
|
|
59
|
-
-import logger from '../../logger'
|
|
60
|
-
|
|
61
|
-
passport.serializeUser((user, done) => {
|
|
62
|
-
// Not used but required for Passport
|
|
63
|
-
@@ -18,26 +14,17 @@ passport.deserializeUser((user, done) => {
|
|
64
|
-
done(null, user as Express.User)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
-passport.use(
|
|
68
|
-
- new Strategy(
|
|
69
|
-
- {
|
|
70
|
-
- authorizationURL: `${config.apis.hmppsAuth.externalUrl}/oauth/authorize`,
|
|
71
|
-
- tokenURL: `${config.apis.hmppsAuth.url}/oauth/token`,
|
|
72
|
-
- clientID: config.apis.hmppsAuth.authClientId,
|
|
73
|
-
- clientSecret: config.apis.hmppsAuth.authClientSecret,
|
|
74
|
-
- callbackURL: `${config.ingressUrl}/sign-in/callback`,
|
|
75
|
-
- state: true,
|
|
76
|
-
- customHeaders: { Authorization: generateOauthClientToken() },
|
|
77
|
-
- },
|
|
78
|
-
- (token, refreshToken, params, profile, done) => {
|
|
79
|
-
- return done(null, { token, username: params.user_name, authSource: params.auth_source })
|
|
80
|
-
- },
|
|
81
|
-
- ),
|
|
82
|
-
-)
|
|
83
|
-
+const prisonerAuth = new PrisonerAuth({
|
|
84
|
-
+ launchpadAuthUrl: 'VALUE_HERE',
|
|
85
|
-
+ clientId: 'VALUE_HERE',
|
|
86
|
-
+ clientSecret: 'VALUE_HERE',
|
|
87
|
-
+ tokenMinimumLifespan: minutes(5),
|
|
88
|
-
+})
|
|
89
|
-
+
|
|
90
|
-
+passport.use('prisoner-auth', prisonerAuth.passportStrategy())
|
|
91
|
-
|
|
92
|
-
export default function setupAuthentication() {
|
|
93
|
-
const router = Router()
|
|
94
|
-
- const tokenVerificationClient = new VerificationClient(config.apis.tokenVerification, logger)
|
|
95
|
-
|
|
96
|
-
router.use(passport.initialize())
|
|
97
|
-
router.use(passport.session())
|
|
98
|
-
@@ -48,40 +35,39 @@ export default function setupAuthentication() {
|
|
99
|
-
return res.render('autherror')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
- router.get('/sign-in', passport.authenticate('oauth2'))
|
|
103
|
-
+ router.get('/sign-in', passport.authenticate('prisoner-auth'))
|
|
104
|
-
|
|
105
|
-
router.get('/sign-in/callback', (req, res, next) =>
|
|
106
|
-
- passport.authenticate('oauth2', {
|
|
107
|
-
+ passport.authenticate('prisoner-auth', {
|
|
108
|
-
successReturnToOrRedirect: req.session.returnTo || '/',
|
|
109
|
-
failureRedirect: '/autherror',
|
|
110
|
-
})(req, res, next),
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
- const authUrl = config.apis.hmppsAuth.externalUrl
|
|
114
|
-
- const authParameters = `client_id=${config.apis.hmppsAuth.authClientId}&redirect_uri=${config.ingressUrl}`
|
|
115
|
-
-
|
|
116
|
-
router.use('/sign-out', (req, res, next) => {
|
|
117
|
-
- const authSignOutUrl = `${authUrl}/sign-out?${authParameters}`
|
|
118
|
-
if (req.user) {
|
|
119
|
-
req.logout(err => {
|
|
120
|
-
if (err) return next(err)
|
|
121
|
-
- return req.session.destroy(() => res.redirect(authSignOutUrl))
|
|
122
|
-
+ return req.session.destroy(() => res.redirect('/'))
|
|
123
|
-
})
|
|
124
|
-
- } else res.redirect(authSignOutUrl)
|
|
125
|
-
- })
|
|
126
|
-
-
|
|
127
|
-
- router.use('/account-details', (req, res) => {
|
|
128
|
-
- res.redirect(`${authUrl}/account-details?${authParameters}`)
|
|
129
|
-
+ } else res.redirect('/')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
router.use(async (req, res, next) => {
|
|
133
|
-
- if (req.isAuthenticated() && (await tokenVerificationClient.verifyToken(req as unknown as AuthenticatedRequest))) {
|
|
134
|
-
- return next()
|
|
135
|
-
+ if (!req.isAuthenticated()) {
|
|
136
|
-
+ req.session.returnTo = req.originalUrl
|
|
137
|
-
+ return res.redirect('/sign-in')
|
|
138
|
-
}
|
|
139
|
-
- req.session.returnTo = req.originalUrl
|
|
140
|
-
- return res.redirect('/sign-in')
|
|
141
|
-
- })
|
|
142
|
-
|
|
143
|
-
+ return prisonerAuth
|
|
144
|
-
+ .validateAndRefreshUser(req.user as LaunchpadUser)
|
|
145
|
-
+ .then(user => {
|
|
146
|
-
+ req.user = user
|
|
147
|
-
+ next()
|
|
148
|
-
+ })
|
|
149
|
-
+ .catch(() => res.redirect('/autherror'))
|
|
150
|
-
+ })
|
|
151
|
-
+
|
|
152
|
-
router.use((req, res, next) => {
|
|
153
|
-
res.locals.user = req.user as HmppsUser
|
|
154
|
-
next()
|