backend-app-api: refactor auth service implementation to use UserTokenHandler more

Co-authored-by: Camila Belo <camilaibs@gmail.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-04-02 18:01:43 +02:00
committed by Camila Belo
parent c2aa1ffe1c
commit 22c0bd2adf
2 changed files with 43 additions and 69 deletions
@@ -26,6 +26,7 @@ import {
FlattenedJWSInput,
JWSHeaderParameters,
jwtVerify,
JWTVerifyOptions,
} from 'jose';
import { GetKeyFunction } from 'jose/dist/types/types';
@@ -49,21 +50,23 @@ export class UserTokenHandler {
this.#algorithms = ['ES256']; // TODO: configurable?
}
async verifyFullUserToken(token: string): Promise<{ userEntityRef: string }> {
// Check if the keystore needs to be updated
async verifyToken(token: string) {
const verifyOpts = this.#getTokenVerificationOptions(token);
if (!verifyOpts) {
return undefined;
}
await this.refreshKeyStore(token);
if (!this.#keyStore) {
throw new AuthenticationError('No keystore exists');
}
// Verify token claims and signature
// Note: Claims must match those set by TokenFactory when issuing tokens
// Note: verify throws if verification fails
const { payload } = await jwtVerify(token, this.#keyStore, {
algorithms: this.#algorithms,
audience: 'backstage',
}).catch(e => {
console.log(`DEBUG: e=`, e);
// Verify a limited token, ensuring the necessarily claims are present and token type is correct
const { payload } = await jwtVerify(
token,
this.#keyStore,
verifyOpts,
).catch(e => {
throw new AuthenticationError('invalid token', e);
});
@@ -76,31 +79,28 @@ export class UserTokenHandler {
return { userEntityRef };
}
async verifyLimitedUserToken(
token: string,
): Promise<{ userEntityRef: string }> {
await this.refreshKeyStore(token);
if (!this.#keyStore) {
throw new AuthenticationError('No keystore exists');
#getTokenVerificationOptions(token: string): JWTVerifyOptions | undefined {
const { typ } = decodeProtectedHeader(token);
if (typ === TokenTypes.user.typParam) {
return {
algorithms: this.#algorithms,
typ: TokenTypes.user.typParam,
};
}
// Verify a limited token, ensuring the necessarily claims are present and token type is correct
const { payload } = await jwtVerify(token, this.#keyStore, {
if (typ === TokenTypes.limitedUser.typParam) {
return {
algorithms: this.#algorithms,
requiredClaims: ['iat', 'exp', 'sub'],
typ: TokenTypes.limitedUser.typParam,
};
}
return {
algorithms: this.#algorithms,
requiredClaims: ['iat', 'exp', 'sub'],
typ: TokenTypes.limitedUser.typParam,
}).catch(e => {
console.log(`DEBUG: e=`, e);
throw new AuthenticationError('invalid token', e);
});
const userEntityRef = payload.sub;
if (!userEntityRef) {
throw new AuthenticationError('No user sub found in token');
}
return { userEntityRef };
audience: 'backstage',
};
}
createLimitedUserToken(backstageToken: string) {
@@ -26,8 +26,7 @@ import {
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import { TokenTypes } from '@backstage/plugin-auth-node';
import { decodeJwt, decodeProtectedHeader } from 'jose';
import { decodeJwt } from 'jose';
import { UserTokenHandler } from './UserTokenHandler';
/** @internal */
@@ -112,49 +111,24 @@ class DefaultAuthService implements AuthService {
// allowLimitedAccess is currently ignored, since we currently always use the full user tokens
async authenticate(token: string): Promise<BackstageCredentials> {
const { typ } = decodeProtectedHeader(token);
const { sub, aud } = decodeJwt(token);
if (typ) {
switch (typ) {
case TokenTypes.user.typParam: {
const { userEntityRef } =
await this.userTokenHandler.verifyFullUserToken(token);
return createCredentialsWithUserPrincipal(
userEntityRef,
token,
this.#getJwtExpiration(token),
);
}
case TokenTypes.limitedUser.typParam: {
const { userEntityRef } =
await this.userTokenHandler.verifyLimitedUserToken(token);
return createCredentialsWithUserPrincipal(
userEntityRef,
token,
this.#getJwtExpiration(token),
);
}
default:
throw new AuthenticationError("Invalid token 'typ' claim");
}
}
// Legacy service-to-service token
if (sub === 'backstage-server' && !aud) {
await this.tokenManager.authenticate(token);
return createCredentialsWithServicePrincipal('external:backstage-plugin');
}
// User Backstage token
const { userEntityRef } = await this.userTokenHandler.verifyFullUserToken(
token,
);
return createCredentialsWithUserPrincipal(
userEntityRef,
token,
this.#getJwtExpiration(token),
);
const userResult = await this.userTokenHandler.verifyToken(token);
if (userResult) {
return createCredentialsWithUserPrincipal(
userResult.userEntityRef,
token,
this.#getJwtExpiration(token),
);
}
throw new AuthenticationError('Unknown token');
}
isPrincipal<TType extends keyof BackstagePrincipalTypes>(