Skip to content

Commit 552ee3f

Browse files
committed
Improve OIDC security by using state and nonce variables & fix referrer redirect when using OIDC
1 parent 65aa77c commit 552ee3f

File tree

7 files changed

+117
-17
lines changed

7 files changed

+117
-17
lines changed

src/authentication/baseAPIAuthProvider.tsx

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,62 @@ export default abstract class BaseAPIAuthProvider extends BaseAuthProvider {
144144
});
145145
}
146146

147+
public verifyOIDCStateParam(stateToTest: string | null): boolean {
148+
const oidcState = sessionStorage.getItem('oidcState');
149+
150+
// TODO: should we verify if session storage has been cleared?
151+
if (oidcState === null) return true;
152+
153+
if (oidcState === stateToTest) return true;
154+
155+
document.dispatchEvent(
156+
new CustomEvent('scigateway', {
157+
detail: {
158+
type: NotificationType,
159+
payload: {
160+
message:
161+
'It is not possible to authenticate you at the moment. Please, try again later.',
162+
severity: 'error',
163+
},
164+
},
165+
})
166+
);
167+
168+
return false;
169+
}
170+
171+
public async verifyOIDCNonce(accessToken: string): Promise<boolean> {
172+
const oidcNonce = sessionStorage.getItem('oidcNonce');
173+
174+
// TODO: should we verify if session storage has been cleared?
175+
if (oidcNonce === null) return true;
176+
177+
const encryptedOIDCNonce =
178+
await generateCodeChallengeFromVerifier(oidcNonce);
179+
180+
const { nonce: nonceToTest } = JSON.parse(parseJwt(accessToken));
181+
182+
if (encryptedOIDCNonce === nonceToTest) {
183+
return true;
184+
}
185+
186+
log.error('Nonce verification failed');
187+
document.dispatchEvent(
188+
new CustomEvent('scigateway', {
189+
detail: {
190+
type: NotificationType,
191+
payload: {
192+
message:
193+
'It is not possible to authenticate you at the moment. Please, try again later.',
194+
severity: 'error',
195+
},
196+
},
197+
})
198+
);
199+
200+
return false;
201+
}
202+
147203
async pkceToken(
148204
token: string,
149205
oidcProvider: InitialisedOIDCProvider
@@ -191,7 +247,15 @@ export default abstract class BaseAPIAuthProvider extends BaseAuthProvider {
191247
id_token = await this.nonPKCEToken(token, oidcProvider);
192248
}
193249

194-
// TODO: sometimes login request fails here - maybe because it was too fast?
250+
if (!(await this.verifyOIDCNonce(id_token)))
251+
throw Error('Nonce verification failed');
252+
253+
// TODO: sometimes login request fails here because it was too fast
254+
// aka JWT iat is not yet "in the past"
255+
// need to talk to backend people about it
256+
await new Promise<void>((resolve, _reject) => {
257+
setTimeout(() => resolve(), 1_000);
258+
});
195259

196260
const { data: jwt } = await axios.post(
197261
`${this.authUrl}/oidc_login/${oidcProvider.provider_id}`,
@@ -206,6 +270,8 @@ export default abstract class BaseAPIAuthProvider extends BaseAuthProvider {
206270
this.storeToken(jwt);
207271
sessionStorage.removeItem('codeVerifier');
208272
sessionStorage.removeItem('oidcProviderId');
273+
sessionStorage.removeItem('oidcState');
274+
sessionStorage.removeItem('oidcNonce');
209275
const payload: {
210276
sessionId: string;
211277
username: string;
@@ -220,16 +286,28 @@ export default abstract class BaseAPIAuthProvider extends BaseAuthProvider {
220286
}
221287
}
222288

223-
public async setupOIDC(oidcProvider: InitialisedOIDCProvider): Promise<void> {
289+
public async setupOIDC(
290+
oidcProvider: InitialisedOIDCProvider,
291+
referrer?: string
292+
): Promise<void> {
293+
console.log('setupOIDC called');
224294
let codeChallenge: string | undefined;
225295
if (oidcProvider.pkce) {
226296
const codeVerifier = generateCodeVerifier();
227297
sessionStorage.setItem('codeVerifier', codeVerifier);
228298
codeChallenge = await generateCodeChallengeFromVerifier(codeVerifier);
229299
}
230300
sessionStorage.setItem('oidcProviderId', oidcProvider.provider_id);
301+
const state = generateCodeVerifier();
302+
sessionStorage.setItem('oidcState', state);
303+
const nonce = generateCodeVerifier();
304+
const encryptedNonce = await generateCodeChallengeFromVerifier(nonce);
305+
306+
sessionStorage.setItem('oidcNonce', nonce);
307+
308+
if (referrer) sessionStorage.setItem('referrer', referrer);
231309

232-
this.redirectUrl = `${oidcProvider.authorization_endpoint}?client_id=${oidcProvider.client_id}&redirect_uri=${window.location.origin}/login&response_type=code${oidcProvider.pkce ? `&code_challenge_method=S256&code_challenge=${codeChallenge}` : ''}&scope=${oidcProvider.scope}`;
310+
this.redirectUrl = `${oidcProvider.authorization_endpoint}?client_id=${oidcProvider.client_id}&redirect_uri=${window.location.origin}/login&response_type=code${oidcProvider.pkce ? `&code_challenge_method=S256&code_challenge=${codeChallenge}` : ''}&scope=${oidcProvider.scope}&state=${state}&nonce=${encryptedNonce}`;
233311
}
234312

235313
public async initialiseOIDCProviders(): Promise<InitialisedOIDCProvider[]> {

src/authentication/icatAuthProvider.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,15 @@ export default class ICATAuthProvider extends BaseAPIAuthProvider {
9191

9292
public async setAuthenticator(
9393
mnemonic: string,
94-
disableSideEffects?: boolean
94+
disableSideEffects?: boolean,
95+
referrer?: string
9596
): Promise<void> {
9697
const oidcProvider = this.oidcProviders.find(
9798
(op) => op.provider_id === mnemonic
9899
);
99100
this.mnemonic = mnemonic;
100101
if (oidcProvider && !disableSideEffects) {
101-
await this.setupOIDC(oidcProvider);
102+
await this.setupOIDC(oidcProvider, referrer);
102103
}
103104
return Promise.resolve();
104105
}
@@ -133,7 +134,10 @@ export default class ICATAuthProvider extends BaseAPIAuthProvider {
133134
if (oidcProvider) {
134135
const params = new URLSearchParams(password);
135136
const code = params.get('code');
136-
if (!code) {
137+
138+
const state = params.get('state');
139+
140+
if (!code || !this.verifyOIDCStateParam(state)) {
137141
this.logOut();
138142
return Promise.resolve();
139143
}

src/authentication/oidcAuthProvider.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,16 @@ export default class OIDCAuthProvider extends BaseAPIAuthProvider {
3737

3838
public async setAuthenticator(
3939
provider: string,
40-
disableSideEffects?: boolean
40+
disableSideEffects?: boolean,
41+
referrer?: string
4142
): Promise<void> {
4243
const oidcProvider = this.oidcProviders.find(
4344
(oidc) => oidc.provider_id === provider
4445
);
4546
if (oidcProvider) {
4647
this.oidcProvider = oidcProvider;
4748
if (!disableSideEffects) {
48-
await this.setupOIDC(oidcProvider);
49+
await this.setupOIDC(oidcProvider, referrer);
4950
}
5051
} else {
5152
log.error(
@@ -59,7 +60,9 @@ export default class OIDCAuthProvider extends BaseAPIAuthProvider {
5960
const params = new URLSearchParams(password);
6061
const code = params.get('code');
6162

62-
if (!code || !this.oidcProvider) {
63+
const state = params.get('state');
64+
65+
if (!code || !this.oidcProvider || !this.verifyOIDCStateParam(state)) {
6366
this.logOut();
6467
return Promise.resolve();
6568
}

src/authentication/passwordAndOIDCAuthProvider.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export default class PasswordAndOIDCAuthProvider extends BaseAPIAuthProvider {
4242

4343
public async setAuthenticator(
4444
authenticatorKey: string,
45-
disableSideEffects?: boolean
45+
disableSideEffects?: boolean,
46+
referrer?: string
4647
): Promise<void> {
4748
const newAuthenticator = this.authenticators.find(
4849
(a) => a.key === authenticatorKey
@@ -61,7 +62,7 @@ export default class PasswordAndOIDCAuthProvider extends BaseAPIAuthProvider {
6162
(oidc) => oidc.provider_id === this.authenticator?.key
6263
);
6364
if (oidcProvider) {
64-
await this.setupOIDC(oidcProvider);
65+
await this.setupOIDC(oidcProvider, referrer);
6566
} else {
6667
log.error(
6768
`Can't find oidc provider matching the specified authenticator: ${authenticatorKey}`
@@ -81,7 +82,9 @@ export default class PasswordAndOIDCAuthProvider extends BaseAPIAuthProvider {
8182
(op) => op.provider_id === this.authenticator?.key
8283
);
8384

84-
if (!code || !oidcProvider) {
85+
const state = params.get('state');
86+
87+
if (!code || !oidcProvider || !this.verifyOIDCStateParam(state)) {
8588
this.logOut();
8689
return Promise.resolve();
8790
}

src/loginPage/loginPage.component.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,11 @@ export const LoginPageComponent = (
340340
setAuthenticator(newAuthenticator);
341341
props.auth.provider.setAuthenticator?.(
342342
newAuthenticator,
343-
disableSideEffects
343+
disableSideEffects,
344+
location.state?.referrer
344345
);
345346
},
346-
[props.auth.provider]
347+
[location.state?.referrer, props.auth.provider]
347348
);
348349

349350
React.useEffect(() => {

src/routing/routing.component.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ export const UnauthorisedPlugin = PluginPlaceHolder;
124124
// Prevents the component from updating when the draw is opened/closed
125125
export const AuthorisedAdminPage = withAuth(true)(AdminPage);
126126

127+
const popSessionStorageItem = (
128+
key: string
129+
): ReturnType<typeof sessionStorage.getItem> => {
130+
const result = sessionStorage.getItem(key);
131+
sessionStorage.removeItem(key);
132+
return result;
133+
};
134+
127135
const Routing: React.FC<RoutingProps> = (props: RoutingProps) => {
128136
const adminRoutes = getAdminRoutes({ plugins: props.plugins });
129137
// only set to false if we're on a plugin route i.e. not a scigateway route
@@ -248,8 +256,10 @@ const Routing: React.FC<RoutingProps> = (props: RoutingProps) => {
248256
to={{
249257
pathname:
250258
prevUserIsLoggedIn === false && props.userIsLoggedIn
251-
? (props.location.state as { referrer?: string })
252-
?.referrer ?? scigatewayRoutes.home
259+
? (((props.location.state as { referrer?: string })
260+
?.referrer ||
261+
popSessionStorageItem('referrer')) ??
262+
scigatewayRoutes.home)
253263
: scigatewayRoutes.logout,
254264
state: { referrer: props.location.pathname },
255265
}}

src/state/state.types.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ export interface AuthProvider {
111111
}[];
112112
setAuthenticator?: (
113113
key: string,
114-
disableSideEffects?: boolean
114+
disableSideEffects?: boolean,
115+
referrer?: string
115116
) => Promise<void>;
116117
getAuthenticator?: () => string;
117118
initialise?: () => Promise<void>;

0 commit comments

Comments
 (0)