backend-plugin-api: add support for limited user tokens
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -111,6 +111,7 @@ class DefaultAuthService implements AuthService {
|
||||
private readonly disableDefaultAuthPolicy: boolean,
|
||||
) {}
|
||||
|
||||
// allowLimitedAccess is currently ignored, since we currently always use the full user tokens
|
||||
async authenticate(token: string): Promise<BackstageCredentials> {
|
||||
const { sub, aud } = decodeJwt(token);
|
||||
|
||||
@@ -193,6 +194,27 @@ class DefaultAuthService implements AuthService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{ token: string; expiresAt: Date }> {
|
||||
const internalCredentials = toInternalBackstageCredentials(credentials);
|
||||
|
||||
const { token } = internalCredentials;
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError(
|
||||
'User credentials is unexpectedly missing token',
|
||||
);
|
||||
}
|
||||
|
||||
const { exp } = decodeJwt(token);
|
||||
if (!exp) {
|
||||
throw new AuthenticationError('User token is missing expiration');
|
||||
}
|
||||
|
||||
return { token, expiresAt: new Date(exp * 1000) };
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -123,6 +123,27 @@ class AuthCompat implements AuthService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{ token: string; expiresAt: Date }> {
|
||||
const internalCredentials = toInternalBackstageCredentials(credentials);
|
||||
|
||||
const { token } = internalCredentials;
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError(
|
||||
'User credentials is unexpectedly missing token',
|
||||
);
|
||||
}
|
||||
|
||||
const { exp } = decodeJwt(token);
|
||||
if (!exp) {
|
||||
throw new AuthenticationError('User token is missing expiration');
|
||||
}
|
||||
|
||||
return { token, expiresAt: new Date(exp * 1000) };
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenFromRequest(req: Request) {
|
||||
|
||||
@@ -24,7 +24,19 @@ import { Response as Response_2 } from 'express';
|
||||
// @public (undocumented)
|
||||
export interface AuthService {
|
||||
// (undocumented)
|
||||
authenticate(token: string): Promise<BackstageCredentials>;
|
||||
authenticate(
|
||||
token: string,
|
||||
options?: {
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials>;
|
||||
// (undocumented)
|
||||
getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}>;
|
||||
// (undocumented)
|
||||
getOwnServiceCredentials(): Promise<
|
||||
BackstageCredentials<BackstageServicePrincipal>
|
||||
|
||||
@@ -63,7 +63,12 @@ export type BackstagePrincipalTypes = {
|
||||
* @public
|
||||
*/
|
||||
export interface AuthService {
|
||||
authenticate(token: string): Promise<BackstageCredentials>;
|
||||
authenticate(
|
||||
token: string,
|
||||
options?: {
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials>;
|
||||
|
||||
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
|
||||
credentials: BackstageCredentials,
|
||||
@@ -78,4 +83,8 @@ export interface AuthService {
|
||||
onBehalfOf: BackstageCredentials;
|
||||
targetPluginId: string;
|
||||
}): Promise<{ token: string }>;
|
||||
|
||||
getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{ token: string; expiresAt: Date }>;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,17 @@ export function isDockerDisabledForTests(): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export namespace mockCredentials {
|
||||
export function limitedUser(
|
||||
userEntityRef?: string,
|
||||
): BackstageCredentials<BackstageUserPrincipal>;
|
||||
export namespace limitedUser {
|
||||
export function header(userEntityRef?: string): string;
|
||||
// (undocumented)
|
||||
export function invalidHeader(): string;
|
||||
// (undocumented)
|
||||
export function invalidToken(): string;
|
||||
export function token(userEntityRef?: string): string;
|
||||
}
|
||||
export function none(): BackstageCredentials<BackstageNonePrincipal>;
|
||||
export namespace none {
|
||||
export function header(): string;
|
||||
|
||||
@@ -53,6 +53,12 @@ describe('MockAuthService', () => {
|
||||
auth.authenticate(mockCredentials.user.token()),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.user.token(), {
|
||||
allowLimitedAccess: true,
|
||||
}),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.user.token()),
|
||||
).resolves.toEqual(mockCredentials.user(DEFAULT_MOCK_USER_ENTITY_REF));
|
||||
@@ -66,6 +72,44 @@ describe('MockAuthService', () => {
|
||||
).rejects.toThrow('User token is invalid');
|
||||
});
|
||||
|
||||
it('should authenticate mock limited user tokens', async () => {
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token()),
|
||||
).rejects.toThrow('Limited user token is not allowed');
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token(), {}),
|
||||
).rejects.toThrow('Limited user token is not allowed');
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token(), {
|
||||
allowLimitedAccess: false,
|
||||
}),
|
||||
).rejects.toThrow('Limited user token is not allowed');
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token(), {
|
||||
allowLimitedAccess: true,
|
||||
}),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token(), {
|
||||
allowLimitedAccess: true,
|
||||
}),
|
||||
).resolves.toEqual(mockCredentials.user(DEFAULT_MOCK_USER_ENTITY_REF));
|
||||
|
||||
await expect(
|
||||
auth.authenticate(
|
||||
mockCredentials.limitedUser.token('user:default/other'),
|
||||
{
|
||||
allowLimitedAccess: true,
|
||||
},
|
||||
),
|
||||
).resolves.toEqual(mockCredentials.user('user:default/other'));
|
||||
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.invalidToken()),
|
||||
).rejects.toThrow('Limited user token is invalid');
|
||||
});
|
||||
|
||||
it('should authenticate mock service tokens', async () => {
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.service.token()),
|
||||
|
||||
@@ -27,9 +27,12 @@ import {
|
||||
mockCredentials,
|
||||
MOCK_USER_TOKEN,
|
||||
MOCK_USER_TOKEN_PREFIX,
|
||||
MOCK_INVALID_USER_TOKEN,
|
||||
MOCK_USER_LIMITED_TOKEN,
|
||||
MOCK_USER_LIMITED_TOKEN_PREFIX,
|
||||
MOCK_INVALID_USER_LIMITED_TOKEN,
|
||||
MOCK_SERVICE_TOKEN,
|
||||
MOCK_SERVICE_TOKEN_PREFIX,
|
||||
MOCK_INVALID_USER_TOKEN,
|
||||
MOCK_INVALID_SERVICE_TOKEN,
|
||||
UserTokenPayload,
|
||||
ServiceTokenPayload,
|
||||
@@ -48,14 +51,24 @@ export class MockAuthService implements AuthService {
|
||||
this.disableDefaultAuthPolicy = options.disableDefaultAuthPolicy;
|
||||
}
|
||||
|
||||
async authenticate(token: string): Promise<BackstageCredentials> {
|
||||
async authenticate(
|
||||
token: string,
|
||||
options?: { allowLimitedAccess?: boolean },
|
||||
): Promise<BackstageCredentials> {
|
||||
switch (token) {
|
||||
case MOCK_USER_TOKEN:
|
||||
return mockCredentials.user();
|
||||
case MOCK_USER_LIMITED_TOKEN:
|
||||
if (!options?.allowLimitedAccess) {
|
||||
throw new AuthenticationError('Limited user token is not allowed');
|
||||
}
|
||||
return mockCredentials.user();
|
||||
case MOCK_SERVICE_TOKEN:
|
||||
return mockCredentials.service();
|
||||
case MOCK_INVALID_USER_TOKEN:
|
||||
throw new AuthenticationError('User token is invalid');
|
||||
case MOCK_INVALID_USER_LIMITED_TOKEN:
|
||||
throw new AuthenticationError('Limited user token is invalid');
|
||||
case MOCK_INVALID_SERVICE_TOKEN:
|
||||
throw new AuthenticationError('Service token is invalid');
|
||||
case '':
|
||||
@@ -72,6 +85,18 @@ export class MockAuthService implements AuthService {
|
||||
return mockCredentials.user(userEntityRef);
|
||||
}
|
||||
|
||||
if (token.startsWith(MOCK_USER_LIMITED_TOKEN_PREFIX)) {
|
||||
if (!options?.allowLimitedAccess) {
|
||||
throw new AuthenticationError('Limited user token is not allowed');
|
||||
}
|
||||
|
||||
const { sub: userEntityRef }: UserTokenPayload = JSON.parse(
|
||||
token.slice(MOCK_USER_LIMITED_TOKEN_PREFIX.length),
|
||||
);
|
||||
|
||||
return mockCredentials.user(userEntityRef);
|
||||
}
|
||||
|
||||
if (token.startsWith(MOCK_SERVICE_TOKEN_PREFIX)) {
|
||||
const { sub, target, obo }: ServiceTokenPayload = JSON.parse(
|
||||
token.slice(MOCK_SERVICE_TOKEN_PREFIX.length),
|
||||
@@ -144,4 +169,21 @@ export class MockAuthService implements AuthService {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{ token: string; expiresAt: Date }> {
|
||||
if (credentials.principal.type !== 'user') {
|
||||
throw new AuthenticationError(
|
||||
`Refused to issue limited user token for credential type '${credentials.principal.type}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
token: mockCredentials.limitedUser.token(
|
||||
credentials.principal.userEntityRef,
|
||||
),
|
||||
expiresAt: new Date(Date.now() + 3600),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@ describe('mockCredentials', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a mocked credentials object for a limited user principal', () => {
|
||||
expect(mockCredentials.limitedUser()).toEqual({
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
principal: { type: 'user', userEntityRef: 'user:default/mock' },
|
||||
});
|
||||
|
||||
expect(mockCredentials.limitedUser('user:default/other')).toEqual({
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
principal: { type: 'user', userEntityRef: 'user:default/other' },
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a mocked credentials object for a service principal', () => {
|
||||
expect(mockCredentials.service()).toEqual({
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
@@ -68,6 +80,26 @@ describe('mockCredentials', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('creates limited user tokens and headers', () => {
|
||||
expect(mockCredentials.limitedUser.token()).toBe('mock-limited-user-token');
|
||||
expect(mockCredentials.limitedUser.token('user:default/other')).toBe(
|
||||
'mock-limited-user-token:{"sub":"user:default/other"}',
|
||||
);
|
||||
expect(mockCredentials.limitedUser.invalidToken()).toBe(
|
||||
'mock-invalid-limited-user-token',
|
||||
);
|
||||
|
||||
expect(mockCredentials.limitedUser.header()).toBe(
|
||||
'Bearer mock-limited-user-token',
|
||||
);
|
||||
expect(mockCredentials.limitedUser.header('user:default/other')).toBe(
|
||||
'Bearer mock-limited-user-token:{"sub":"user:default/other"}',
|
||||
);
|
||||
expect(mockCredentials.limitedUser.invalidHeader()).toBe(
|
||||
'Bearer mock-invalid-limited-user-token',
|
||||
);
|
||||
});
|
||||
|
||||
it('creates service tokens and headers', () => {
|
||||
expect(mockCredentials.service.token()).toBe('mock-service-token');
|
||||
expect(
|
||||
|
||||
@@ -25,9 +25,16 @@ export const DEFAULT_MOCK_USER_ENTITY_REF = 'user:default/mock';
|
||||
export const DEFAULT_MOCK_SERVICE_SUBJECT = 'external:test-service';
|
||||
|
||||
export const MOCK_NONE_TOKEN = 'mock-none-token';
|
||||
|
||||
export const MOCK_USER_TOKEN = 'mock-user-token';
|
||||
export const MOCK_USER_TOKEN_PREFIX = 'mock-user-token:';
|
||||
export const MOCK_INVALID_USER_TOKEN = 'mock-invalid-user-token';
|
||||
|
||||
export const MOCK_USER_LIMITED_TOKEN = 'mock-limited-user-token';
|
||||
export const MOCK_USER_LIMITED_TOKEN_PREFIX = 'mock-limited-user-token:';
|
||||
export const MOCK_INVALID_USER_LIMITED_TOKEN =
|
||||
'mock-invalid-limited-user-token';
|
||||
|
||||
export const MOCK_SERVICE_TOKEN = 'mock-service-token';
|
||||
export const MOCK_SERVICE_TOKEN_PREFIX = 'mock-service-token:';
|
||||
export const MOCK_INVALID_SERVICE_TOKEN = 'mock-invalid-service-token';
|
||||
@@ -143,6 +150,55 @@ export namespace mockCredentials {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mocked credentials object for a user principal with limited
|
||||
* access.
|
||||
*
|
||||
* The default user entity reference is 'user:default/mock'.
|
||||
*/
|
||||
export function limitedUser(
|
||||
userEntityRef: string = DEFAULT_MOCK_USER_ENTITY_REF,
|
||||
): BackstageCredentials<BackstageUserPrincipal> {
|
||||
return user(userEntityRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities related to limited user credentials.
|
||||
*/
|
||||
export namespace limitedUser {
|
||||
/**
|
||||
* Creates a mocked limited user token. If a payload is provided it will be
|
||||
* encoded into the token and forwarded to the credentials object when
|
||||
* authenticated by the mock auth service.
|
||||
*/
|
||||
export function token(userEntityRef?: string): string {
|
||||
if (userEntityRef) {
|
||||
validateUserEntityRef(userEntityRef);
|
||||
return `${MOCK_USER_LIMITED_TOKEN_PREFIX}${JSON.stringify({
|
||||
sub: userEntityRef,
|
||||
} satisfies UserTokenPayload)}`;
|
||||
}
|
||||
return MOCK_USER_LIMITED_TOKEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an authorization header with a mocked limited user token. If a
|
||||
* payload is provided it will be encoded into the token and forwarded to
|
||||
* the credentials object when authenticated by the mock auth service.
|
||||
*/
|
||||
export function header(userEntityRef?: string): string {
|
||||
return `Bearer ${token(userEntityRef)}`;
|
||||
}
|
||||
|
||||
export function invalidToken(): string {
|
||||
return MOCK_INVALID_USER_LIMITED_TOKEN;
|
||||
}
|
||||
|
||||
export function invalidHeader(): string {
|
||||
return `Bearer ${invalidToken()}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mocked credentials object for a service principal.
|
||||
*
|
||||
|
||||
@@ -205,6 +205,7 @@ export namespace mockServices {
|
||||
getOwnServiceCredentials: jest.fn(),
|
||||
isPrincipal: jest.fn() as any,
|
||||
getPluginRequestToken: jest.fn(),
|
||||
getLimitedUserToken: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user