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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user