chore: move implementation to new plugin
Signed-off-by: djamaile <rdjamaile@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
`oauth2-proxy` auth implementation has been moved to `@backstage/plugin-auth-backend-module-oauth2-proxy-provider`
|
||||
@@ -15,18 +15,21 @@
|
||||
*/
|
||||
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { OAuth2ProxyResult } from '@backstage/plugin-auth-backend';
|
||||
import {
|
||||
createProxyAuthenticator,
|
||||
getBearerTokenFromAuthorizationHeader,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { decodeJwt } from 'jose';
|
||||
import { OAuth2ProxyResult } from './types';
|
||||
|
||||
// NOTE: This may come in handy if you're doing work on this provider:
|
||||
// plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml
|
||||
export const OAUTH2_PROXY_JWT_HEADER = 'X-OAUTH2-PROXY-ID-TOKEN';
|
||||
|
||||
export const oauth2ProxyAuthenticator = createProxyAuthenticator({
|
||||
export const oauth2ProxyAuthenticator = createProxyAuthenticator<
|
||||
unknown,
|
||||
OAuth2ProxyResult
|
||||
>({
|
||||
defaultProfileTransform: async (result: OAuth2ProxyResult) => {
|
||||
return {
|
||||
profile: {
|
||||
|
||||
@@ -20,3 +20,8 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export { authModuleOauth2ProxyProvider } from './module';
|
||||
export {
|
||||
oauth2ProxyAuthenticator,
|
||||
OAUTH2_PROXY_JWT_HEADER,
|
||||
} from './authenticator';
|
||||
export type { OAuth2ProxyResult } from './types';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
/**
|
||||
* JWT header extraction result, containing the raw value and the parsed JWT
|
||||
* payload.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OAuth2ProxyResult<JWTPayload = {}> = {
|
||||
/**
|
||||
* The parsed payload of the `accessToken`. The token is only parsed, not verified.
|
||||
*
|
||||
* @deprecated Access through the `headers` instead. This will be removed in a future release.
|
||||
*/
|
||||
fullProfile: JWTPayload;
|
||||
|
||||
/**
|
||||
* The token received via the X-OAUTH2-PROXY-ID-TOKEN header. Will be an empty string
|
||||
* if the header is not set. Note the this is typically an OpenID Connect token.
|
||||
*
|
||||
* @deprecated Access through the `headers` instead. This will be removed in a future release.
|
||||
*/
|
||||
accessToken: string;
|
||||
|
||||
/**
|
||||
* The headers of the incoming request from the OAuth2 proxy. This will include
|
||||
* both the headers set by the client as well as the ones added by the OAuth2 proxy.
|
||||
* You should only trust the headers that are injected by the OAuth2 proxy.
|
||||
*
|
||||
* Useful headers to use to complete the sign-in are for example `x-forwarded-user`
|
||||
* and `x-forwarded-email`. See the OAuth2 proxy documentation for more information
|
||||
* about the available headers and how to enable them. In particular it is possible
|
||||
* to forward access and identity tokens, which can be user for additional verification
|
||||
* and lookups.
|
||||
*/
|
||||
headers: IncomingHttpHeaders;
|
||||
|
||||
/**
|
||||
* Provides convenient access to the request headers.
|
||||
*
|
||||
* This call is simply forwarded to `req.get(name)`.
|
||||
*/
|
||||
getHeader(name: string): string | undefined;
|
||||
};
|
||||
@@ -45,6 +45,7 @@
|
||||
"@backstage/plugin-auth-backend-module-google-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-okta-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
|
||||
@@ -15,4 +15,10 @@
|
||||
*/
|
||||
|
||||
export { oauth2Proxy } from './provider';
|
||||
export type { OAuth2ProxyResult } from './provider';
|
||||
import { OAuth2ProxyResult as _OAuth2ProxyResult } from '@backstage/plugin-auth-backend-module-oauth2-proxy-provider';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated import from `@backstage/plugin-auth-backend-module-oauth2-proxy-provider` instead
|
||||
*/
|
||||
export type OAuth2ProxyResult = _OAuth2ProxyResult;
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock('jose', () => ({
|
||||
decodeJwt: jest.fn(),
|
||||
}));
|
||||
jest.mock('@backstage/catalog-client');
|
||||
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import express from 'express';
|
||||
import * as jose from 'jose';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { AuthHandler, AuthResolverContext, SignInResolver } from '../types';
|
||||
import {
|
||||
oauth2Proxy,
|
||||
Oauth2ProxyAuthProvider,
|
||||
OAuth2ProxyResult,
|
||||
OAUTH2_PROXY_JWT_HEADER,
|
||||
} from './provider';
|
||||
|
||||
describe('Oauth2ProxyAuthProvider', () => {
|
||||
const mockToken =
|
||||
'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob';
|
||||
|
||||
let provider: Oauth2ProxyAuthProvider<any>;
|
||||
let logger: jest.Mocked<LoggerService>;
|
||||
let signInResolver: jest.MockedFunction<
|
||||
SignInResolver<OAuth2ProxyResult<any>>
|
||||
>;
|
||||
let authHandler: jest.MockedFunction<AuthHandler<OAuth2ProxyResult<any>>>;
|
||||
let mockResponse: jest.Mocked<express.Response>;
|
||||
let mockRequest: jest.Mocked<express.Request>;
|
||||
let mockJwtDecode: jest.MockedFunction<typeof jose.decodeJwt>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
mockJwtDecode = jose.decodeJwt as jest.MockedFunction<
|
||||
typeof jose.decodeJwt
|
||||
>;
|
||||
authHandler = jest.fn();
|
||||
signInResolver = jest.fn();
|
||||
logger = { error: jest.fn() } as unknown as jest.Mocked<LoggerService>;
|
||||
|
||||
mockResponse = {
|
||||
status: jest.fn(),
|
||||
end: jest.fn(),
|
||||
json: jest.fn(),
|
||||
} as unknown as jest.Mocked<express.Response>;
|
||||
|
||||
mockRequest = {
|
||||
body: {},
|
||||
header: jest.fn(),
|
||||
headers: {
|
||||
'x-mock': 'mock',
|
||||
},
|
||||
} as unknown as jest.Mocked<express.Request>;
|
||||
|
||||
provider = new Oauth2ProxyAuthProvider<any>({
|
||||
authHandler,
|
||||
signInResolver,
|
||||
resolverContext: {
|
||||
_: 'resolver-context',
|
||||
} as unknown as AuthResolverContext,
|
||||
});
|
||||
});
|
||||
|
||||
describe('frameHandler()', () => {
|
||||
it('should do nothing and return undefined', async () => {
|
||||
const result = await provider.frameHandler();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('start()', () => {
|
||||
it('should do nothing and return undefined', async () => {
|
||||
const result = await provider.start();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh()', () => {
|
||||
it('should throw an error when auth header is missing', async () => {
|
||||
mockRequest.header.mockReturnValue(undefined);
|
||||
|
||||
await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow(
|
||||
AuthenticationError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the bearer token is invalid', async () => {
|
||||
mockRequest.header.mockReturnValue('Basic asdf=');
|
||||
|
||||
await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow(
|
||||
AuthenticationError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return if auth header is set and valid', async () => {
|
||||
mockRequest.header.mockReturnValue(`Bearer token`);
|
||||
authHandler.mockResolvedValue({
|
||||
profile: {},
|
||||
});
|
||||
signInResolver.mockResolvedValue({
|
||||
token: mockToken,
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(mockRequest.header).toHaveBeenCalledWith(OAUTH2_PROXY_JWT_HEADER);
|
||||
expect(mockJwtDecode).toHaveBeenCalledWith('token');
|
||||
expect(mockResponse.json).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should load profile from authHandler and backstage identity from signInResolver', async () => {
|
||||
const decodedToken = {
|
||||
oid: 'oid',
|
||||
name: 'name',
|
||||
upn: 'john.doe@example.com',
|
||||
};
|
||||
const profile = { displayName: 'some value' };
|
||||
mockRequest.header.mockReturnValue(`Bearer token`);
|
||||
signInResolver.mockResolvedValue({
|
||||
token: mockToken,
|
||||
});
|
||||
authHandler.mockResolvedValue({ profile: profile });
|
||||
mockJwtDecode.mockReturnValue(decodedToken as any);
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(signInResolver).toHaveBeenCalledWith(
|
||||
{
|
||||
profile: profile,
|
||||
result: {
|
||||
accessToken: 'token',
|
||||
fullProfile: decodedToken,
|
||||
getHeader: expect.any(Function),
|
||||
headers: {
|
||||
'x-mock': 'mock',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ _: 'resolver-context' },
|
||||
);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
backstageIdentity: {
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/jimmymarkum',
|
||||
ownershipEntityRefs: ['user:default/jimmymarkum'],
|
||||
},
|
||||
token: mockToken,
|
||||
},
|
||||
profile: { displayName: 'some value' },
|
||||
providerInfo: {
|
||||
accessToken: 'token',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('oauth2Proxy.create()', () => {
|
||||
beforeEach(() => {
|
||||
mockRequest.header.mockReturnValue(`Bearer token`);
|
||||
authHandler.mockResolvedValue({
|
||||
profile: {},
|
||||
});
|
||||
signInResolver.mockResolvedValue({
|
||||
token: mockToken,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a valid provider', async () => {
|
||||
const factory = oauth2Proxy.create({
|
||||
authHandler,
|
||||
signIn: { resolver: signInResolver },
|
||||
});
|
||||
const handler = factory({
|
||||
logger,
|
||||
catalogApi: {},
|
||||
tokenIssuer: {},
|
||||
} as any);
|
||||
await handler.refresh!(mockRequest, mockResponse);
|
||||
|
||||
expect(mockRequest.header).toHaveBeenCalledWith(OAUTH2_PROXY_JWT_HEADER);
|
||||
expect(mockJwtDecode).toHaveBeenCalledWith('token');
|
||||
expect(mockResponse.json).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,164 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
AuthHandler,
|
||||
SignInResolver,
|
||||
AuthProviderRouteHandlers,
|
||||
AuthResponse,
|
||||
AuthResolverContext,
|
||||
AuthHandlerResult,
|
||||
} from '../types';
|
||||
import { decodeJwt } from 'jose';
|
||||
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
|
||||
import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node';
|
||||
import { AuthHandler, SignInResolver } from '../types';
|
||||
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
// NOTE: This may come in handy if you're doing work on this provider:
|
||||
//
|
||||
// plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml
|
||||
//
|
||||
|
||||
export const OAUTH2_PROXY_JWT_HEADER = 'X-OAUTH2-PROXY-ID-TOKEN';
|
||||
|
||||
/**
|
||||
* JWT header extraction result, containing the raw value and the parsed JWT
|
||||
* payload.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OAuth2ProxyResult<JWTPayload = {}> = {
|
||||
/**
|
||||
* The parsed payload of the `accessToken`. The token is only parsed, not verified.
|
||||
*
|
||||
* @deprecated Access through the `headers` instead. This will be removed in a future release.
|
||||
*/
|
||||
fullProfile: JWTPayload;
|
||||
|
||||
/**
|
||||
* The token received via the X-OAUTH2-PROXY-ID-TOKEN header. Will be an empty string
|
||||
* if the header is not set. Note the this is typically an OpenID Connect token.
|
||||
*
|
||||
* @deprecated Access through the `headers` instead. This will be removed in a future release.
|
||||
*/
|
||||
accessToken: string;
|
||||
|
||||
/**
|
||||
* The headers of the incoming request from the OAuth2 proxy. This will include
|
||||
* both the headers set by the client as well as the ones added by the OAuth2 proxy.
|
||||
* You should only trust the headers that are injected by the OAuth2 proxy.
|
||||
*
|
||||
* Useful headers to use to complete the sign-in are for example `x-forwarded-user`
|
||||
* and `x-forwarded-email`. See the OAuth2 proxy documentation for more information
|
||||
* about the available headers and how to enable them. In particular it is possible
|
||||
* to forward access and identity tokens, which can be user for additional verification
|
||||
* and lookups.
|
||||
*/
|
||||
headers: IncomingHttpHeaders;
|
||||
|
||||
/**
|
||||
* Provides convenient access to the request headers.
|
||||
*
|
||||
* This call is simply forwarded to `req.get(name)`.
|
||||
*/
|
||||
getHeader(name: string): string | undefined;
|
||||
};
|
||||
|
||||
interface Options<JWTPayload> {
|
||||
resolverContext: AuthResolverContext;
|
||||
signInResolver: SignInResolver<OAuth2ProxyResult<JWTPayload>>;
|
||||
authHandler: AuthHandler<OAuth2ProxyResult<JWTPayload>>;
|
||||
}
|
||||
|
||||
export class Oauth2ProxyAuthProvider<JWTPayload>
|
||||
implements AuthProviderRouteHandlers
|
||||
{
|
||||
private readonly resolverContext: AuthResolverContext;
|
||||
private readonly signInResolver: SignInResolver<
|
||||
OAuth2ProxyResult<JWTPayload>
|
||||
>;
|
||||
private readonly authHandler: AuthHandler<OAuth2ProxyResult<JWTPayload>>;
|
||||
|
||||
constructor(options: Options<JWTPayload>) {
|
||||
this.resolverContext = options.resolverContext;
|
||||
this.signInResolver = options.signInResolver;
|
||||
this.authHandler = options.authHandler;
|
||||
}
|
||||
|
||||
frameHandler(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async refresh(req: express.Request, res: express.Response): Promise<void> {
|
||||
try {
|
||||
// TODO(Rugvip): This parsing was deprecated in 1.2 and should be removed in a future release.
|
||||
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
|
||||
const jwt = getBearerTokenFromAuthorizationHeader(authHeader);
|
||||
const decodedJWT = jwt && (decodeJwt(jwt) as unknown as JWTPayload);
|
||||
|
||||
const result = {
|
||||
fullProfile: decodedJWT || ({} as JWTPayload),
|
||||
accessToken: jwt || '',
|
||||
headers: req.headers,
|
||||
getHeader(name: string) {
|
||||
if (name.toLocaleLowerCase('en-US') === 'set-cookie') {
|
||||
throw new Error('Access Set-Cookie via the headers object instead');
|
||||
}
|
||||
return req.get(name);
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.handleResult(result);
|
||||
res.json(response);
|
||||
} catch (e) {
|
||||
throw new AuthenticationError('Refresh failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
start(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private async handleResult(
|
||||
result: OAuth2ProxyResult<JWTPayload>,
|
||||
): Promise<AuthResponse<{ accessToken: string }>> {
|
||||
const { profile } = await this.authHandler(result, this.resolverContext);
|
||||
|
||||
const backstageSignInResult = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
},
|
||||
this.resolverContext,
|
||||
);
|
||||
|
||||
return {
|
||||
providerInfo: {
|
||||
accessToken: result.accessToken,
|
||||
},
|
||||
backstageIdentity: prepareBackstageIdentityResponse(
|
||||
backstageSignInResult,
|
||||
),
|
||||
profile,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function defaultAuthHandler(
|
||||
result: OAuth2ProxyResult<unknown>,
|
||||
): Promise<AuthHandlerResult> {
|
||||
return {
|
||||
profile: {
|
||||
email: result.getHeader('x-forwarded-email'),
|
||||
displayName:
|
||||
result.getHeader('x-forwarded-preferred-username') ||
|
||||
result.getHeader('x-forwarded-user'),
|
||||
},
|
||||
};
|
||||
}
|
||||
import {
|
||||
type OAuth2ProxyResult,
|
||||
oauth2ProxyAuthenticator,
|
||||
} from '@backstage/plugin-auth-backend-module-oauth2-proxy-provider';
|
||||
|
||||
/**
|
||||
* Auth provider integration for oauth2-proxy auth
|
||||
@@ -179,7 +28,7 @@ async function defaultAuthHandler(
|
||||
* @public
|
||||
*/
|
||||
export const oauth2Proxy = createAuthProviderIntegration({
|
||||
create<JWTPayload>(options: {
|
||||
create(options: {
|
||||
/**
|
||||
* Configure an auth handler to generate a profile for the user.
|
||||
*
|
||||
@@ -187,7 +36,7 @@ export const oauth2Proxy = createAuthProviderIntegration({
|
||||
* header as the display name, falling back to `X-Forwarded-User`, and the value of
|
||||
* the `X-Forwarded-Email` header as the email address.
|
||||
*/
|
||||
authHandler?: AuthHandler<OAuth2ProxyResult<JWTPayload>>;
|
||||
authHandler?: AuthHandler<OAuth2ProxyResult>;
|
||||
|
||||
/**
|
||||
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
|
||||
@@ -196,17 +45,13 @@ export const oauth2Proxy = createAuthProviderIntegration({
|
||||
/**
|
||||
* Maps an auth result to a Backstage identity for the user.
|
||||
*/
|
||||
resolver: SignInResolver<OAuth2ProxyResult<JWTPayload>>;
|
||||
resolver: SignInResolver<OAuth2ProxyResult>;
|
||||
};
|
||||
}) {
|
||||
return ({ resolverContext }) => {
|
||||
const signInResolver = options.signIn.resolver;
|
||||
const authHandler = options.authHandler;
|
||||
return new Oauth2ProxyAuthProvider<JWTPayload>({
|
||||
resolverContext,
|
||||
signInResolver,
|
||||
authHandler: authHandler ?? defaultAuthHandler,
|
||||
});
|
||||
};
|
||||
return createProxyAuthProviderFactory({
|
||||
authenticator: oauth2ProxyAuthenticator,
|
||||
profileTransform: options?.authHandler,
|
||||
signInResolver: options?.signIn?.resolver,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user