auth-backend: track backstage session expiration separately

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-08-18 17:14:05 +02:00
parent b2ff304ddd
commit 18619f793c
18 changed files with 193 additions and 29 deletions
+2 -1
View File
@@ -100,6 +100,7 @@ export type AuthResolverContext = {
// @public
export interface BackstageIdentityResponse extends BackstageSignInResult {
expiresInSeconds?: number;
identity: BackstageUserIdentity;
}
@@ -377,7 +378,7 @@ export interface OAuthSession {
// (undocumented)
accessToken: string;
// (undocumented)
expiresInSeconds: number;
expiresInSeconds?: number;
// (undocumented)
idToken?: string;
// (undocumented)
@@ -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,
},
};
}
+1 -1
View File
@@ -24,7 +24,7 @@ export interface OAuthSession {
tokenType: string;
idToken?: string;
scope: string;
expiresInSeconds: number;
expiresInSeconds?: number;
refreshToken?: string;
}
+5
View File
@@ -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.
*/