backend-*-api: updates auth APIs for BEP changes

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Camila Belo <camilaibs@gmail.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-02-15 14:45:08 +01:00
parent 05b7e91861
commit 413a9e7de8
11 changed files with 65 additions and 76 deletions
@@ -51,8 +51,9 @@ describe('authServiceFactory', () => {
const searchAuth = await tester.get('search');
const catalogAuth = await tester.get('catalog');
const { token: searchToken } = await searchAuth.issueServiceToken({
forward: await searchAuth.getOwnCredentials(),
const { token: searchToken } = await searchAuth.getPluginRequestToken({
onBehalfOf: await searchAuth.getOwnServiceCredentials(),
targetPluginId: 'catalog',
});
await expect(searchAuth.authenticate(searchToken)).resolves.toEqual(
@@ -81,8 +82,8 @@ describe('authServiceFactory', () => {
const catalogAuth = await tester.get('catalog');
await expect(
catalogAuth.issueServiceToken({
forward: {
catalogAuth.getPluginRequestToken({
onBehalfOf: {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
authMethod: 'token',
@@ -92,6 +93,7 @@ describe('authServiceFactory', () => {
userEntityRef: 'user:default/alice',
},
} as InternalBackstageCredentials<BackstageUserPrincipal>,
targetPluginId: 'catalog',
}),
).resolves.toEqual({ token: 'alice-token' });
});
@@ -103,8 +105,8 @@ describe('authServiceFactory', () => {
const catalogAuth = await tester.get('catalog');
const { token } = await catalogAuth.issueServiceToken({
forward: {
const { token } = await catalogAuth.getPluginRequestToken({
onBehalfOf: {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
authMethod: 'token',
@@ -114,6 +116,7 @@ describe('authServiceFactory', () => {
subject: 'external:upstream-service',
},
} as InternalBackstageCredentials<BackstageServicePrincipal>,
targetPluginId: 'catalog',
});
expect(decodeJwt(token)).toEqual(
@@ -154,16 +154,17 @@ class DefaultAuthService implements AuthService {
return true;
}
async getOwnCredentials(): Promise<
async getOwnServiceCredentials(): Promise<
BackstageCredentials<BackstageServicePrincipal>
> {
return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);
}
async issueServiceToken(options: {
forward: BackstageCredentials;
async getPluginRequestToken(options: {
onBehalfOf: BackstageCredentials;
targetPluginId: string;
}): Promise<{ token: string }> {
const internalForward = toInternalBackstageCredentials(options.forward);
const internalForward = toInternalBackstageCredentials(options.onBehalfOf);
const { type } = internalForward.principal;
switch (type) {
@@ -63,7 +63,7 @@ type RequestWithCredentials = Request & {
[credentialsSymbol]?: Promise<BackstageCredentials>;
};
class DefaultHttpAuthService implements HttpAuthService {
export class DefaultHttpAuthService implements HttpAuthService {
constructor(
private readonly auth: AuthService,
private readonly discovery: DiscoveryService,
@@ -132,14 +132,6 @@ class DefaultHttpAuthService implements HttpAuthService {
return credentials as any;
}
async requestHeaders(options: {
forward: BackstageCredentials;
}): Promise<Record<string, string>> {
return {
Authorization: `Bearer ${await this.auth.issueServiceToken(options)}`,
};
}
async issueUserCookie(res: Response): Promise<void> {
const credentials = await this.credentials(res.req, { allow: ['user'] });
+1 -1
View File
@@ -234,7 +234,7 @@ export function createDatabaseClient(
},
): knexFactory.Knex<any, any[]>;
// @public (undocumented)
// @public
export function createLegacyAuthAdapters<
TOptions extends {
auth?: AuthService;
@@ -63,42 +63,43 @@ class AuthCompat implements AuthService {
return true;
}
async getOwnCredentials(): Promise<
async getOwnServiceCredentials(): Promise<
BackstageCredentials<BackstageServicePrincipal>
> {
return createCredentialsWithServicePrincipal('external:backstage-plugin');
}
async authenticate(token: string): Promise<BackstageCredentials> {
const { sub, aud } = decodeJwt(token);
const { aud } = decodeJwt(token);
// Legacy service-to-service token
if (sub === 'backstage-server' && !aud) {
await this.tokenManager.authenticate(token);
return createCredentialsWithServicePrincipal('external:backstage-plugin');
if (aud === 'backstage') {
// User Backstage token
const identity = await this.identity.getIdentity({
request: {
headers: { authorization: `Bearer ${token}` },
},
} as IdentityApiGetIdentityRequest);
if (!identity) {
throw new AuthenticationError('Invalid user token');
}
return createCredentialsWithUserPrincipal(
identity.identity.userEntityRef,
token,
);
}
// User Backstage token
const identity = await this.identity.getIdentity({
request: {
headers: { authorization: `Bearer ${token}` },
},
} as IdentityApiGetIdentityRequest);
await this.tokenManager.authenticate(token);
if (!identity) {
throw new AuthenticationError('Invalid user token');
}
return createCredentialsWithUserPrincipal(
identity.identity.userEntityRef,
token,
);
return createCredentialsWithServicePrincipal('external:backstage-plugin');
}
async issueServiceToken(options: {
forward: BackstageCredentials;
async getPluginRequestToken(options: {
onBehalfOf: BackstageCredentials;
targetPluginId: string;
}): Promise<{ token: string }> {
const internalForward = toInternalBackstageCredentials(options.forward);
const internalForward = toInternalBackstageCredentials(options.onBehalfOf);
const { type } = internalForward.principal;
switch (type) {
@@ -199,14 +200,6 @@ class HttpAuthCompat implements HttpAuthService {
return credentials as any;
}
async requestHeaders(options: {
forward: BackstageCredentials;
}): Promise<Record<string, string>> {
return {
Authorization: `Bearer ${await this.auth.issueServiceToken(options)}`,
};
}
async issueUserCookie(_res: Response): Promise<void> {}
}
+10 -9
View File
@@ -22,16 +22,21 @@ export interface AuthService {
// (undocumented)
authenticate(token: string): Promise<BackstageCredentials>;
// (undocumented)
getOwnCredentials(): Promise<BackstageCredentials<BackstageServicePrincipal>>;
getOwnServiceCredentials(): Promise<
BackstageCredentials<BackstageServicePrincipal>
>;
// (undocumented)
getPluginRequestToken(options: {
onBehalfOf: BackstageCredentials;
targetPluginId: string;
}): Promise<{
token: string;
}>;
// (undocumented)
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
credentials: BackstageCredentials,
type: TType,
): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]>;
// (undocumented)
issueServiceToken(options: { forward: BackstageCredentials }): Promise<{
token: string;
}>;
}
// @public (undocumented)
@@ -295,10 +300,6 @@ export interface HttpAuthService {
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>>;
// (undocumented)
issueUserCookie(res: Response_2): Promise<void>;
// (undocumented)
requestHeaders(options: {
forward: BackstageCredentials;
}): Promise<Record<string, string>>;
}
// @public (undocumented)
@@ -70,10 +70,12 @@ export interface AuthService {
type: TType,
): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]>;
getOwnCredentials(): Promise<BackstageCredentials<BackstageServicePrincipal>>;
getOwnServiceCredentials(): Promise<
BackstageCredentials<BackstageServicePrincipal>
>;
// TODO: should the caller provide the target plugin ID?
issueServiceToken(options: {
forward: BackstageCredentials;
getPluginRequestToken(options: {
onBehalfOf: BackstageCredentials;
targetPluginId: string;
}): Promise<{ token: string }>;
}
@@ -27,9 +27,5 @@ export interface HttpAuthService {
},
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>>;
requestHeaders(options: {
forward: BackstageCredentials;
}): Promise<Record<string, string>>;
issueUserCookie(res: Response): Promise<void>;
}
@@ -44,7 +44,7 @@ export class MockAuthService implements AuthService {
throw new AuthenticationError('Invalid token');
}
async getOwnCredentials(): Promise<
async getOwnServiceCredentials(): Promise<
BackstageCredentials<BackstageServicePrincipal>
> {
return {
@@ -68,10 +68,11 @@ export class MockAuthService implements AuthService {
return true;
}
async issueServiceToken(options: {
forward: BackstageCredentials;
async getPluginRequestToken(options: {
onBehalfOf: BackstageCredentials;
targetPluginId: string;
}): Promise<{ token: string }> {
const principal = options.forward.principal as
const principal = options.onBehalfOf.principal as
| BackstageUserPrincipal
| BackstageServicePrincipal
| BackstageNonePrincipal;
@@ -80,7 +81,7 @@ export class MockAuthService implements AuthService {
case 'user':
return { token: 'mock-user-token' };
case 'service':
return { token: 'mock-service-token' };
return { token: `mock-service-token-for-${options.targetPluginId}` };
default:
throw new AuthenticationError(
`Refused to issue service token for credential type '${principal.type}'`,
@@ -180,9 +180,9 @@ export namespace mockServices {
});
export const mock = simpleMock(coreServices.auth, () => ({
authenticate: jest.fn(),
getOwnCredentials: jest.fn(),
getOwnServiceCredentials: jest.fn(),
isPrincipal: jest.fn() as any,
issueServiceToken: jest.fn(),
getPluginRequestToken: jest.fn(),
}));
}
@@ -217,7 +217,6 @@ export namespace mockServices {
export const mock = simpleMock(coreServices.httpAuth, () => ({
credentials: jest.fn(),
issueUserCookie: jest.fn(),
requestHeaders: jest.fn(),
}));
}
+1
View File
@@ -3287,6 +3287,7 @@ __metadata:
"@backstage/errors": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/integration-aws-node": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/types": "workspace:^"
"@google-cloud/storage": ^7.0.0
"@keyv/memcache": ^1.3.5