diff --git a/.changeset/proxied-signin-base64url-token.md b/.changeset/proxied-signin-base64url-token.md new file mode 100644 index 0000000000..e5cc665509 --- /dev/null +++ b/.changeset/proxied-signin-base64url-token.md @@ -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. diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts index 7eebc13d56..9ca00aa75c 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts @@ -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', () => { diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts index bd1fc1bf39..27807046ff 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts @@ -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; }