fix(core-components): decode url-safe base64 tokens in proxy sign-in

The proxy-based sign-in page derived the session expiry from the JWT by
decoding its payload with `window.atob`, which only accepts the standard
base64 alphabet. JWTs are encoded using base64url (RFC 7515), so any
token whose payload contained '-' or '_' raised a decoding error and
broke sign-in. Translate the payload back to the standard alphabet and
restore its padding before decoding.

Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
This commit is contained in:
Asish Kumar
2026-05-25 03:30:23 +05:30
parent 6fa1cacfbe
commit 8add9b992d
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;
}