Skip to content

Commit dc43ffa

Browse files
authored
RHINENG-25551 fix(cost-management): surface SSO auth errors instead of silent failures (#2826)
* RHINENG-25551 fix(cost-management): surface SSO auth errors instead of silent failures When the plugin is configured with invalid hybrid cloud service account credentials, the UI previously showed a generic "Bad Gateway" or empty table instead of an actionable error message. - tokenUtil: extract SSO error details (error_description/error) from the response body instead of throwing bare HTTP status text - secureProxy: return 502 with credential-specific message when SSO authentication fails, distinguishing it from other proxy errors - OptimizationsClient & CostManagementSlimClient: read the error field from JSON response bodies on non-OK responses so the frontend displays the backend's descriptive message Made-with: Cursor * RHINENG-25551 refactor(cost-management): use custom error class for SSO auth failures Replace brittle string-prefix matching with SsoAuthenticationError class so secureProxy detects auth failures via instanceof instead of message.startsWith(), making the contract compile-time enforced. Made-with: Cursor
1 parent 4a316f7 commit dc43ffa

4 files changed

Lines changed: 69 additions & 7 deletions

File tree

workspaces/cost-management/plugins/cost-management-backend/src/routes/secureProxy.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
costPluginPermissions,
2626
} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';
2727
import { AuthorizeResult } from '@backstage/plugin-permission-common';
28-
import { getTokenFromApi } from '../util/tokenUtil';
28+
import { getTokenFromApi, SsoAuthenticationError } from '../util/tokenUtil';
2929
import { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';
3030
import { resolveActor, emitAuditLog } from '../util/auditLog';
3131

@@ -410,6 +410,15 @@ export const secureProxy: (options: RouterOptions) => RequestHandler =
410410
return res.send(await upstreamResponse.text());
411411
} catch (error) {
412412
options.logger.error('Secure proxy error', error);
413+
414+
if (error instanceof SsoAuthenticationError) {
415+
return res.status(502).json({
416+
error:
417+
'Unable to authenticate with the hybrid cloud console. ' +
418+
'Please check your service account credentials (clientId/clientSecret) in the RHDH Cost Management configuration.',
419+
});
420+
}
421+
413422
return res.status(500).json({ error: 'Internal proxy error' });
414423
}
415424
};

workspaces/cost-management/plugins/cost-management-backend/src/util/tokenUtil.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ import assert from 'assert';
1818
import { RouterOptions } from '../models/RouterOptions';
1919
import { DEFAULT_SSO_BASE_URL } from './constant';
2020

21+
export class SsoAuthenticationError extends Error {
22+
constructor(message: string, public readonly statusCode: number) {
23+
super(message);
24+
this.name = 'SsoAuthenticationError';
25+
}
26+
}
27+
2128
// Cache key for token storage
2229
const TOKEN_CACHE_KEY = 'sso_access_token';
2330

@@ -100,7 +107,18 @@ export const getTokenFromApi = async (options: RouterOptions) => {
100107

101108
logger.info(`Token cached, expires in ${expires_in} seconds`);
102109
} else {
103-
throw new Error(rhSsoResponse.statusText);
110+
let detail = '';
111+
try {
112+
const body = await rhSsoResponse.json();
113+
detail = body.error_description || body.error || '';
114+
} catch {
115+
// response may not be JSON
116+
}
117+
const message = detail
118+
? `SSO authentication failed: ${detail}`
119+
: `SSO authentication failed with status ${rhSsoResponse.status} (${rhSsoResponse.statusText})`;
120+
logger.error(message);
121+
throw new SsoAuthenticationError(message, rhSsoResponse.status);
104122
}
105123

106124
return accessToken;

workspaces/cost-management/plugins/cost-management-common/src/clients/cost-management/CostManagementSlimClient.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,14 @@ export class CostManagementSlimClient implements CostManagementSlimApi {
8888
});
8989

9090
if (!response.ok) {
91-
throw new Error(response.statusText);
91+
let message = response.statusText;
92+
try {
93+
const body = (await response.json()) as any;
94+
if (body.error) message = body.error;
95+
} catch {
96+
// response may not be JSON
97+
}
98+
throw new Error(message);
9299
}
93100

94101
// Get the response data
@@ -518,7 +525,14 @@ export class CostManagementSlimClient implements CostManagementSlimApi {
518525
});
519526

520527
if (!response.ok) {
521-
throw new Error(response.statusText);
528+
let message = response.statusText;
529+
try {
530+
const body = (await response.json()) as any;
531+
if (body.error) message = body.error;
532+
} catch {
533+
// response may not be JSON
534+
}
535+
throw new Error(message);
522536
}
523537

524538
return {
@@ -544,7 +558,14 @@ export class CostManagementSlimClient implements CostManagementSlimApi {
544558
});
545559

546560
if (!response.ok) {
547-
throw new Error(response.statusText);
561+
let message = response.statusText;
562+
try {
563+
const body = (await response.json()) as any;
564+
if (body.error) message = body.error;
565+
} catch {
566+
// response may not be JSON
567+
}
568+
throw new Error(message);
548569
}
549570

550571
return {

workspaces/cost-management/plugins/cost-management-common/src/clients/optimizations/OptimizationsClient.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,14 @@ export class OptimizationsClient implements OptimizationsApi {
9898
);
9999

100100
if (!response.ok) {
101-
throw new Error(response.statusText);
101+
let message = response.statusText;
102+
try {
103+
const body = (await response.json()) as any;
104+
if (body.error) message = body.error;
105+
} catch {
106+
// response may not be JSON
107+
}
108+
throw new Error(message);
102109
}
103110

104111
return {
@@ -133,7 +140,14 @@ export class OptimizationsClient implements OptimizationsApi {
133140
);
134141

135142
if (!response.ok) {
136-
throw new Error(response.statusText);
143+
let message = response.statusText;
144+
try {
145+
const body = (await response.json()) as any;
146+
if (body.error) message = body.error;
147+
} catch {
148+
// response may not be JSON
149+
}
150+
throw new Error(message);
137151
}
138152

139153
return {

0 commit comments

Comments
 (0)