Skip to content

Commit e3bc79a

Browse files
authored
fix(controlplane): handle authentication errors correctly (#2354)
1 parent b4c18f7 commit e3bc79a

File tree

3 files changed

+43
-12
lines changed

3 files changed

+43
-12
lines changed

controlplane/src/core/auth-utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ const pkceMaxAgeSec = 60 * 15; // 15 minutes
4343
const pkceCodeAlgorithm = 'S256';
4444
const scope = 'openid profile email';
4545

46+
const axiosStatusValidator = function (status: number): boolean {
47+
// All user-level (4xx) errors are handled below by returning `AuthenticationError`.
48+
// For any server errors (5xx), we want to surface those separately.
49+
return status < 500;
50+
};
51+
4652
export default class AuthUtils {
4753
private webUrl: URL;
4854
private readonly webDomain: string;
@@ -131,6 +137,7 @@ export default class AuthUtils {
131137
headers: {
132138
Authorization: `Bearer ${accessToken}`,
133139
},
140+
validateStatus: axiosStatusValidator,
134141
});
135142

136143
if (res.status !== 200) {
@@ -156,6 +163,7 @@ export default class AuthUtils {
156163
client_id: this.opts.oauth.clientID,
157164
scope,
158165
}),
166+
validateStatus: axiosStatusValidator,
159167
});
160168

161169
if (res.status !== 200) {
@@ -273,6 +281,7 @@ export default class AuthUtils {
273281
code,
274282
redirect_uri: this.getRedirectUri({ redirectURL, sso: ssoSlug }),
275283
}),
284+
validateStatus: axiosStatusValidator,
276285
});
277286

278287
if (resp.status !== 200) {

controlplane/src/core/crypto/jwt.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { hkdf, randomFill, randomUUID, subtle } from 'node:crypto';
2-
import { decodeJwt, EncryptJWT, jwtDecrypt, JWTPayload, jwtVerify, KeyLike, SignJWT } from 'jose';
2+
import { decodeJwt, EncryptJWT, jwtDecrypt, errors, JWTPayload, jwtVerify, KeyLike, SignJWT } from 'jose';
3+
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
4+
import { AuthenticationError } from '../errors/errors.js';
35
import { JWTDecodeParams, JWTEncodeParams } from '../../types/index.js';
46
import { base64URLEncode } from '../util.js';
57

@@ -76,10 +78,21 @@ export async function decrypt<Payload = JWTPayload>(params: JWTDecodeParams): Pr
7678
throw new Error('No token provided');
7779
}
7880
const encryptionSecret = await getDerivedEncryptionKey(secret);
79-
const { payload } = await jwtDecrypt(token, encryptionSecret, {
80-
clockTolerance: 15,
81-
});
82-
return payload as Payload;
81+
try {
82+
const { payload } = await jwtDecrypt(token, encryptionSecret, {
83+
clockTolerance: 15,
84+
});
85+
return payload as Payload;
86+
} catch (err: any) {
87+
if (
88+
err instanceof errors.JWTExpired ||
89+
err instanceof errors.JWTClaimValidationFailed ||
90+
err instanceof errors.JWTInvalid
91+
) {
92+
throw new AuthenticationError(EnumStatusCode.ERROR_NOT_AUTHENTICATED, err.message);
93+
}
94+
throw err;
95+
}
8396
}
8497

8598
/**

controlplane/src/core/services/Keycloak.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import KeycloakAdminClient from '@keycloak/keycloak-admin-client';
1+
import KeycloakAdminClient, { NetworkError } from '@keycloak/keycloak-admin-client';
22
import { RequiredActionAlias } from '@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation.js';
3+
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
34
import { uid } from 'uid';
45
import { FastifyBaseLogger } from 'fastify';
56
import { MemberRole } from '../../db/models.js';
67
import { organizationRoleEnum } from '../../db/schema.js';
8+
import { AuthenticationError } from '../errors/errors.js';
79

810
export default class Keycloak {
911
client: KeycloakAdminClient;
@@ -35,12 +37,19 @@ export default class Keycloak {
3537
}
3638

3739
public async authenticateClient() {
38-
await this.client.auth({
39-
grantType: 'password',
40-
username: this.adminUser,
41-
password: this.adminPassword,
42-
clientId: 'admin-cli',
43-
});
40+
try {
41+
await this.client.auth({
42+
grantType: 'password',
43+
username: this.adminUser,
44+
password: this.adminPassword,
45+
clientId: 'admin-cli',
46+
});
47+
} catch (err: any) {
48+
if (err instanceof NetworkError) {
49+
throw new AuthenticationError(EnumStatusCode.ERROR_NOT_AUTHENTICATED, err.message);
50+
}
51+
throw err;
52+
}
4453
}
4554

4655
public async roleExists({ realm, roleName }: { realm?: string; roleName: string }): Promise<boolean> {

0 commit comments

Comments
 (0)