@ministryofjustice/hmpps-prisoner-auth 0.0.1 → 0.0.3
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/package.json +2 -4
- package/dist/main/launchpadAuthStrategy.d.ts +0 -11
- package/dist/main/prisonerAuthStrategy.d.ts +0 -12
- package/dist/main/prisonerAuthStrategy.test.d.ts +0 -1
- package/dist/main/tokenRefresh.d.ts +0 -3
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ministryofjustice/hmpps-prisoner-auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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"
|
|
@@ -39,7 +38,6 @@
|
|
|
39
38
|
"jsonwebtoken": "^9.0.3"
|
|
40
39
|
},
|
|
41
40
|
"dependencies": {
|
|
42
|
-
"@ministryofjustice/hmpps-rest-client": "^1.1.0",
|
|
43
41
|
"passport-openidconnect": "^0.1.2",
|
|
44
42
|
"superagent": "^10.3.0"
|
|
45
43
|
},
|
|
@@ -1,11 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import OpenIDConnectStrategy from 'passport-openidconnect';
|
|
2
|
-
type Scope = 'user.basic.read' | 'user.establishment.read' | 'user.booking.read';
|
|
3
|
-
export type PrisonerAuthStrategyOptions = {
|
|
4
|
-
launchpadAuthUrl: string;
|
|
5
|
-
clientID: string;
|
|
6
|
-
clientSecret: string;
|
|
7
|
-
callbackURL?: string;
|
|
8
|
-
scope?: Scope | Scope[];
|
|
9
|
-
};
|
|
10
|
-
export declare function prisonerAuthStrategy(options: PrisonerAuthStrategyOptions): OpenIDConnectStrategy;
|
|
11
|
-
export declare const openIDConnectStrategyOptionsFrom: (options: PrisonerAuthStrategyOptions) => OpenIDConnectStrategy.StrategyOptions;
|
|
12
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
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()
|