Merge pull request #8396 from SDA-SE/feat/DOCKM-1134

feat: Add new authentication provider: OAuth2Proxy
This commit is contained in:
Fredrik Adelöw
2022-01-17 15:50:13 +01:00
committed by GitHub
26 changed files with 692 additions and 96 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-auth-backend': minor
---
Add new authentication provider to support the oauth2-proxy.
**BREAKING** The `AuthHandler` requires now an `AuthResolverContext` parameter. This aligns with the
behavior of the `SignInResolver`.
+1
View File
@@ -25,6 +25,7 @@ Backstage comes with many common authentication providers in the core library:
- [Google](google/provider.md)
- [Okta](okta/provider.md)
- [OneLogin](onelogin/provider.md)
- [OAuth2Proxy](oauth2-proxy/provider.md)
These built-in providers handle the authentication flow for a particular service
including required scopes, callbacks, etc. These providers are each added to a
+105
View File
@@ -0,0 +1,105 @@
---
id: provider
title: OAuth 2 Proxy Provider
sidebar_label: OAuth 2 Custom Proxy
description: Adding OAuth2Proxy as an authentication provider in Backstage
---
The Backstage `@backstage/plugin-auth-backend` package comes with an
`oauth2proxy` authentication provider that can authenticate users by using a
[oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) in front of an
actual Backstage instance. This enables to reuse existing authentications within
a cluster. In general the `oauth2-proxy` supports all OpenID Connect providers,
for more details check this
[list of supported providers](https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider).
## Configuration
The provider configuration can be added to your `app-config.yaml` under the root
`auth` configuration:
```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).
_Example for kubernetes ingress:_
```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;
```
## 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`:
```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 };
},
},
}),
}
```
## Adding the provider to the Backstage frontend
All Backstage apps need a `SignInPage` to be configured. Its purpose is to
establish who the user is and what their identifying credentials are, blocking
rendering the rest of the UI until that's complete, and then keeping those
credentials fresh.
When using the OAuth2-Proxy, the Backstage UI can only be accessed after the
user has already been authenticated at the proxy. Instead of showing the user
another login page when accessing Backstage, it will handle the login in the
background. Backstage provides for this case a special `SignInPage` component
which has no UI.
Update your `createApp` call in `packages/app/src/App.tsx`, as follows.
```diff
+import { ProxiedSignInPage } from '@backstage/core-components';
const app = createApp({
components: {
+ SignInPage: props => <ProxiedSignInPage {...props} provider="oauth2-proxy" />,
```
After this, your app should be ready to leverage the OAuth2-Proxy for
authentication!
+2 -1
View File
@@ -228,7 +228,8 @@
"auth/gitlab/provider",
"auth/google/provider",
"auth/okta/provider",
"auth/onelogin/provider"
"auth/onelogin/provider",
"auth/oauth2-proxy/provider"
]
},
"auth/add-auth-provider",
+1
View File
@@ -144,6 +144,7 @@ nav:
- Google: 'auth/google/provider.md'
- Okta: 'auth/okta/provider.md'
- OneLogin: 'auth/onelogin/provider.md'
- OAuth2Proxy: 'auth/oauth2-proxy/provider.md'
- Bitbucket: 'auth/bitbucket/provider.md'
- Adding authentication providers: 'auth/add-auth-provider.md'
- Using authentication and identity: 'auth/using-auth.md'
+29 -6
View File
@@ -62,6 +62,7 @@ export type Auth0ProviderOptions = {
// @public
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
context: AuthResolverContext,
) => Promise<AuthHandlerResult>;
// @public
@@ -99,6 +100,13 @@ export interface AuthProviderRouteHandlers {
start(req: express.Request, res: express.Response): Promise<void>;
}
// @public
export type AuthResolverContext = {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger_2;
};
// Warning: (ae-missing-release-tag) "AuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -270,6 +278,11 @@ export const createOAuth2Provider: (
options?: OAuth2ProviderOptions | undefined,
) => AuthProviderFactory;
// @public
export const createOauth2ProxyProvider: <JWTPayload>(
options: Oauth2ProxyProviderOptions<JWTPayload>,
) => AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createOidcProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -436,6 +449,20 @@ export type OAuth2ProviderOptions = {
};
};
// @public
export type Oauth2ProxyProviderOptions<JWTPayload> = {
authHandler: AuthHandler<OAuth2ProxyResult<JWTPayload>>;
signIn: {
resolver: SignInResolver<OAuth2ProxyResult<JWTPayload>>;
};
};
// @public
export type OAuth2ProxyResult<JWTPayload> = {
fullProfile: JWTPayload;
accessToken: string;
};
// Warning: (ae-missing-release-tag) "OAuthAdapter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -664,11 +691,7 @@ export type SignInInfo<TAuthResult> = {
// @public
export type SignInResolver<TAuthResult> = (
info: SignInInfo<TAuthResult>,
context: {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger_2;
},
context: AuthResolverContext,
) => Promise<BackstageSignInResult>;
// Warning: (ae-missing-release-tag) "TokenIssuer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -704,5 +727,5 @@ export type WebMessageResponse =
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts
// src/providers/github/provider.d.ts:81:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:88:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:98:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
```
@@ -120,7 +120,12 @@ export class AtlassianAuthProvider implements OAuthHandlers {
}
private async handleResult(result: OAuthResult): Promise<OAuthResponse> {
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -138,11 +143,7 @@ export class AtlassianAuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -149,7 +149,12 @@ export class Auth0AuthProvider implements OAuthHandlers {
}
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -167,11 +172,7 @@ export class Auth0AuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -182,17 +182,18 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
}
private async handleResult(result: AwsAlbResult): Promise<AwsAlbResponse> {
const { profile } = await this.authHandler(result);
const context = {
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
};
const { profile } = await this.authHandler(result, context);
const backstageIdentity = await this.signInResolver(
{
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
return {
@@ -174,7 +174,12 @@ export class BitbucketAuthProvider implements OAuthHandlers {
private async handleResult(result: BitbucketOAuthResult) {
result.fullProfile.avatarUrl =
result.fullProfile._json!.links!.avatar!.href;
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -192,11 +197,7 @@ export class BitbucketAuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -71,16 +71,17 @@ export class GcpIapProvider implements AuthProviderRouteHandlers {
req.header(IAP_JWT_HEADER),
this.tokenValidator,
);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result);
const { profile } = await this.authHandler(result, context);
const backstageIdentity = await this.signInResolver(
{ profile, result },
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
const response: GcpIapResponse = {
@@ -152,7 +152,12 @@ export class GithubAuthProvider implements OAuthHandlers {
}
private async handleResult(result: GithubOAuthResult) {
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const expiresInStr = result.params.expires_in;
const response: OAuthResponse = {
@@ -171,11 +176,7 @@ export class GithubAuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -167,7 +167,12 @@ export class GitlabAuthProvider implements OAuthHandlers {
}
private async handleResult(result: OAuthResult): Promise<OAuthResponse> {
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -185,11 +190,7 @@ export class GitlabAuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -148,7 +148,12 @@ export class GoogleAuthProvider implements OAuthHandlers {
}
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -166,11 +171,7 @@ export class GoogleAuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -23,6 +23,7 @@ export * from './gitlab';
export * from './google';
export * from './microsoft';
export * from './oauth2';
export * from './oauth2-proxy';
export * from './oidc';
export * from './okta';
export * from './onelogin';
@@ -38,6 +39,7 @@ export type {
AuthProviderFactoryOptions,
AuthProviderFactory,
AuthHandler,
AuthResolverContext,
AuthHandlerResult,
SignInResolver,
SignInInfo,
@@ -143,7 +143,12 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
const photo = await this.getUserPhoto(result.accessToken);
result.fullProfile.photos = photo ? [{ value: photo }] : undefined;
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -161,11 +166,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -0,0 +1,18 @@
/*
* 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.
*/
export { createOauth2ProxyProvider } from './provider';
export type { Oauth2ProxyProviderOptions, OAuth2ProxyResult } from './provider';
@@ -0,0 +1,214 @@
/*
* 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', () => ({
JWT: {
decode: jest.fn(),
},
}));
jest.mock('@backstage/catalog-client');
import express from 'express';
import { JWT } from 'jose';
import { Logger } from 'winston';
import {
AuthHandler,
SignInResolver,
AuthProviderFactoryOptions,
} from '../types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { TokenIssuer } from '../../identity/types';
import {
createOauth2ProxyProvider,
Oauth2ProxyAuthProvider,
Oauth2ProxyProviderOptions,
OAuth2ProxyResult,
OAUTH2_PROXY_JWT_HEADER,
} from './provider';
describe('Oauth2ProxyAuthProvider', () => {
const mockToken =
'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob';
let provider: Oauth2ProxyAuthProvider<any>;
let logger: jest.Mocked<Logger>;
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 JWTMock: jest.Mocked<typeof JWT>;
beforeEach(() => {
jest.resetAllMocks();
JWTMock = JWT as jest.Mocked<typeof JWT>;
authHandler = jest.fn();
signInResolver = jest.fn();
logger = { error: jest.fn() } as unknown as jest.Mocked<Logger>;
mockResponse = {
status: jest.fn(),
end: jest.fn(),
json: jest.fn(),
} as unknown as jest.Mocked<express.Response>;
mockRequest = {
body: {},
header: jest.fn(),
} as unknown as jest.Mocked<express.Request>;
provider = new Oauth2ProxyAuthProvider<any>({
authHandler,
logger,
signInResolver,
catalogIdentityClient: {} as CatalogIdentityClient,
tokenIssuer: {} as TokenIssuer,
});
});
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 provider.refresh(mockRequest, mockResponse);
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
it('should throw an error if the bearer token is invalid', async () => {
mockRequest.header.mockReturnValue('Basic asdf=');
await provider.refresh(mockRequest, mockResponse);
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
it('should return if auth header is set and valid', async () => {
mockRequest.header.mockReturnValue(`Bearer token`);
authHandler.mockResolvedValue({
profile: {},
});
signInResolver.mockResolvedValue({
id: 'some-id',
token: mockToken,
});
await provider.refresh(mockRequest, mockResponse);
expect(mockRequest.header).toBeCalledWith(OAUTH2_PROXY_JWT_HEADER);
expect(JWTMock.decode).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({
id: 'some-id',
token: mockToken,
});
authHandler.mockResolvedValue({ profile: profile });
JWTMock.decode.mockReturnValue(decodedToken as any);
await provider.refresh(mockRequest, mockResponse);
expect(signInResolver).toHaveBeenCalledWith(
{
profile: profile,
result: {
accessToken: 'token',
fullProfile: decodedToken,
},
},
{ catalogIdentityClient: {}, logger, tokenIssuer: {} },
);
expect(mockResponse.json).toHaveBeenCalledWith({
backstageIdentity: {
id: 'some-id',
idToken: mockToken,
identity: {
ownershipEntityRefs: ['user:default/jimmymarkum'],
type: 'user',
userEntityRef: 'jimmymarkum',
},
token: mockToken,
},
profile: { displayName: 'some value' },
providerInfo: {
accessToken: 'token',
},
});
});
});
describe('createOauth2ProxyProvider()', () => {
beforeEach(() => {
mockRequest.header.mockReturnValue(`Bearer token`);
authHandler.mockResolvedValue({
profile: {},
});
signInResolver.mockResolvedValue({
id: 'some-id',
token: mockToken,
});
});
it('should create a valid provider', async () => {
const providerOptions = {
authHandler,
signIn: { resolver: signInResolver },
} as Oauth2ProxyProviderOptions<any>;
const factoryOptions = {
logger,
catalogApi: {},
tokenIssuer: {},
} as unknown as AuthProviderFactoryOptions;
const factory = createOauth2ProxyProvider(providerOptions);
const handler = factory(factoryOptions);
await handler.refresh!(mockRequest, mockResponse);
expect(mockRequest.header).toBeCalledWith(OAUTH2_PROXY_JWT_HEADER);
expect(JWTMock.decode).toHaveBeenCalledWith('token');
expect(mockResponse.json).toHaveBeenCalled();
});
});
});
@@ -0,0 +1,199 @@
/*
* 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.
*/
import express from 'express';
import { Logger } from 'winston';
import { AuthenticationError } from '@backstage/errors';
import {
AuthHandler,
SignInResolver,
AuthProviderFactory,
AuthProviderRouteHandlers,
AuthResponse,
} from '../types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { JWT } from 'jose';
import { IdentityClient } from '../../identity';
import { TokenIssuer } from '../../identity/types';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
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> = {
/**
* Parsed and decoded JWT payload.
*/
fullProfile: JWTPayload;
/**
* Raw JWT token
*/
accessToken: string;
};
/**
* Options for the oauth2-proxy provider factory
*
* @public
*/
export type Oauth2ProxyProviderOptions<JWTPayload> = {
/**
* Configure an auth handler to generate a profile for the user.
*/
authHandler: AuthHandler<OAuth2ProxyResult<JWTPayload>>;
/**
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
*/
signIn: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<OAuth2ProxyResult<JWTPayload>>;
};
};
interface Options<JWTPayload> {
logger: Logger;
signInResolver: SignInResolver<OAuth2ProxyResult<JWTPayload>>;
authHandler: AuthHandler<OAuth2ProxyResult<JWTPayload>>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
}
export class Oauth2ProxyAuthProvider<JWTPayload>
implements AuthProviderRouteHandlers
{
private readonly logger: Logger;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly signInResolver: SignInResolver<
OAuth2ProxyResult<JWTPayload>
>;
private readonly authHandler: AuthHandler<OAuth2ProxyResult<JWTPayload>>;
private readonly tokenIssuer: TokenIssuer;
constructor(options: Options<JWTPayload>) {
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
this.tokenIssuer = options.tokenIssuer;
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 {
const result = this.getResult(req);
const response = await this.handleResult(result);
res.json(response);
} catch (e) {
this.logger.error(
`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`,
e,
);
res.status(401);
res.end();
}
}
start(): Promise<void> {
return Promise.resolve(undefined);
}
private async handleResult(
result: OAuth2ProxyResult<JWTPayload>,
): Promise<AuthResponse<{ accessToken: string }>> {
const ctx = {
logger: this.logger,
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
};
const { profile } = await this.authHandler(result, ctx);
const backstageSignInResult = await this.signInResolver(
{
result,
profile,
},
ctx,
);
return {
providerInfo: {
accessToken: result.accessToken,
},
backstageIdentity: prepareBackstageIdentityResponse(
backstageSignInResult,
),
profile,
};
}
private getResult(req: express.Request): OAuth2ProxyResult<JWTPayload> {
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
const jwt = IdentityClient.getBearerToken(authHeader);
if (!jwt) {
throw new AuthenticationError(
`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`,
);
}
const decodedJWT = JWT.decode(jwt) as unknown as JWTPayload;
return {
fullProfile: decodedJWT,
accessToken: jwt,
};
}
}
/**
* Factory function for oauth2-proxy auth provider
*
* @public
*/
export const createOauth2ProxyProvider =
<JWTPayload>(
options: Oauth2ProxyProviderOptions<JWTPayload>,
): AuthProviderFactory =>
({ catalogApi, logger, tokenIssuer }) => {
const signInResolver = options.signIn.resolver;
const authHandler = options.authHandler;
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
return new Oauth2ProxyAuthProvider<JWTPayload>({
logger,
signInResolver,
authHandler,
tokenIssuer,
catalogIdentityClient,
});
};
@@ -15,4 +15,5 @@
*/
export { createOAuth2Provider } from './provider';
export type { OAuth2ProviderOptions } from './provider';
@@ -163,7 +163,12 @@ export class OAuth2AuthProvider implements OAuthHandlers {
}
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -181,11 +186,7 @@ export class OAuth2AuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -182,7 +182,12 @@ export class OidcAuthProvider implements OAuthHandlers {
// Use this function to grab the user profile info from the token
// Then populate the profile with it
private async handleResult(result: OidcAuthResult): Promise<OAuthResponse> {
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
idToken: result.tokenset.id_token,
@@ -198,11 +203,7 @@ export class OidcAuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -169,7 +169,12 @@ export class OktaAuthProvider implements OAuthHandlers {
}
private async handleResult(result: OAuthResult) {
const { profile } = await this._authHandler(result);
const context = {
logger: this._logger,
catalogIdentityClient: this._catalogIdentityClient,
tokenIssuer: this._tokenIssuer,
};
const { profile } = await this._authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -187,11 +192,7 @@ export class OktaAuthProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this._tokenIssuer,
catalogIdentityClient: this._catalogIdentityClient,
logger: this._logger,
},
context,
);
}
@@ -148,7 +148,12 @@ export class OneLoginProvider implements OAuthHandlers {
}
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { profile } = await this.authHandler(result, context);
const response: OAuthResponse = {
providerInfo: {
@@ -166,11 +171,7 @@ export class OneLoginProvider implements OAuthHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
}
@@ -93,12 +93,18 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
res: express.Response,
): Promise<void> {
try {
const context = {
logger: this.logger,
catalogIdentityClient: this.catalogIdentityClient,
tokenIssuer: this.tokenIssuer,
};
const { result } = await executeFrameHandlerStrategy<SamlAuthResult>(
req,
this.strategy,
);
const { profile } = await this.authHandler(result);
const { profile } = await this.authHandler(result, context);
const response: AuthResponse<{}> = {
profile,
@@ -111,11 +117,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
context,
);
response.backstageIdentity =
+13 -5
View File
@@ -24,6 +24,17 @@ import { TokenIssuer } from '../identity/types';
import { OAuthStartRequest } from '../lib/oauth/types';
import { CatalogIdentityClient } from '../lib/catalog';
/**
* The context that is used for auth processing.
*
* @public
*/
export type AuthResolverContext = {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
};
export type AuthProviderConfig = {
/**
* The protocol://domain[:port] where the app is hosted. This is used to construct the
@@ -259,11 +270,7 @@ export type SignInInfo<TAuthResult> = {
*/
export type SignInResolver<TAuthResult> = (
info: SignInInfo<TAuthResult>,
context: {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
},
context: AuthResolverContext,
) => Promise<BackstageSignInResult>;
/**
@@ -289,6 +296,7 @@ export type AuthHandlerResult = { profile: ProfileInfo };
*/
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
context: AuthResolverContext,
) => Promise<AuthHandlerResult>;
export type StateEncoder = (