auth-backend: track backstage session expiration separately
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -23,7 +23,37 @@ function mkToken(payload: unknown) {
|
||||
}
|
||||
|
||||
describe('prepareBackstageIdentityResponse', () => {
|
||||
afterEach(jest.resetAllMocks);
|
||||
|
||||
it('parses a complete token to determine the identity', () => {
|
||||
jest.spyOn(Date, 'now').mockReturnValue(5000);
|
||||
|
||||
const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'], exp: 1005 });
|
||||
expect(
|
||||
prepareBackstageIdentityResponse({
|
||||
token,
|
||||
}),
|
||||
).toEqual({
|
||||
token,
|
||||
expiresInSeconds: 1000,
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: 'k:ns/n',
|
||||
ownershipEntityRefs: ['k:ns/o'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject tokens without subject', () => {
|
||||
const token = mkToken({});
|
||||
expect(() =>
|
||||
prepareBackstageIdentityResponse({
|
||||
token,
|
||||
}),
|
||||
).toThrow('Identity response must return a token with subject claim');
|
||||
});
|
||||
|
||||
it('should treat expiration as optional', () => {
|
||||
const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'] });
|
||||
expect(
|
||||
prepareBackstageIdentityResponse({
|
||||
@@ -38,4 +68,15 @@ describe('prepareBackstageIdentityResponse', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject tokens with negative expiration', () => {
|
||||
jest.spyOn(Date, 'now').mockReturnValue(5000);
|
||||
|
||||
const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'], exp: 1 });
|
||||
expect(() =>
|
||||
prepareBackstageIdentityResponse({
|
||||
token,
|
||||
}),
|
||||
).toThrow('Identity response must not return an expired token');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,14 +39,28 @@ export function prepareBackstageIdentityResponse(
|
||||
throw new InputError(`Identity response must return a token`);
|
||||
}
|
||||
|
||||
const { sub, ent } = parseJwtPayload(result.token);
|
||||
const { sub, ent = [], exp: expStr } = parseJwtPayload(result.token);
|
||||
if (!sub) {
|
||||
throw new InputError(
|
||||
`Identity response must return a token with subject claim`,
|
||||
);
|
||||
}
|
||||
|
||||
const expAt = Number(expStr);
|
||||
|
||||
// Default to 1 hour if no expiration is set, in particular to make testing simpler
|
||||
const exp = expAt ? Math.round(expAt - Date.now() / 1000) : undefined;
|
||||
if (exp && exp < 0) {
|
||||
throw new InputError(`Identity response must not return an expired token`);
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
expiresInSeconds: exp,
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: sub,
|
||||
ownershipEntityRefs: ent ?? [],
|
||||
ownershipEntityRefs: ent,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface OAuthSession {
|
||||
tokenType: string;
|
||||
idToken?: string;
|
||||
scope: string;
|
||||
expiresInSeconds: number;
|
||||
expiresInSeconds?: number;
|
||||
refreshToken?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ export interface BackstageSignInResult {
|
||||
* @public
|
||||
*/
|
||||
export interface BackstageIdentityResponse extends BackstageSignInResult {
|
||||
/**
|
||||
* The number of seconds until the token expires. If not set, it can be assumed that the token does not expire.
|
||||
*/
|
||||
expiresInSeconds?: number;
|
||||
|
||||
/**
|
||||
* A plaintext description of the identity that is encapsulated within the token.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user