diff --git a/.changeset/blue-meals-chew.md b/.changeset/blue-meals-chew.md
new file mode 100644
index 0000000000..479e77a982
--- /dev/null
+++ b/.changeset/blue-meals-chew.md
@@ -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`
diff --git a/.changeset/cool-knives-argue.md b/.changeset/cool-knives-argue.md
new file mode 100644
index 0000000000..74890a0a9b
--- /dev/null
+++ b/.changeset/cool-knives-argue.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-auth-backend-module-oauth2-proxy-provider': minor
+---
+
+Release of `oauth2-proxy-provider` plugin
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/.eslintrc.js b/plugins/auth-backend-module-oauth2-proxy-provider/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/README.md b/plugins/auth-backend-module-oauth2-proxy-provider/README.md
new file mode 100644
index 0000000000..edec8fb3c2
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/README.md
@@ -0,0 +1,5 @@
+# @backstage/plugin-auth-backend-module-oauth2-proxy-provider
+
+The oauth2-proxy-provider backend module for the auth plugin.
+
+_This plugin was created through the Backstage CLI_
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/api-report.md b/plugins/auth-backend-module-oauth2-proxy-provider/api-report.md
new file mode 100644
index 0000000000..a07db4ce11
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/api-report.md
@@ -0,0 +1,31 @@
+## API Report File for "@backstage/plugin-auth-backend-module-oauth2-proxy-provider"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { IncomingHttpHeaders } from 'http';
+import { ProxyAuthenticator } from '@backstage/plugin-auth-node';
+
+// @public (undocumented)
+export const authModuleOauth2ProxyProvider: () => BackendFeature;
+
+// @public
+export const OAUTH2_PROXY_JWT_HEADER = 'X-OAUTH2-PROXY-ID-TOKEN';
+
+// @public (undocumented)
+export const oauth2ProxyAuthenticator: ProxyAuthenticator<
+ unknown,
+ OAuth2ProxyResult
+>;
+
+// @public
+export type OAuth2ProxyResult = {
+ fullProfile: JWTPayload;
+ accessToken: string;
+ headers: IncomingHttpHeaders;
+ getHeader(name: string): string | undefined;
+};
+```
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/catalog-info.yaml b/plugins/auth-backend-module-oauth2-proxy-provider/catalog-info.yaml
new file mode 100644
index 0000000000..c26202c97d
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/catalog-info.yaml
@@ -0,0 +1,10 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: backstage-plugin-auth-backend-module-oauth2-proxy-provider
+ title: '@backstage/plugin-auth-backend-module-oauth2-proxy-provider'
+ description: The oauth2-proxy-provider backend module for the auth plugin.
+spec:
+ lifecycle: experimental
+ type: backstage-backend-plugin-module
+ owner: maintainers
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json
new file mode 100644
index 0000000000..be5617eb69
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider",
+ "description": "The oauth2-proxy-provider backend module for the auth plugin.",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "jose": "^4.6.0"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/authenticator.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/authenticator.ts
new file mode 100644
index 0000000000..a3da83791a
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/authenticator.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { AuthenticationError } from '@backstage/errors';
+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
+ *
+ * @public
+ */
+export const OAUTH2_PROXY_JWT_HEADER = 'X-OAUTH2-PROXY-ID-TOKEN';
+
+/** @public */
+export const oauth2ProxyAuthenticator = createProxyAuthenticator<
+ unknown,
+ OAuth2ProxyResult
+>({
+ defaultProfileTransform: async (result: OAuth2ProxyResult) => {
+ return {
+ profile: {
+ email: result.getHeader('x-forwarded-email'),
+ displayName:
+ result.getHeader('x-forwarded-preferred-username') ||
+ result.getHeader('x-forwarded-user'),
+ },
+ };
+ },
+ async initialize() {},
+ async authenticate({ req }) {
+ try {
+ const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
+ const jwt = getBearerTokenFromAuthorizationHeader(authHeader);
+ const decodedJWT = jwt && decodeJwt(jwt);
+
+ const result = {
+ fullProfile: decodedJWT || {},
+ 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);
+ },
+ };
+ return { result };
+ } catch (e) {
+ throw new AuthenticationError('Authentication failed', e);
+ }
+ },
+});
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/index.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/index.ts
new file mode 100644
index 0000000000..db0da00a16
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/index.ts
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+/**
+ * The oauth2-proxy-provider backend module for the auth plugin.
+ *
+ * @packageDocumentation
+ */
+export { authModuleOauth2ProxyProvider } from './module';
+export {
+ oauth2ProxyAuthenticator,
+ OAUTH2_PROXY_JWT_HEADER,
+} from './authenticator';
+export type { OAuth2ProxyResult } from './types';
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/module.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/module.ts
new file mode 100644
index 0000000000..0d9b9ae74f
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/module.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { createBackendModule } from '@backstage/backend-plugin-api';
+import {
+ authProvidersExtensionPoint,
+ createProxyAuthProviderFactory,
+ commonSignInResolvers,
+} from '@backstage/plugin-auth-node';
+import { oauth2ProxyAuthenticator } from './authenticator';
+import { oauth2ProxySignInResolvers } from './resolvers';
+
+/** @public */
+export const authModuleOauth2ProxyProvider = createBackendModule({
+ pluginId: 'auth',
+ moduleId: 'oauth2ProxyProvider',
+ register(reg) {
+ reg.registerInit({
+ deps: {
+ providers: authProvidersExtensionPoint,
+ },
+ async init({ providers }) {
+ providers.registerProvider({
+ providerId: 'oauth2ProxyProvider',
+ factory: createProxyAuthProviderFactory({
+ authenticator: oauth2ProxyAuthenticator,
+ signInResolverFactories: {
+ ...commonSignInResolvers,
+ ...oauth2ProxySignInResolvers,
+ },
+ }),
+ });
+ },
+ });
+ },
+});
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts
new file mode 100644
index 0000000000..99ca8bc156
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 {
+ createSignInResolverFactory,
+ SignInInfo,
+} from '@backstage/plugin-auth-node';
+import { OAuth2ProxyResult } from './types';
+
+/**
+ * @public
+ */
+export namespace oauth2ProxySignInResolvers {
+ export const forwardedUserMatchingUserEntityName =
+ createSignInResolverFactory({
+ create() {
+ return async (info: SignInInfo, ctx) => {
+ const name = info.result.getHeader('x-forwarded-user');
+ if (!name) {
+ throw new Error('Request did not contain a user');
+ }
+ return ctx.signInWithCatalogUser({
+ entityRef: { name },
+ });
+ };
+ },
+ });
+}
diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/types.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/types.ts
new file mode 100644
index 0000000000..5b9e97ab05
--- /dev/null
+++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/types.ts
@@ -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 = {
+ /**
+ * 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;
+};
diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md
index c20ddbe5a8..c269a09d2c 100644
--- a/plugins/auth-backend/api-report.md
+++ b/plugins/auth-backend/api-report.md
@@ -3,8 +3,6 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
-///
-
import { AuthProviderConfig as AuthProviderConfig_2 } from '@backstage/plugin-auth-node';
import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-auth-node';
import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node';
@@ -23,8 +21,8 @@ import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { GcpIapResult as GcpIapResult_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
import { GcpIapTokenInfo as GcpIapTokenInfo_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
-import { IncomingHttpHeaders } from 'http';
import { LoggerService } from '@backstage/backend-plugin-api';
+import { OAuth2ProxyResult as OAuth2ProxyResult_2 } from '@backstage/plugin-auth-backend-module-oauth2-proxy-provider';
import { OAuthEnvironmentHandler as OAuthEnvironmentHandler_2 } from '@backstage/plugin-auth-node';
import { OAuthState as OAuthState_2 } from '@backstage/plugin-auth-node';
import { PluginDatabaseManager } from '@backstage/backend-common';
@@ -228,13 +226,8 @@ export type GithubOAuthResult = {
refreshToken?: string;
};
-// @public
-export type OAuth2ProxyResult = {
- fullProfile: JWTPayload;
- accessToken: string;
- headers: IncomingHttpHeaders;
- getHeader(name: string): string | undefined;
-};
+// @public @deprecated (undocumented)
+export type OAuth2ProxyResult = OAuth2ProxyResult_2;
// @public @deprecated (undocumented)
export class OAuthAdapter implements AuthProviderRouteHandlers {
@@ -560,9 +553,9 @@ export const providers: Readonly<{
}>;
oauth2Proxy: Readonly<{
create: (options: {
- authHandler?: AuthHandler> | undefined;
+ authHandler?: AuthHandler | undefined;
signIn: {
- resolver: SignInResolver>;
+ resolver: SignInResolver;
};
}) => AuthProviderFactory_2;
resolvers: never;
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 67d1b0734d..b551d114bb 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -44,6 +44,7 @@
"@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-google-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^",
+ "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-okta-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/index.ts b/plugins/auth-backend/src/providers/oauth2-proxy/index.ts
index d3004f31e4..2e4e7d016f 100644
--- a/plugins/auth-backend/src/providers/oauth2-proxy/index.ts
+++ b/plugins/auth-backend/src/providers/oauth2-proxy/index.ts
@@ -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;
diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts
deleted file mode 100644
index f585d4831f..0000000000
--- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts
+++ /dev/null
@@ -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;
- let logger: jest.Mocked;
- let signInResolver: jest.MockedFunction<
- SignInResolver>
- >;
- let authHandler: jest.MockedFunction>>;
- let mockResponse: jest.Mocked;
- let mockRequest: jest.Mocked;
- let mockJwtDecode: jest.MockedFunction;
-
- 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;
-
- mockResponse = {
- status: jest.fn(),
- end: jest.fn(),
- json: jest.fn(),
- } as unknown as jest.Mocked;
-
- mockRequest = {
- body: {},
- header: jest.fn(),
- headers: {
- 'x-mock': 'mock',
- },
- } as unknown as jest.Mocked;
-
- provider = new Oauth2ProxyAuthProvider({
- 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();
- });
- });
-});
diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
index 3ac7d09000..5d75167e84 100644
--- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
+++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
@@ -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 = {
- /**
- * 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 {
- resolverContext: AuthResolverContext;
- signInResolver: SignInResolver>;
- authHandler: AuthHandler>;
-}
-
-export class Oauth2ProxyAuthProvider
- implements AuthProviderRouteHandlers
-{
- private readonly resolverContext: AuthResolverContext;
- private readonly signInResolver: SignInResolver<
- OAuth2ProxyResult
- >;
- private readonly authHandler: AuthHandler>;
-
- constructor(options: Options) {
- this.resolverContext = options.resolverContext;
- this.signInResolver = options.signInResolver;
- this.authHandler = options.authHandler;
- }
-
- frameHandler(): Promise {
- return Promise.resolve(undefined);
- }
-
- async refresh(req: express.Request, res: express.Response): Promise {
- 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 {
- return Promise.resolve(undefined);
- }
-
- private async handleResult(
- result: OAuth2ProxyResult,
- ): Promise> {
- 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,
-): Promise {
- 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(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>;
+ authHandler?: AuthHandler;
/**
* 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>;
+ resolver: SignInResolver;
};
}) {
- return ({ resolverContext }) => {
- const signInResolver = options.signIn.resolver;
- const authHandler = options.authHandler;
- return new Oauth2ProxyAuthProvider({
- resolverContext,
- signInResolver,
- authHandler: authHandler ?? defaultAuthHandler,
- });
- };
+ return createProxyAuthProviderFactory({
+ authenticator: oauth2ProxyAuthenticator,
+ profileTransform: options?.authHandler,
+ signInResolver: options?.signIn?.resolver,
+ });
},
});
diff --git a/yarn.lock b/yarn.lock
index 2cd8d238df..2399f06ac5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4873,6 +4873,20 @@ __metadata:
languageName: unknown
linkType: soft
+"@backstage/plugin-auth-backend-module-oauth2-proxy-provider@workspace:^, @backstage/plugin-auth-backend-module-oauth2-proxy-provider@workspace:plugins/auth-backend-module-oauth2-proxy-provider":
+ version: 0.0.0-use.local
+ resolution: "@backstage/plugin-auth-backend-module-oauth2-proxy-provider@workspace:plugins/auth-backend-module-oauth2-proxy-provider"
+ dependencies:
+ "@backstage/backend-common": "workspace:^"
+ "@backstage/backend-plugin-api": "workspace:^"
+ "@backstage/backend-test-utils": "workspace:^"
+ "@backstage/cli": "workspace:^"
+ "@backstage/errors": "workspace:^"
+ "@backstage/plugin-auth-node": "workspace:^"
+ jose: ^4.6.0
+ languageName: unknown
+ linkType: soft
+
"@backstage/plugin-auth-backend-module-okta-provider@workspace:^, @backstage/plugin-auth-backend-module-okta-provider@workspace:plugins/auth-backend-module-okta-provider":
version: 0.0.0-use.local
resolution: "@backstage/plugin-auth-backend-module-okta-provider@workspace:plugins/auth-backend-module-okta-provider"
@@ -4957,6 +4971,7 @@ __metadata:
"@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-google-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^"
+ "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-okta-provider": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"