Merge pull request #23285 from backstage/rugvip/adapt-fix

backend-common: forward service tokens from request if no token manager is available
This commit is contained in:
Patrik Oldsberg
2024-02-27 16:36:22 +01:00
committed by GitHub
3 changed files with 77 additions and 20 deletions
@@ -39,10 +39,12 @@ 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,
@@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mockServices } from '@backstage/backend-test-utils';
import { createLegacyAuthAdapters } from './createLegacyAuthAdapters';
import { Request } from 'express';
describe('createLegacyAuthAdapters', () => {
it('should pass through auth if only auth is provided', () => {
@@ -86,4 +88,54 @@ describe('createLegacyAuthAdapters', () => {
userInfo: expect.any(Object),
});
});
it('should forward tokens if no token manager is provided', async () => {
const { auth, httpAuth } = createLegacyAuthAdapters({
auth: undefined,
httpAuth: undefined,
discovery: {} as any,
identity: mockServices.identity(),
});
const credentials = await httpAuth.credentials({
headers: {
authorization: 'Bearer my-token',
},
} as Request);
await expect(
auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'test',
}),
).resolves.toEqual({ token: 'my-token' });
});
it('should issue a new token if a token manager is provided', async () => {
const { auth, httpAuth } = createLegacyAuthAdapters({
auth: undefined,
httpAuth: undefined,
tokenManager: {
...mockServices.tokenManager(),
async getToken() {
return { token: 'new-token' };
},
},
discovery: {} as any,
identity: mockServices.identity(),
});
const credentials = await httpAuth.credentials({
headers: {
authorization: 'Bearer mock-token',
},
} as Request);
await expect(
auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'test',
}),
).resolves.toEqual({ token: 'new-token' });
});
});
@@ -27,7 +27,7 @@ import {
TokenManagerService,
UserInfoService,
} from '@backstage/backend-plugin-api';
import { ServerTokenManager, TokenManager } from '../tokens';
import { TokenManager } from '../tokens';
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
import type { Request, Response } from 'express';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
@@ -48,7 +48,7 @@ import { PluginEndpointDiscovery } from '../discovery';
class AuthCompat implements AuthService {
constructor(
private readonly identity: IdentityService,
private readonly tokenManager: TokenManagerService,
private readonly tokenManager?: TokenManagerService,
) {}
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
@@ -79,9 +79,12 @@ class AuthCompat implements AuthService {
}
async authenticate(token: string): Promise<BackstageCredentials> {
const { aud } = decodeJwt(token);
// Defensively check whether it seems token-like first, just to support
// custom TokenManager implementations that don't emit JWTs specifically.
const payload =
token.split('.').length === 3 ? decodeJwt(token) : undefined;
if (aud === 'backstage') {
if (payload?.aud === 'backstage') {
// User Backstage token
const identity = await this.identity.getIdentity({
request: {
@@ -100,9 +103,12 @@ class AuthCompat implements AuthService {
);
}
await this.tokenManager.authenticate(token);
await this.tokenManager?.authenticate(token);
return createCredentialsWithServicePrincipal('external:backstage-plugin');
return createCredentialsWithServicePrincipal(
'external:backstage-plugin',
token,
);
}
async getPluginRequestToken(options: {
@@ -114,8 +120,12 @@ class AuthCompat implements AuthService {
switch (type) {
// TODO: Check whether the principal is ourselves
case 'service':
return this.tokenManager.getToken();
case 'service': {
if (this.tokenManager) {
return this.tokenManager.getToken();
}
return { token: internalForward.token ?? '' };
}
case 'user':
if (!internalForward.token) {
throw new Error('User credentials is unexpectedly missing token');
@@ -187,17 +197,13 @@ class HttpAuthCompat implements HttpAuthService {
async #extractCredentialsFromRequest(req: Request) {
const token = getTokenFromRequest(req);
if (!token) {
return createCredentialsWithNonePrincipal();
return this.#auth.getNoneCredentials();
}
const credentials = toInternalBackstageCredentials(
await this.#auth.authenticate(token),
);
return credentials;
return this.#auth.authenticate(token);
}
async #getCredentials(req: /* */ RequestWithCredentials) {
async #getCredentials(req: RequestWithCredentials) {
return (req[credentialsSymbol] ??=
this.#extractCredentialsFromRequest(req));
}
@@ -209,9 +215,7 @@ class HttpAuthCompat implements HttpAuthService {
allowLimitedAccess?: boolean;
},
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {
const credentials = toInternalBackstageCredentials(
await this.#getCredentials(req),
);
const credentials = await this.#getCredentials(req);
const allowed = options?.allow;
if (!allowed) {
@@ -335,9 +339,8 @@ export function createLegacyAuthAdapters<
const identity =
options.identity ?? DefaultIdentityClient.create({ discovery });
const tokenManager = options.tokenManager ?? ServerTokenManager.noop();
const authImpl = new AuthCompat(identity, tokenManager);
const authImpl = new AuthCompat(identity, options.tokenManager);
const httpAuthImpl = new HttpAuthCompat(authImpl);