1
1
import { joinUrl } from '@guardian/libs' ;
2
2
import type { NextFunction , Request , Response } from 'express' ;
3
+ import { z } from 'zod' ;
3
4
import {
4
5
clearIdentityLocalState ,
5
6
getIdentityLocalState ,
6
7
setIdentityLocalState ,
7
8
} from '@/server/IdentityLocalState' ;
8
9
import { OAuthAccessTokenCookieName } from '@/server/oauth' ;
10
+ import type { OktaConfig } from '@/server/oktaConfig' ;
9
11
import { getConfig as getOktaConfig } from '@/server/oktaConfig' ;
10
12
import { requiresSignin } from '../../shared/requiresSignin' ;
11
13
12
14
declare const CYPRESS : string ;
13
15
14
- type OktaUserInfo = {
15
- legacy_identity_id : string ;
16
- email : string ;
17
- name : string ;
16
+ const OktaUserInfo = z . object ( {
17
+ legacy_identity_id : z . optional ( z . string ( ) ) ,
18
+ email : z . optional ( z . string ( ) ) ,
19
+ name : z . optional ( z . string ( ) ) ,
20
+ } ) ;
21
+
22
+ interface OktaValidationResponse {
23
+ ok : boolean ;
24
+ valid : boolean ;
25
+ userId ?: string ;
26
+ displayName ?: string ;
27
+ email ?: string ;
28
+ }
29
+
30
+ const validateWithOkta = async ( {
31
+ oktaConfig,
32
+ accessToken,
33
+ } : {
34
+ oktaConfig : OktaConfig ;
35
+ accessToken : string ;
36
+ } ) : Promise < OktaValidationResponse > => {
37
+ const issuerUrl = joinUrl (
38
+ oktaConfig . orgUrl ,
39
+ '/oauth2/' ,
40
+ oktaConfig . authServerId ,
41
+ ) ;
42
+
43
+ try {
44
+ const oktaResponse = await fetch ( `${ issuerUrl } /v1/userinfo/` , {
45
+ headers : {
46
+ 'Content-Type' : 'application/json' ,
47
+ Authorization : `Bearer ${ accessToken } ` ,
48
+ } ,
49
+ } ) ;
50
+ if ( oktaResponse . status === 200 ) {
51
+ // Valid token
52
+ const oktaUserInfo = OktaUserInfo . parse ( await oktaResponse . json ( ) ) ;
53
+ return {
54
+ ok : true ,
55
+ valid : true ,
56
+ userId : oktaUserInfo . legacy_identity_id ,
57
+ displayName : oktaUserInfo . name ,
58
+ email : oktaUserInfo . email ,
59
+ } ;
60
+ } else if ( [ 401 , 403 ] . includes ( oktaResponse . status ) ) {
61
+ console . error (
62
+ `OAuth / Serverside Validation Middleware / Error: invalid token.` ,
63
+ ) ;
64
+ return { ok : true , valid : false } ;
65
+ } else {
66
+ console . error (
67
+ `OAuth / Serverside Validation Middleware / Error: unexpected status code from Okta: ${ oktaResponse . status } ` ,
68
+ ) ;
69
+ return { ok : false , valid : false } ;
70
+ }
71
+ } catch ( error ) {
72
+ console . error (
73
+ `OAuth / Serverside Validation Middleware / Error: ${ error . message } ` ,
74
+ ) ;
75
+ return { ok : false , valid : false } ;
76
+ }
18
77
} ;
19
78
20
79
export const withOktaSeverSideValidation = async (
@@ -27,66 +86,83 @@ export const withOktaSeverSideValidation = async (
27
86
}
28
87
const oktaConfig = await getOktaConfig ( ) ;
29
88
if ( ! oktaConfig . useOkta ) {
30
- console . log ( 'Okta disabled, skipping Okta server side validation...' ) ;
89
+ /**
90
+ * OKTA NOT ENABLED
91
+ *
92
+ * If Okta is disabled, we will still run this middleware, but
93
+ * it won't be able to do anyting so we just pass through.
94
+ */
31
95
return next ( ) ;
32
96
}
33
97
34
- console . log ( 'Validating token server side ...' ) ;
35
98
const signinRequired = requiresSignin ( req . originalUrl ) ;
36
99
37
100
const locallyValidatedIdentityData = getIdentityLocalState ( res ) ;
38
101
const accessToken = req . signedCookies [ OAuthAccessTokenCookieName ] ;
39
102
if ( ! accessToken || ! locallyValidatedIdentityData ?. userId ) {
40
103
if ( signinRequired ) {
104
+ /**
105
+ * NO TOKEN OR USER - SIGN IN REQUIRED
106
+ *
107
+ * This is unexpected and should be impossible because the withIdentity
108
+ * middleware should have run first and not allowed the request to get
109
+ * here. Return a 500.
110
+ */
41
111
console . error (
42
- 'error : no access token or user in request for a sign-in required endpoint! this should have failed local validation' ,
112
+ `OAuth / Serverside Validation Middleware / Error : no access token or user in request for a sign-in required endpoint! This should have failed local validation.` ,
43
113
) ;
44
114
return res . sendStatus ( 500 ) ;
45
115
} else {
116
+ /**
117
+ * NO TOKEN OR USER - SIGN IN NOT REQUIRED
118
+ *
119
+ * This is expected - continue.
120
+ */
46
121
return next ( ) ;
47
122
}
48
123
}
49
124
50
- const issuerUrl = joinUrl (
51
- oktaConfig . orgUrl ,
52
- '/oauth2/' ,
53
- oktaConfig . authServerId ,
54
- ) ;
55
-
56
- const oktaResponse = await fetch ( `${ issuerUrl } /v1/userinfo/` , {
57
- method : 'GET' ,
58
- headers : {
59
- 'Content-Type' : 'application/json' ,
60
- Authorization : `Bearer ${ accessToken } ` ,
61
- } ,
125
+ const oktaValidationResponse = await validateWithOkta ( {
126
+ oktaConfig,
127
+ accessToken,
62
128
} ) ;
63
129
64
- console . log ( `okta response status: ${ oktaResponse . status } ` ) ;
65
- if ( oktaResponse . status == 200 ) {
66
- //valid token
67
- const oktaUserInfo = ( await oktaResponse . json ( ) ) as OktaUserInfo ;
68
- // Refresh the local state from the Okta response.
69
- setIdentityLocalState ( res , {
70
- // This is always 'signedInRecently' because we've just checked
71
- // the token is valid with Okta, and it's only valid for 30 minutes.
72
- signInStatus : 'signedInRecently' ,
73
- userId : oktaUserInfo . legacy_identity_id ,
74
- displayName : oktaUserInfo . name ,
75
- email : oktaUserInfo . email ,
76
- } ) ;
77
- return next ( ) ;
78
- } else if ( [ 401 , 403 ] . includes ( oktaResponse . status ) ) {
79
- //invalid token
130
+ if ( ! oktaValidationResponse . ok ) {
131
+ /**
132
+ * UNEXPECTED ERROR
133
+ */
134
+ return res . sendStatus ( 500 ) ;
135
+ }
136
+
137
+ if ( ! oktaValidationResponse . valid ) {
138
+ /**
139
+ * INVALID TOKEN
140
+ *
141
+ * Clear the local state. If the endpoint requires sign-in ,
142
+ * return a 401 which can be handled down the line. Otherwise
143
+ * continue (the user will see the page as if they are not
144
+ * signed in).
145
+ */
80
146
clearIdentityLocalState ( res ) ;
81
147
if ( signinRequired ) {
82
148
return res . sendStatus ( 401 ) ;
83
149
}
84
150
return next ( ) ;
85
- } else {
86
- //unexpected return status
87
- console . error (
88
- `unexpected status from okta userinfo ${ oktaResponse . status } ` ,
89
- ) ;
90
- return res . sendStatus ( 500 ) ;
91
151
}
152
+
153
+ /**
154
+ * VALID TOKEN
155
+ *
156
+ * Update the local state with the latest user info from Okta.
157
+ */
158
+ setIdentityLocalState ( res , {
159
+ // This is always 'signedInRecently' because we've just checked
160
+ // the token is valid with Okta, and it's only valid for 30 minutes.
161
+ signInStatus : 'signedinrecently' ,
162
+ userId : oktaValidationResponse . userId ,
163
+ displayName : oktaValidationResponse . displayName ,
164
+ email : oktaValidationResponse . email ,
165
+ } ) ;
166
+
167
+ return next ( ) ;
92
168
} ;
0 commit comments