diff --git a/.changeset/hungry-goats-mate.md b/.changeset/hungry-goats-mate.md
new file mode 100644
index 0000000000..733b1c7cf5
--- /dev/null
+++ b/.changeset/hungry-goats-mate.md
@@ -0,0 +1,9 @@
+---
+'@backstage/plugin-auth-backend': patch
+---
+
+Updates the OAuth2 Proxy provider to require less infrastructure configuration.
+
+The auth result object of the OAuth2 Proxy now provides access to the request headers, both through the `headers` object as well as `getHeader` method. The existing logic that parses and extracts the user information from ID tokens is deprecated and will be removed in a future release. See the OAuth2 Proxy provider documentation for more details.
+
+The OAuth2 Proxy provider now also has a default `authHandler` implementation that reads the display name and email from the incoming request headers.
diff --git a/app-config.yaml b/app-config.yaml
index b3ca3c7203..1156b9b340 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -378,6 +378,8 @@ auth:
clientId: ${AUTH_ATLASSIAN_CLIENT_ID}
clientSecret: ${AUTH_ATLASSIAN_CLIENT_SECRET}
scopes: ${AUTH_ATLASSIAN_SCOPES}
+ myproxy:
+ development: {}
costInsights:
engineerCost: 200000
products:
diff --git a/docs/auth/oauth2-proxy/provider.md b/docs/auth/oauth2-proxy/provider.md
index f366929fe8..644219ffa0 100644
--- a/docs/auth/oauth2-proxy/provider.md
+++ b/docs/auth/oauth2-proxy/provider.md
@@ -20,63 +20,39 @@ The provider configuration can be added to your `app-config.yaml` under the root
```yaml
auth:
- environment: development
providers:
oauth2proxy: {}
```
-Right now no configuration options are supported. To make use of the provider,
-make sure that your `oauth2-proxy` is configured correctly and provides a custom
-`X-OAUTH2-PROXY-ID-TOKEN` header. To do so, enable the
-`--set-authorization-header=true` of your `oauth2-proxy` and forward the
-`Authorization` header as `X-OAUTH2-PROXY-ID-TOKEN`. For more details check the
-[configuration docs](https://oauth2-proxy.github.io/oauth2-proxy/configuration).
+Right now no configuration options are supported, but the empty object is needed
+to enable the provider in the auth backend.
-_Example for kubernetes ingress:_
+To use the `oauth2proxy` provider you must also configure it with a sign-in resolver.
+For more information about the sign-in process in general, see the
+[Sign-in Identities and Resolvers](../identity-resolver.md) documentation.
-```bash
-# forward the authorization header from the auth request in the X-OAUTH2-PROXY-ID-TOKEN header
-auth_request_set $name_upstream_authorization $upstream_http_authorization;
-proxy_set_header X-OAUTH2-PROXY-ID-TOKEN $name_upstream_authorization;
-```
+For the `oauth2proxy` provider, the sign-in result is quite different than other providers.
+Because it's a proxy provider that can be configured to forward information through
+arbitrary headers, the auth result simply just gives you access to the HTTP headers
+of the incoming request. Using these you can either extract the information directly,
+or grab ID or access tokens to look up additional information and/or validate the request.
-## Adding the provider to the Backstage backend
-
-When using `oauth2proxy` auth you can configure it as described
-[here](https://backstage.io/docs/auth/identity-resolver).
-
-- use the following code below to introduce changes to
- `packages/backend/plugin/auth.ts`:
+A simple sign-in resolver might for example look like this:
```ts
- providerFactories: {
- oauth2proxy: createOauth2ProxyProvider<{
- id: string;
- email: string;
- }>({
- authHandler: async input => {
- const { email } = input.fullProfile;
-
- return {
- profile: {
- email,
- },
- };
- },
- signIn: {
- resolver: async (signInInfo, ctx) => {
- const { preferred_username: id } = signInInfo.result.fullProfile;
- const sub = `user:default/${id}`;
-
- const token = await ctx.tokenIssuer.issueToken({
- claims: { sub, ent: [`group:default/optional-user-group`] },
- });
-
- return { id, token };
- },
- },
- }),
- }
+providers.oauth2Proxy.create({
+ signIn: {
+ async resolver({ result }, ctx) {
+ const name = result.getHeader('x-forwarded-user');
+ if (!name) {
+ throw new Error('Request did not contain a user')
+ }
+ return ctx.signInWithCatalogUser({
+ entityRef: { name },
+ });
+ },
+ },
+}),
```
## Adding the provider to the Backstage frontend
diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts
index 8a60b4b4bd..cd0c42afe5 100644
--- a/packages/backend/src/plugins/auth.ts
+++ b/packages/backend/src/plugins/auth.ts
@@ -14,6 +14,10 @@
* limitations under the License.
*/
+import {
+ DEFAULT_NAMESPACE,
+ stringifyEntityRef,
+} from '@backstage/catalog-model';
import {
createRouter,
providers,
@@ -89,6 +93,27 @@ export default async function createPlugin(
},
},
}),
+
+ // This is an example of how to configure the OAuth2Proxy provider as well
+ // as how to sign a user in without a matching user entity in the catalog.
+ // You can try it out using ``
+ myproxy: providers.oauth2Proxy.create({
+ signIn: {
+ async resolver({ result }, ctx) {
+ const entityRef = stringifyEntityRef({
+ kind: 'user',
+ namespace: DEFAULT_NAMESPACE,
+ name: result.getHeader('x-forwarded-user')!,
+ });
+ return ctx.issueToken({
+ claims: {
+ sub: entityRef,
+ ent: [entityRef],
+ },
+ });
+ },
+ },
+ }),
},
});
}
diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md
index 18924f7c23..a38197668e 100644
--- a/plugins/auth-backend/api-report.md
+++ b/plugins/auth-backend/api-report.md
@@ -12,6 +12,7 @@ import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { GetEntitiesRequest } from '@backstage/catalog-client';
+import { IncomingHttpHeaders } from 'http';
import { JsonValue } from '@backstage/types';
import { Logger } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
@@ -376,7 +377,7 @@ export const createOAuth2Provider: (
// @public @deprecated (undocumented)
export const createOauth2ProxyProvider: (options: {
- authHandler: AuthHandler>;
+ authHandler?: AuthHandler> | undefined;
signIn: {
resolver: SignInResolver>;
};
@@ -563,9 +564,11 @@ export type Oauth2ProxyProviderOptions = {
};
// @public
-export type OAuth2ProxyResult = {
+export type OAuth2ProxyResult = {
fullProfile: JWTPayload;
accessToken: string;
+ headers: IncomingHttpHeaders;
+ getHeader(name: string): string | undefined;
};
// Warning: (ae-missing-release-tag) "OAuthAdapter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -912,7 +915,7 @@ export const providers: Readonly<{
}>;
oauth2Proxy: Readonly<{
create: (options: {
- authHandler: AuthHandler>;
+ authHandler?: AuthHandler> | undefined;
signIn: {
resolver: SignInResolver>;
};
diff --git a/plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml b/plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml
new file mode 100755
index 0000000000..f00e2e0987
--- /dev/null
+++ b/plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml
@@ -0,0 +1,44 @@
+#!/usr/bin/env docker-compose -f
+
+# This docker compose file can be used to try out the oauth2-proxy auth provider.
+# You'll need to provide your own GitHub client ID and secret through
+# GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET.
+#
+# The only modifications you need to make to run this example is to switch the
+# SignInPage to the following:
+#
+#
+#
+# You also need to switch out the baseUrl and listen port of both the frontend and
+# backend, but we can do that through env vars when running `yarn dev`:
+#
+# APP_CONFIG_app_baseUrl=http://localhost APP_CONFIG_app_listen_port=3000 \
+# APP_CONFIG_backend_baseUrl=http://localhost APP_CONFIG_backend_listen_port=7007 yarn dev
+#
+# Once done, you can run the following from the root and then navigate to http://localhost
+#
+# ./plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml up
+
+version: '3'
+services:
+ proxy:
+ container_name: oauth2-proxy
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.2.1
+
+ # The below config assumes that you are running the frontend and backend
+ # in development mode, and that `host.docker.internal` resolves to the host machine.
+ command: >-
+ --cookie-secret=super-super-secret-cookie-secret
+ --cookie-secure=false
+ --http-address=0.0.0.0:80
+ --email-domain=*
+
+ --provider=github
+ --client-id=${GITHUB_CLIENT_ID}
+ --client-secret=${GITHUB_CLIENT_SECRET}
+
+ --redirect-url=http://localhost/oauth2/callback
+ --upstream=http://host.docker.internal:7007/api/,http://host.docker.internal:3000
+
+ ports:
+ - 80:80
diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts
index d258c17e61..f10a81c76b 100644
--- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts
+++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts
@@ -25,7 +25,7 @@ import * as jose from 'jose';
import { Logger } from 'winston';
import { AuthHandler, AuthResolverContext, SignInResolver } from '../types';
import {
- createOauth2ProxyProvider,
+ oauth2Proxy,
Oauth2ProxyAuthProvider,
OAuth2ProxyResult,
OAUTH2_PROXY_JWT_HEADER,
@@ -64,6 +64,9 @@ describe('Oauth2ProxyAuthProvider', () => {
mockRequest = {
body: {},
header: jest.fn(),
+ headers: {
+ 'x-mock': 'mock',
+ },
} as unknown as jest.Mocked;
provider = new Oauth2ProxyAuthProvider({
@@ -146,6 +149,10 @@ describe('Oauth2ProxyAuthProvider', () => {
result: {
accessToken: 'token',
fullProfile: decodedToken,
+ getHeader: expect.any(Function),
+ headers: {
+ 'x-mock': 'mock',
+ },
},
},
{ _: 'resolver-context' },
@@ -167,7 +174,7 @@ describe('Oauth2ProxyAuthProvider', () => {
});
});
- describe('createOauth2ProxyProvider()', () => {
+ describe('oauth2Proxy.create()', () => {
beforeEach(() => {
mockRequest.header.mockReturnValue(`Bearer token`);
authHandler.mockResolvedValue({
@@ -179,7 +186,7 @@ describe('Oauth2ProxyAuthProvider', () => {
});
it('should create a valid provider', async () => {
- const factory = createOauth2ProxyProvider({
+ const factory = oauth2Proxy.create({
authHandler,
signIn: { resolver: signInResolver },
});
diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
index acef4a74ef..625c9e3d62 100644
--- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
+++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
@@ -23,10 +23,17 @@ import {
AuthProviderRouteHandlers,
AuthResponse,
AuthResolverContext,
+ AuthHandlerResult,
} from '../types';
import { decodeJwt } from 'jose';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
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';
@@ -36,16 +43,41 @@ export const OAUTH2_PROXY_JWT_HEADER = 'X-OAUTH2-PROXY-ID-TOKEN';
*
* @public
*/
-export type OAuth2ProxyResult = {
+export type OAuth2ProxyResult = {
/**
- * Parsed and decoded JWT payload.
+ * 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;
/**
- * Raw JWT token
+ * 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;
};
/**
@@ -96,7 +128,23 @@ export class Oauth2ProxyAuthProvider
async refresh(req: express.Request, res: express.Response): Promise {
try {
- const result = this.getResult(req);
+ // 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) {
@@ -131,24 +179,19 @@ export class Oauth2ProxyAuthProvider
profile,
};
}
+}
- private getResult(req: express.Request): OAuth2ProxyResult {
- const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
- const jwt = getBearerTokenFromAuthorizationHeader(authHeader);
-
- if (!jwt) {
- throw new AuthenticationError(
- `Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`,
- );
- }
-
- const decodedJWT = decodeJwt(jwt) as unknown as JWTPayload;
-
- return {
- fullProfile: decodedJWT,
- accessToken: jwt,
- };
- }
+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'),
+ },
+ };
}
/**
@@ -160,8 +203,12 @@ export const oauth2Proxy = createAuthProviderIntegration({
create(options: {
/**
* Configure an auth handler to generate a profile for the user.
+ *
+ * The default implementation uses the value of the `X-Forwarded-Preferred-Username`
+ * 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.
@@ -179,7 +226,7 @@ export const oauth2Proxy = createAuthProviderIntegration({
return new Oauth2ProxyAuthProvider({
resolverContext,
signInResolver,
- authHandler,
+ authHandler: authHandler ?? defaultAuthHandler,
});
};
},