Merge pull request #34354 from officialasishkumar/fix-proxied-signin-base64url-token

fix(core-components): decode url-safe base64 tokens in proxy sign-in
This commit is contained in:
Fredrik Adelöw
2026-05-25 12:00:29 +02:00
committed by GitHub
3 changed files with 37 additions and 1 deletions
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Fixed the proxy-based sign-in page failing to read the session token when the proxy issues a token whose payload is encoded using the URL-safe base64 alphabet. Such tokens are now decoded correctly so sign-in no longer breaks.
@@ -53,6 +53,27 @@ describe('ProxiedSignInIdentity', () => {
new Date(new Date(Date.now() + DEFAULTS.defaultTokenExpiryMillis)),
);
});
it('handles a token with a url-safe base64 encoded payload', async () => {
const exp = 1641216199;
const [header, _b, signature] = validBackstageToken.split('.');
// The `note` value is chosen so that the standard base64 encoding of the
// payload contains url-unsafe characters, which a real-world JWT encodes
// using the base64url alphabet ('-' and '_') without padding.
const standardBase64 = window.btoa(
JSON.stringify({ exp, note: '???>>>' }),
);
expect(standardBase64).toMatch(/[+/]/);
const urlSafePayload = standardBase64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
const token = `${header}.${urlSafePayload}.${signature}`;
expect(tokenToExpiry(token)).toEqual(
new Date(exp * 1000 - DEFAULTS.tokenExpiryMarginMillis),
);
});
});
describe('ProxiedSignInIdentity', () => {
@@ -32,6 +32,16 @@ export const DEFAULTS = {
tokenExpiryMarginMillis: 5 * 60 * 1000,
} as const;
// Decodes a base64url-encoded string. JWTs encode their segments using base64url
// (RFC 7515), which substitutes '-' and '_' for '+' and '/' and omits padding.
// `window.atob` only accepts standard base64, so translate back to that alphabet
// and restore the padding before decoding.
function decodeBase64Url(value: string): string {
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
return window.atob(padded);
}
// When the token expires, with some margin
export function tokenToExpiry(jwtToken: string | undefined): Date {
const fallback = new Date(Date.now() + DEFAULTS.defaultTokenExpiryMillis);
@@ -40,7 +50,7 @@ export function tokenToExpiry(jwtToken: string | undefined): Date {
}
const [_header, rawPayload, _signature] = jwtToken.split('.');
const payload = JSON.parse(window.atob(rawPayload));
const payload = JSON.parse(decodeBase64Url(rawPayload));
if (typeof payload.exp !== 'number') {
return fallback;
}