feat: add oauth2proxy provider
Signed-off-by: Adrian Barwicki <adrian.barwicki.extern@sda.se> Signed-off-by: Dominik Schwank <dominik.schwank@sda.se>
This commit is contained in:
committed by
Dominik Schwank
parent
2a13ac8902
commit
6e92ee6267
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Add new authentication provider: OAuth2Proxy
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -59,9 +59,17 @@ export type Auth0ProviderOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthContext = {
|
||||
tokenIssuer: TokenIssuer;
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
logger: Logger_2;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthHandler<TAuthResult> = (
|
||||
input: TAuthResult,
|
||||
context?: AuthContext,
|
||||
) => Promise<AuthHandlerResult>;
|
||||
|
||||
// @public
|
||||
@@ -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: AuthContext,
|
||||
) => 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
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
AuthContext,
|
||||
AuthHandlerResult,
|
||||
SignInResolver,
|
||||
SignInInfo,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 AuthContext = {
|
||||
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: AuthContext,
|
||||
) => Promise<BackstageSignInResult>;
|
||||
|
||||
/**
|
||||
@@ -289,6 +296,7 @@ export type AuthHandlerResult = { profile: ProfileInfo };
|
||||
*/
|
||||
export type AuthHandler<TAuthResult> = (
|
||||
input: TAuthResult,
|
||||
context?: AuthContext,
|
||||
) => Promise<AuthHandlerResult>;
|
||||
|
||||
export type StateEncoder = (
|
||||
|
||||
Reference in New Issue
Block a user