backend-common: forward service tokens from request if no token manager is available
Co-authored-by: Camila Belo <camilaibs@gmail.com> Co-authored-by: Fredrik Adelöw <freben@gmail.com> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user