backend-plugin-api: auth refactor to issueServiceToken with required credentials + getOwnCredentials

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-02-10 12:04:11 +01:00
parent 9ae4b9b6c2
commit c599fe1f7b
6 changed files with 39 additions and 34 deletions
@@ -51,7 +51,9 @@ describe('authServiceFactory', () => {
const searchAuth = await tester.get('search');
const catalogAuth = await tester.get('catalog');
const { token: searchToken } = await searchAuth.issueToken({});
const { token: searchToken } = await searchAuth.issueServiceToken({
forward: await searchAuth.getOwnCredentials(),
});
await expect(searchAuth.authenticate(searchToken)).resolves.toEqual(
expect.objectContaining({
@@ -79,7 +81,7 @@ describe('authServiceFactory', () => {
const catalogAuth = await tester.get('catalog');
await expect(
catalogAuth.issueToken({
catalogAuth.issueServiceToken({
forward: {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
@@ -101,7 +103,7 @@ describe('authServiceFactory', () => {
const catalogAuth = await tester.get('catalog');
const { token } = await catalogAuth.issueToken({
const { token } = await catalogAuth.issueServiceToken({
forward: {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
@@ -43,12 +43,10 @@ export type InternalBackstageCredentials<TPrincipal = unknown> =
export function createCredentialsWithServicePrincipal(
sub: string,
token: string,
): InternalBackstageCredentials<BackstageServicePrincipal> {
return {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
token,
principal: {
type: 'service',
subject: sub,
@@ -112,6 +110,7 @@ class DefaultAuthService implements AuthService {
constructor(
private readonly tokenManager: TokenManager,
private readonly identity: IdentityService,
private readonly pluginId: string,
) {}
async authenticate(token: string): Promise<BackstageCredentials> {
@@ -120,10 +119,7 @@ class DefaultAuthService implements AuthService {
// Legacy service-to-service token
if (sub === 'backstage-server' && !aud) {
await this.tokenManager.authenticate(token);
return createCredentialsWithServicePrincipal(
'external:backstage-plugin',
token,
);
return createCredentialsWithServicePrincipal('external:backstage-plugin');
}
// User Backstage token
@@ -158,28 +154,31 @@ class DefaultAuthService implements AuthService {
return true;
}
async issueToken(options?: {
forward?: BackstageCredentials;
}): Promise<{ token: string }> {
const internalForward =
options?.forward && toInternalBackstageCredentials(options.forward);
async getOwnCredentials(): Promise<
BackstageCredentials<BackstageServicePrincipal>
> {
return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);
}
if (internalForward) {
const { type } = internalForward.principal;
if (type === 'user') {
async issueServiceToken(options: {
forward: BackstageCredentials;
}): Promise<{ token: string }> {
const internalForward = toInternalBackstageCredentials(options.forward);
const { type } = internalForward.principal;
switch (type) {
case 'service':
return this.tokenManager.getToken();
case 'user':
if (!internalForward.token) {
throw new Error('User credentials is unexpectedly missing token');
}
return { token: internalForward.token };
} else if (type !== 'service') {
default:
throw new AuthenticationError(
`Refused to issue service token for credential type '${type}'`,
);
}
}
const { token } = await this.tokenManager.getToken();
return { token };
}
}
@@ -190,12 +189,13 @@ export const authServiceFactory = createServiceFactory({
config: coreServices.rootConfig,
logger: coreServices.rootLogger,
discovery: coreServices.discovery,
plugin: coreServices.pluginMetadata,
},
createRootContext({ config, logger }) {
return ServerTokenManager.fromConfig(config, { logger });
},
async factory({ discovery }, tokenManager) {
async factory({ discovery, plugin }, tokenManager) {
const identity = DefaultIdentityClient.create({ discovery });
return new DefaultAuthService(tokenManager, identity);
return new DefaultAuthService(tokenManager, identity, plugin.getId());
},
});
@@ -133,10 +133,10 @@ class DefaultHttpAuthService implements HttpAuthService {
}
async requestHeaders(options: {
forward?: BackstageCredentials;
forward: BackstageCredentials;
}): Promise<Record<string, string>> {
return {
Authorization: `Bearer ${await this.auth.issueToken(options)}`,
Authorization: `Bearer ${await this.auth.issueServiceToken(options)}`,
};
}
+5 -3
View File
@@ -22,12 +22,14 @@ export interface AuthService {
// (undocumented)
authenticate(token: string): Promise<BackstageCredentials>;
// (undocumented)
getOwnCredentials(): Promise<BackstageCredentials<BackstageServicePrincipal>>;
// (undocumented)
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
credentials: BackstageCredentials,
type: TType,
): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]>;
// (undocumented)
issueToken(options: { forward?: BackstageCredentials }): Promise<{
issueServiceToken(options: { forward: BackstageCredentials }): Promise<{
token: string;
}>;
}
@@ -305,8 +307,8 @@ export interface HttpAuthService {
// (undocumented)
issueUserCookie(res: Response_2): Promise<void>;
// (undocumented)
requestHeaders(options?: {
forward?: BackstageCredentials;
requestHeaders(options: {
forward: BackstageCredentials;
}): Promise<Record<string, string>>;
}
@@ -71,9 +71,10 @@ export interface AuthService {
type: TType,
): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]>;
getOwnCredentials(): Promise<BackstageCredentials<BackstageServicePrincipal>>;
// TODO: should the caller provide the target plugin ID?
// TODO: how can we make it very difficult to forget to forward credentials
issueToken(options: {
forward?: BackstageCredentials;
issueServiceToken(options: {
forward: BackstageCredentials;
}): Promise<{ token: string }>;
}
@@ -43,8 +43,8 @@ export interface HttpAuthService {
BackstageCredentials<BackstageHttpAccessToPrincipalTypesMapping[TAllowed]>
>;
requestHeaders(options?: {
forward?: BackstageCredentials;
requestHeaders(options: {
forward: BackstageCredentials;
}): Promise<Record<string, string>>;
issueUserCookie(res: Response): Promise<void>;