Skip to content

Commit a83e948

Browse files
committed
Update authentication-backend, prevent open redirect attacks
1 parent 95d6b5e commit a83e948

File tree

13 files changed

+379
-233
lines changed

13 files changed

+379
-233
lines changed

backend/.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
package-lock=true
2+
ignore-scripts=true

backend/package-lock.json

Lines changed: 276 additions & 197 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"test": "lean-test --preprocess tsc --parallel"
1111
},
1212
"dependencies": {
13-
"authentication-backend": "1.x",
13+
"authentication-backend": "1.3.x",
1414
"collection-storage": "3.x",
1515
"express": "5.x",
1616
"express-static-gzip": "3.x",

backend/src/api-tests/testConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const baseTestConfig: ConfigT = {
4040
accessTokenUrl: '',
4141
userUrl: '',
4242
},
43-
gitlab: { clientId: '', authUrl: '', tokenInfoUrl: '' },
43+
gitlab: { clientId: '', authUrl: '', accessTokenUrl: '', tokenInfoUrl: '' },
4444
},
4545
giphy: { baseUrl: '', apiKey: '' },
4646
};

backend/src/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function getAuthBackend(
3535
userAuthService.grantLoginToken,
3636
);
3737
return {
38-
addRoutes: (app) => app.useHTTP('/api/sso', sso.router),
38+
addRoutes: (app) => app.useHTTP('/api/sso', sso.router()),
3939
clientConfig: sso.service.clientConfig,
4040
};
4141
}

backend/src/config/default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default {
5252
gitlab: {
5353
clientId: '',
5454
authUrl: 'https://gitlab.com/oauth/authorize',
55+
accessTokenUrl: 'https://gitlab.com/oauth/token',
5556
tokenInfoUrl: 'https://gitlab.com/oauth/token/info',
5657
},
5758
},

docs/SERVICES.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,12 @@ SSO_GITHUB_CLIENT_ID="idhere" SSO_GITHUB_CLIENT_SECRET="secrethere" ./index.js
116116

117117
You will need a GitLab client ID:
118118

119-
1. Go to <https://gitlab.com/profile/applications>
119+
1. Go to <https://gitlab.com/-/user_settings/applications>
120120
2. Set the "Redirect URI" to match your deployment with
121121
`/sso/gitlab` appended to the end. e.g. for local
122122
testing, this could be `http://localhost:5000/sso/gitlab`
123-
3. Untick the "confidential" option. You do not need to enable
124-
any scopes.
123+
3. Untick the "confidential" option and select the "email"
124+
scope (this is the closest we can get to no scopes)
125125
4. Record the application ID (you will not need the secret).
126126

127127
You can now invoke the application with the `SSO_GITLAB_CLIENT_ID`
@@ -137,6 +137,7 @@ the auth and token info URLs:
137137

138138
```sh
139139
SSO_GITLAB_AUTH_URL="https://gitlab.example.com/oauth/authorize" \
140+
SSO_GITLAB_ACCESS_TOKEN_URL="https://gitlab.example.com/oauth/token" \
140141
SSO_GITLAB_TOKEN_INFO_URL="https://gitlab.example.com/oauth/token/info" \
141142
SSO_GITLAB_CLIENT_ID="idhere" \
142143
./index.js

frontend/src/api/PasswordService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { sha1 } from '../helpers/crypto';
1+
import { digest, toHex } from '../helpers/crypto';
22

33
export class PasswordService {
44
public constructor(private readonly apiBase: string) {}
@@ -7,7 +7,7 @@ export class PasswordService {
77
password: string,
88
signal: AbortSignal,
99
): Promise<number> {
10-
const passwordHash = (await sha1(password)).toUpperCase();
10+
const passwordHash = toHex(await digest(password, 'SHA-1')).toUpperCase();
1111
const key = passwordHash.substring(0, 5);
1212
const rest = passwordHash.substring(5);
1313

frontend/src/api/UserTokenService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export class UserTokenService {
66
public async login(
77
service: string,
88
externalToken: string,
9+
redirectUri: string,
10+
codeVerifier: string | undefined,
911
signal: AbortSignal,
1012
): Promise<string> {
1113
const body = await jsonFetch<{ userToken: string }>(
@@ -14,7 +16,7 @@ export class UserTokenService {
1416
method: 'POST',
1517
cache: 'no-cache',
1618
headers: { 'Content-Type': 'application/json' },
17-
body: JSON.stringify({ externalToken }),
19+
body: JSON.stringify({ externalToken, redirectUri, codeVerifier }),
1820
signal,
1921
},
2022
);

frontend/src/components/login/LoginCallback.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,35 @@ export const LoginCallback = memo(({ service }: PropsT) => {
1919
useEffect(() => {
2020
const ac = new AbortController();
2121
const { search, hash } = document.location;
22-
const nonce = storage.getItem('login-nonce');
23-
handleLogin(service, nonce, { search, hash }, ac.signal)
22+
const redirectUri = document.location.href.split('?')[0]!;
23+
const localState = storage.getItem('login-state');
24+
handleLogin(service, localState, { search, hash, redirectUri }, ac.signal)
2425
.then((redirect) => {
2526
if (ac.signal.aborted) {
2627
return;
2728
}
28-
storage.removeItem('login-nonce');
29+
storage.removeItem('login-state');
30+
if (
31+
new URL(redirect, document.location.href).host !==
32+
document.location.host
33+
) {
34+
// possibly a malicious redirect - ignore it and substitute a safe one
35+
redirect = '/';
36+
}
2937
stableSetLocation(redirect, { replace: true });
3038
})
3139
.catch((err) => {
3240
if (ac.signal.aborted) {
3341
return;
3442
}
3543
if (!(err instanceof Error)) {
36-
storage.removeItem('login-nonce');
44+
storage.removeItem('login-state');
3745
setError(String(err));
3846
} else if (err.message === 'unrecognised login details') {
3947
// GitLab shows a bare link to the /sso/login URL on the confirmation page
4048
stableSetLocation('/', { replace: true });
4149
} else {
42-
storage.removeItem('login-nonce');
50+
storage.removeItem('login-state');
4351
setError(err.message);
4452
}
4553
});

0 commit comments

Comments
 (0)