Merge pull request #2268 from spotify/rugvip/authref
auth-backend: refactor to split lib, move types closer to home, update docs, renaming things
This commit is contained in:
+3
-4
@@ -120,10 +120,9 @@ auth:
|
||||
audience:
|
||||
$secret:
|
||||
env: GITLAB_BASE_URL
|
||||
# saml:
|
||||
# development:
|
||||
# entryPoint: "http://localhost:7001/"
|
||||
# issuer: "passport-saml"
|
||||
saml:
|
||||
entryPoint: "http://localhost:7001/"
|
||||
issuer: "passport-saml"
|
||||
okta:
|
||||
development:
|
||||
clientId:
|
||||
|
||||
+104
-53
@@ -48,22 +48,35 @@ provider class which implements a handler for the chosen framework.
|
||||
#### Adding an OAuth based provider
|
||||
|
||||
If we're adding an `OAuth` based provider we would implement the
|
||||
[OAuthProviderHandlers](#OAuthProviderHandlers) interface.
|
||||
[OAuthProviderHandlers](#OAuthProviderHandlers) interface. By implementing this
|
||||
interface we can use the `OAuthProvider` class provided by `lib/oauth`, meaning
|
||||
we don't need to implement the full
|
||||
[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) interface that providers
|
||||
otherwise need to implement.
|
||||
|
||||
The provider class takes the provider's configuration as a class parameter. It
|
||||
also imports the `Strategy` from the passport package.
|
||||
The provider class takes the provider's options as a class parameter. It also
|
||||
imports the `Strategy` from the passport package.
|
||||
|
||||
```ts
|
||||
import { Strategy as ProviderAStrategy } from 'passport-provider-a';
|
||||
|
||||
export type ProviderAProviderOptions = OAuthProviderOptions & {
|
||||
// extra options here
|
||||
}
|
||||
|
||||
export class ProviderAAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
private readonly _strategy: ProviderAStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.providerConfig = providerConfig;
|
||||
constructor(options: ProviderAProviderOptions) {
|
||||
this._strategy = new ProviderAStrategy(
|
||||
{ ...providerConfig.options },
|
||||
{
|
||||
clientID: options.clientId,
|
||||
clientSecret: options.clientSecret,
|
||||
callbackURL: options.callbackUrl,
|
||||
passReqToCallback: false as true,
|
||||
response_type: 'code',
|
||||
/// ... etc
|
||||
}
|
||||
verifyFunction, // See the "Verify Callback" section
|
||||
);
|
||||
}
|
||||
@@ -82,14 +95,18 @@ An non-`OAuth` based provider could implement
|
||||
[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead.
|
||||
|
||||
```ts
|
||||
type ProviderAOptions = {
|
||||
// ...
|
||||
};
|
||||
|
||||
export class ProviderAAuthProvider implements AuthProviderRouteHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
private readonly _strategy: ProviderAStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.providerConfig = providerConfig;
|
||||
constructor(options: ProviderAOptions) {
|
||||
this._strategy = new ProviderAStrategy(
|
||||
{ ...providerConfig.options },
|
||||
{
|
||||
// ...
|
||||
},
|
||||
verifyFunction, // See the "Verify Callback" section
|
||||
);
|
||||
}
|
||||
@@ -101,31 +118,61 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers {
|
||||
}
|
||||
```
|
||||
|
||||
#### Create method
|
||||
#### Factory function
|
||||
|
||||
Each provider exports a create method that creates the provider instance,
|
||||
optionally extending a supported authorization framework. This method exists to
|
||||
allow for flexibility if additional frameworks are supported in the future.
|
||||
Each provider exports a factory function that instantiates the provider. The
|
||||
factory should implement [AuthProviderFactory](#AuthProviderFactory), which
|
||||
passes in a object with utilities for configuration, logging, token issuing,
|
||||
etc. The factory should return an implementation of
|
||||
[AuthProviderRouteHandlers](#AuthProviderRouteHandlers).
|
||||
|
||||
Implementing OAuth by returning an instance of `OAuthProvider` based of the
|
||||
provider's class:
|
||||
The factory is what decides the mapping from
|
||||
[static configuration](../conf/index.md) to the creation of auth providers. For
|
||||
example, OAuth providers use `OAuthEnvironmentHandler` to allow for multiple
|
||||
different configurations, one for each environment, which looks like this;
|
||||
|
||||
```ts
|
||||
export function createProviderAProvider(config: AuthProviderConfig) {
|
||||
const provider = new ProviderAAuthProvider(config);
|
||||
const oauthProvider = new OAuthProvider(provider, config.provider, true);
|
||||
return oauthProvider;
|
||||
}
|
||||
export const createOktaProvider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
// read options from config
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
|
||||
// instantiate our OAuthProviderHandlers implementation
|
||||
const provider = new OktaAuthProvider({
|
||||
audience,
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
});
|
||||
|
||||
// Wrap the OAuthProviderHandlers with OAuthProvider, which implements AuthProviderRouteHandlers
|
||||
return OAuthProvider.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Not extending with OAuth, the main difference here is that the create method is
|
||||
returning a instance of the class without adding the OAuth authorization
|
||||
framework to it.
|
||||
The purpose of the different environments is to allow for a single auth-backend
|
||||
to serve as the authentication service for multiple different frontend
|
||||
environments, such as local development, staging, and production.
|
||||
|
||||
The factory function for other providers can be a lot simpler, as they might not
|
||||
have configuration for each environment. Looking something like this:
|
||||
|
||||
```ts
|
||||
export function createProviderAProvider(config: AuthProviderConfig) {
|
||||
return new ProviderAAuthProvider(config);
|
||||
}
|
||||
export const createProviderAProvider: AuthProviderFactory = ({ config }) => {
|
||||
const a = config.getString('a');
|
||||
const b = config.getString('b');
|
||||
|
||||
return new ProviderAAuthProvider({ a, b });
|
||||
};
|
||||
```
|
||||
|
||||
#### Verify Callback
|
||||
@@ -144,7 +191,7 @@ export function createProviderAProvider(config: AuthProviderConfig) {
|
||||
> http://www.passportjs.org/docs/configure/
|
||||
|
||||
**`plugins/auth-backend/src/providers/providerA/index.ts`** is simply
|
||||
re-exporting the create method to be used for hooking the provider up to the
|
||||
re-exporting the factory function to be used for hooking the provider up to the
|
||||
backend.
|
||||
|
||||
```ts
|
||||
@@ -153,26 +200,14 @@ export { createProviderAProvider } from './provider';
|
||||
|
||||
### Hook it up to the backend
|
||||
|
||||
**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be
|
||||
configured properly so you need to add it to the list of configured providers,
|
||||
all of which implement [AuthProviderConfig](#AuthProviderConfig):
|
||||
|
||||
```ts
|
||||
export const providers = [
|
||||
{
|
||||
provider: 'providerA', # used as an identifier
|
||||
options: { ... }, # consult the provider documentation for which options you should provide
|
||||
disableRefresh: true # if the provider lacks refresh tokens
|
||||
},
|
||||
```
|
||||
|
||||
**`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend`
|
||||
starts it sets up routing for all the available providers by calling
|
||||
`createAuthProviderRouter` on each provider. You need to import the create
|
||||
method from the provider and add it to the factory:
|
||||
`createAuthProviderRouter` on each provider. You need to import the factory
|
||||
function from the provider and add it to the factory:
|
||||
|
||||
```ts
|
||||
import { createProviderAProvider } from './providerA';
|
||||
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
providerA: createProviderAProvider,
|
||||
};
|
||||
@@ -203,10 +238,21 @@ web browser and you should be able to trigger the authorization flow.
|
||||
|
||||
```ts
|
||||
export interface OAuthProviderHandlers {
|
||||
start(req: express.Request, options: any): Promise<any>;
|
||||
handler(req: express.Request): Promise<any>;
|
||||
refresh?(refreshToken: string, scope: string): Promise<any>;
|
||||
logout?(): Promise<any>;
|
||||
start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo>;
|
||||
handler(
|
||||
req: express.Request,
|
||||
): Promise<{
|
||||
response: AuthResponse<OAuthProviderInfo>;
|
||||
refreshToken?: string;
|
||||
}>;
|
||||
refresh?(
|
||||
refreshToken: string,
|
||||
scope: string,
|
||||
): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
logout?(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -221,12 +267,17 @@ export interface AuthProviderRouteHandlers {
|
||||
}
|
||||
```
|
||||
|
||||
##### AuthProviderConfig
|
||||
##### AuthProviderFactory
|
||||
|
||||
```ts
|
||||
export type AuthProviderConfig = {
|
||||
provider: string;
|
||||
options: any;
|
||||
disableRefresh?: boolean;
|
||||
export type AuthProviderFactoryOptions = {
|
||||
globalConfig: AuthProviderConfig;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
tokenIssuer: TokenIssuer;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
options: AuthProviderFactoryOptions,
|
||||
) => AuthProviderRouteHandlers;
|
||||
```
|
||||
|
||||
@@ -26,7 +26,7 @@ refer to the type documentation under
|
||||
`plugins/auth-backend/src/providers/types.ts`.
|
||||
|
||||
There are currently two different classes for two authentication mechanisms that
|
||||
implement this interface: an `OAuthProvider` for [OAuth](https://oauth.net/2/)
|
||||
implement this interface: an `OAuthAdapter` for [OAuth](https://oauth.net/2/)
|
||||
based mechanisms and a `SAMLAuthProvider` for
|
||||
[SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html)
|
||||
based mechanisms.
|
||||
@@ -50,11 +50,12 @@ OAuth2) providers, you can configure them by setting the right variables in
|
||||
Each authentication provider (except SAML) needs five parameters: an OAuth
|
||||
client ID, a client secret, an authorization endpoint and a token endpoint, and
|
||||
an app origin. The app origin is the URL at which the frontend of the
|
||||
application is hosted. This is required because the application opens a popup
|
||||
window to perform the authentication, and once the flow is completed, the popup
|
||||
window sends a `postMessage` to the frontend application to indicate the result
|
||||
of the operation. Also this URL is used to verify that authentication requests
|
||||
are coming from only this endpoint.
|
||||
application is hosted, and it is read from the `app.baseUrl` config. This is
|
||||
required because the application opens a popup window to perform the
|
||||
authentication, and once the flow is completed, the popup window sends a
|
||||
`postMessage` to the frontend application to indicate the result of the
|
||||
operation. Also this URL is used to verify that authentication requests are
|
||||
coming from only this endpoint.
|
||||
|
||||
These values are configured via the `app-config.yaml` present in the root of
|
||||
your app folder.
|
||||
@@ -64,8 +65,6 @@ auth:
|
||||
providers:
|
||||
google:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_GOOGLE_CLIENT_ID
|
||||
@@ -74,8 +73,6 @@ auth:
|
||||
env: AUTH_GOOGLE_CLIENT_SECRET
|
||||
github:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_GITHUB_CLIENT_ID
|
||||
@@ -87,8 +84,6 @@ auth:
|
||||
env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
|
||||
gitlab:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
...
|
||||
@@ -96,24 +91,34 @@ auth:
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### EnvironmentHandler
|
||||
### OAuthEnvironmentHandler
|
||||
|
||||
The concept of an "env" is core to the way the auth backend works. It uses an
|
||||
`env` query parameter to identify the environment in which the application is
|
||||
running (`development`, `staging`, `production`, etc). Each runtime can support
|
||||
multiple environments at the same time and the right handler for each request is
|
||||
identified and dispatched to based on the `env` parameter. All
|
||||
`AuthProviderRouteHandlers` are wrapped within an `EnvironmentHandler`.
|
||||
`AuthProviderRouteHandlers` are wrapped within an `OAuthEnvironmentHandler`.
|
||||
|
||||
An `EnvironmentHandler` takes an ID for each provider that it wraps, the
|
||||
handlers for each env the provider is supported in, and a function that, given a
|
||||
`Request` as argument, can extract the information about the env under which it
|
||||
should be processed.
|
||||
To instantiate multiple OAuth providers for different environments, use
|
||||
`OAuthEnvironmentHandler.mapConfig`. It's a helper to iterate over a
|
||||
configuration object that is a map of environment to configurations. See one of
|
||||
the existing OAuth providers for an example of how it is used.
|
||||
|
||||
Each provider exposes a factory function `createXProvider` (where X is the name
|
||||
of the provider) that takes the global config, env and other parameters and
|
||||
returns an `AuthProviderRouteHandlers` for each env, and an `envIdentifier`
|
||||
function to identify the env of a request.
|
||||
Given the following configuration:
|
||||
|
||||
```yaml
|
||||
development:
|
||||
clientId: abc
|
||||
clientSecret: secret
|
||||
production:
|
||||
clientId: xyz
|
||||
clientSecret: supersecret
|
||||
```
|
||||
|
||||
The `OAuthEnvironmentHandler.mapConfig(config, envConfig => ...)` call will
|
||||
split the `config` by the top level `development` and `production` keys, and
|
||||
pass on each block as `envConfig`.
|
||||
|
||||
For a list of currently available providers, look in the `factories` module
|
||||
located in `plugins/auth-backend/src/providers/factories.ts`
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers';
|
||||
import { WebMessageResponse } from './types';
|
||||
|
||||
describe('oauth helpers', () => {
|
||||
describe('postMessageResponse', () => {
|
||||
const appOrigin = 'http://localhost:3000';
|
||||
it('should post a message back with payload success', () => {
|
||||
const mockResponse = ({
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
const data: WebMessageResponse = {
|
||||
type: 'authorization_response',
|
||||
response: {
|
||||
providerInfo: {
|
||||
accessToken: 'ACCESS_TOKEN',
|
||||
idToken: 'ID_TOKEN',
|
||||
expiresInSeconds: 10,
|
||||
scope: 'email',
|
||||
},
|
||||
profile: {
|
||||
email: 'foo@bar.com',
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'a',
|
||||
idToken: 'a.b.c',
|
||||
},
|
||||
},
|
||||
};
|
||||
const jsonData = JSON.stringify(data);
|
||||
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
|
||||
|
||||
postMessageResponse(mockResponse, appOrigin, data);
|
||||
expect(mockResponse.setHeader).toBeCalledTimes(3);
|
||||
expect(mockResponse.end).toBeCalledTimes(1);
|
||||
expect(mockResponse.end).toBeCalledWith(
|
||||
expect.stringContaining(base64Data),
|
||||
);
|
||||
});
|
||||
|
||||
it('should post a message back with payload error', () => {
|
||||
const mockResponse = ({
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
const data: WebMessageResponse = {
|
||||
type: 'authorization_response',
|
||||
error: new Error('Unknown error occured'),
|
||||
};
|
||||
const jsonData = JSON.stringify(data);
|
||||
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
|
||||
|
||||
postMessageResponse(mockResponse, appOrigin, data);
|
||||
expect(mockResponse.setHeader).toBeCalledTimes(3);
|
||||
expect(mockResponse.end).toBeCalledTimes(1);
|
||||
expect(mockResponse.end).toBeCalledWith(
|
||||
expect.stringContaining(base64Data),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensuresXRequestedWith', () => {
|
||||
it('should return false if no header present', () => {
|
||||
const mockRequest = ({
|
||||
header: () => jest.fn(),
|
||||
} as unknown) as express.Request;
|
||||
expect(ensuresXRequestedWith(mockRequest)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if header present with incorrect value', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'INVALID',
|
||||
} as unknown) as express.Request;
|
||||
expect(ensuresXRequestedWith(mockRequest)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if header present with correct value', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
} as unknown) as express.Request;
|
||||
expect(ensuresXRequestedWith(mockRequest)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 crypto from 'crypto';
|
||||
import { WebMessageResponse } from './types';
|
||||
|
||||
export const postMessageResponse = (
|
||||
res: express.Response,
|
||||
appOrigin: string,
|
||||
response: WebMessageResponse,
|
||||
) => {
|
||||
const jsonData = JSON.stringify(response);
|
||||
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.setHeader('X-Frame-Options', 'sameorigin');
|
||||
|
||||
// TODO: Make target app origin configurable globally
|
||||
const script = `
|
||||
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}')
|
||||
window.close()
|
||||
`;
|
||||
const hash = crypto.createHash('sha256').update(script).digest('base64');
|
||||
res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`);
|
||||
|
||||
res.end(`
|
||||
<html>
|
||||
<body>
|
||||
<script>${script}</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
};
|
||||
|
||||
export const ensuresXRequestedWith = (req: express.Request) => {
|
||||
const requiredHeader = req.header('X-Requested-With');
|
||||
|
||||
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers';
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { AuthResponse } from '../../providers/types';
|
||||
|
||||
/**
|
||||
* Payload sent as a post message after the auth request is complete.
|
||||
* If successful then has a valid payload with Auth information else contains an error.
|
||||
*/
|
||||
export type WebMessageResponse =
|
||||
| {
|
||||
type: 'authorization_response';
|
||||
response: AuthResponse<unknown>;
|
||||
}
|
||||
| {
|
||||
type: 'authorization_response';
|
||||
error: Error;
|
||||
};
|
||||
+11
-159
@@ -15,16 +15,9 @@
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import {
|
||||
ensuresXRequestedWith,
|
||||
postMessageResponse,
|
||||
THOUSAND_DAYS_MS,
|
||||
TEN_MINUTES_MS,
|
||||
verifyNonce,
|
||||
encodeState,
|
||||
OAuthProvider,
|
||||
} from './OAuthProvider';
|
||||
import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types';
|
||||
import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter';
|
||||
import { encodeState } from './helpers';
|
||||
import { OAuthHandlers } from './types';
|
||||
|
||||
const mockResponseData = {
|
||||
providerInfo: {
|
||||
@@ -41,149 +34,8 @@ const mockResponseData = {
|
||||
},
|
||||
};
|
||||
|
||||
describe('OAuthProvider Utils', () => {
|
||||
describe('verifyNonce', () => {
|
||||
it('should throw error if cookie nonce missing', () => {
|
||||
const state = { nonce: 'NONCE', env: 'development' };
|
||||
const mockRequest = ({
|
||||
cookies: {},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
expect(() => {
|
||||
verifyNonce(mockRequest, 'providera');
|
||||
}).toThrowError('Auth response is missing cookie nonce');
|
||||
});
|
||||
|
||||
it('should throw error if state nonce missing', () => {
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
'providera-nonce': 'NONCE',
|
||||
},
|
||||
query: {},
|
||||
} as unknown) as express.Request;
|
||||
expect(() => {
|
||||
verifyNonce(mockRequest, 'providera');
|
||||
}).toThrowError('Invalid state passed via request');
|
||||
});
|
||||
|
||||
it('should throw error if nonce mismatch', () => {
|
||||
const state = { nonce: 'NONCEB', env: 'development' };
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
'providera-nonce': 'NONCEA',
|
||||
},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
expect(() => {
|
||||
verifyNonce(mockRequest, 'providera');
|
||||
}).toThrowError('Invalid nonce');
|
||||
});
|
||||
|
||||
it('should not throw any error if nonce matches', () => {
|
||||
const state = { nonce: 'NONCE', env: 'development' };
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
'providera-nonce': 'NONCE',
|
||||
},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
expect(() => {
|
||||
verifyNonce(mockRequest, 'providera');
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('postMessageResponse', () => {
|
||||
const appOrigin = 'http://localhost:3000';
|
||||
it('should post a message back with payload success', () => {
|
||||
const mockResponse = ({
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
const data: WebMessageResponse = {
|
||||
type: 'authorization_response',
|
||||
response: {
|
||||
providerInfo: {
|
||||
accessToken: 'ACCESS_TOKEN',
|
||||
idToken: 'ID_TOKEN',
|
||||
expiresInSeconds: 10,
|
||||
scope: 'email',
|
||||
},
|
||||
profile: {
|
||||
email: 'foo@bar.com',
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'a',
|
||||
idToken: 'a.b.c',
|
||||
},
|
||||
},
|
||||
};
|
||||
const jsonData = JSON.stringify(data);
|
||||
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
|
||||
|
||||
postMessageResponse(mockResponse, appOrigin, data);
|
||||
expect(mockResponse.setHeader).toBeCalledTimes(3);
|
||||
expect(mockResponse.end).toBeCalledTimes(1);
|
||||
expect(mockResponse.end).toBeCalledWith(
|
||||
expect.stringContaining(base64Data),
|
||||
);
|
||||
});
|
||||
|
||||
it('should post a message back with payload error', () => {
|
||||
const mockResponse = ({
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
const data: WebMessageResponse = {
|
||||
type: 'authorization_response',
|
||||
error: new Error('Unknown error occured'),
|
||||
};
|
||||
const jsonData = JSON.stringify(data);
|
||||
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
|
||||
|
||||
postMessageResponse(mockResponse, appOrigin, data);
|
||||
expect(mockResponse.setHeader).toBeCalledTimes(3);
|
||||
expect(mockResponse.end).toBeCalledTimes(1);
|
||||
expect(mockResponse.end).toBeCalledWith(
|
||||
expect.stringContaining(base64Data),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensuresXRequestedWith', () => {
|
||||
it('should return false if no header present', () => {
|
||||
const mockRequest = ({
|
||||
header: () => jest.fn(),
|
||||
} as unknown) as express.Request;
|
||||
expect(ensuresXRequestedWith(mockRequest)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if header present with incorrect value', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'INVALID',
|
||||
} as unknown) as express.Request;
|
||||
expect(ensuresXRequestedWith(mockRequest)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if header present with correct value', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
} as unknown) as express.Request;
|
||||
expect(ensuresXRequestedWith(mockRequest)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuthProvider', () => {
|
||||
class MyAuthProvider implements OAuthProviderHandlers {
|
||||
describe('OAuthAdapter', () => {
|
||||
class MyAuthProvider implements OAuthHandlers {
|
||||
async start() {
|
||||
return {
|
||||
url: '/url',
|
||||
@@ -215,7 +67,7 @@ describe('OAuthProvider', () => {
|
||||
};
|
||||
|
||||
it('sets the correct headers in start', async () => {
|
||||
const oauthProvider = new OAuthProvider(
|
||||
const oauthProvider = new OAuthAdapter(
|
||||
providerInstance,
|
||||
oAuthProviderOptions,
|
||||
);
|
||||
@@ -250,7 +102,7 @@ describe('OAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('sets the refresh cookie if refresh is enabled', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: false,
|
||||
});
|
||||
@@ -284,7 +136,7 @@ describe('OAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('does not set the refresh cookie if refresh is disabled', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: true,
|
||||
});
|
||||
@@ -309,7 +161,7 @@ describe('OAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('removes refresh cookie when logging out', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: false,
|
||||
});
|
||||
@@ -334,7 +186,7 @@ describe('OAuthProvider', () => {
|
||||
|
||||
it('gets new access-token when refreshing', async () => {
|
||||
oAuthProviderOptions.disableRefresh = false;
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: false,
|
||||
});
|
||||
@@ -363,7 +215,7 @@ describe('OAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('handles refresh without capabilities', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: true,
|
||||
});
|
||||
+14
-111
@@ -19,14 +19,14 @@ import crypto from 'crypto';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
AuthProviderRouteHandlers,
|
||||
OAuthProviderHandlers,
|
||||
WebMessageResponse,
|
||||
BackstageIdentity,
|
||||
OAuthState,
|
||||
AuthProviderConfig,
|
||||
} from '../providers/types';
|
||||
} from '../../providers/types';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { TokenIssuer } from '../identity';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { verifyNonce, encodeState } from './helpers';
|
||||
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
|
||||
import { OAuthHandlers } from './types';
|
||||
|
||||
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
|
||||
export const TEN_MINUTES_MS = 600 * 1000;
|
||||
@@ -42,99 +42,20 @@ export type Options = {
|
||||
tokenIssuer: TokenIssuer;
|
||||
};
|
||||
|
||||
const readState = (stateString: string): OAuthState => {
|
||||
const state = Object.fromEntries(
|
||||
new URLSearchParams(decodeURIComponent(stateString)),
|
||||
);
|
||||
if (
|
||||
!state.nonce ||
|
||||
!state.env ||
|
||||
state.nonce?.length === 0 ||
|
||||
state.env?.length === 0
|
||||
) {
|
||||
throw Error(`Invalid state passed via request`);
|
||||
}
|
||||
return {
|
||||
nonce: state.nonce,
|
||||
env: state.env,
|
||||
};
|
||||
};
|
||||
|
||||
export const encodeState = (state: OAuthState): string => {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('nonce', state.nonce);
|
||||
searchParams.append('env', state.env);
|
||||
|
||||
return encodeURIComponent(searchParams.toString());
|
||||
};
|
||||
|
||||
export const verifyNonce = (req: express.Request, providerId: string) => {
|
||||
const cookieNonce = req.cookies[`${providerId}-nonce`];
|
||||
const state: OAuthState = readState(req.query.state?.toString() ?? '');
|
||||
const stateNonce = state.nonce;
|
||||
|
||||
if (!cookieNonce) {
|
||||
throw new Error('Auth response is missing cookie nonce');
|
||||
}
|
||||
if (stateNonce.length === 0) {
|
||||
throw new Error('Auth response is missing state nonce');
|
||||
}
|
||||
if (cookieNonce !== stateNonce) {
|
||||
throw new Error('Invalid nonce');
|
||||
}
|
||||
};
|
||||
|
||||
export const postMessageResponse = (
|
||||
res: express.Response,
|
||||
appOrigin: string,
|
||||
response: WebMessageResponse,
|
||||
) => {
|
||||
const jsonData = JSON.stringify(response);
|
||||
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.setHeader('X-Frame-Options', 'sameorigin');
|
||||
|
||||
// TODO: Make target app origin configurable globally
|
||||
const script = `
|
||||
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}')
|
||||
window.close()
|
||||
`;
|
||||
const hash = crypto.createHash('sha256').update(script).digest('base64');
|
||||
res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`);
|
||||
|
||||
res.end(`
|
||||
<html>
|
||||
<body>
|
||||
<script>${script}</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
};
|
||||
|
||||
export const ensuresXRequestedWith = (req: express.Request) => {
|
||||
const requiredHeader = req.header('X-Requested-With');
|
||||
|
||||
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
static fromConfig(
|
||||
config: AuthProviderConfig,
|
||||
providerHandlers: OAuthProviderHandlers,
|
||||
handlers: OAuthHandlers,
|
||||
options: Pick<
|
||||
Options,
|
||||
'providerId' | 'persistScopes' | 'disableRefresh' | 'tokenIssuer'
|
||||
>,
|
||||
): OAuthProvider {
|
||||
): OAuthAdapter {
|
||||
const { origin: appOrigin } = new URL(config.appUrl);
|
||||
const secure = config.baseUrl.startsWith('https://');
|
||||
const url = new URL(config.baseUrl);
|
||||
const cookiePath = `${url.pathname}/${options.providerId}`;
|
||||
return new OAuthProvider(providerHandlers, {
|
||||
return new OAuthAdapter(handlers, {
|
||||
...options,
|
||||
appOrigin,
|
||||
cookieDomain: url.hostname,
|
||||
@@ -144,7 +65,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly providerHandlers: OAuthProviderHandlers,
|
||||
private readonly handlers: OAuthHandlers,
|
||||
private readonly options: Options,
|
||||
) {}
|
||||
|
||||
@@ -173,10 +94,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
state: stateParameter,
|
||||
};
|
||||
|
||||
const { url, status } = await this.providerHandlers.start(
|
||||
req,
|
||||
queryParameters,
|
||||
);
|
||||
const { url, status } = await this.handlers.start(req, queryParameters);
|
||||
|
||||
res.statusCode = status || 302;
|
||||
res.setHeader('Location', url);
|
||||
@@ -192,9 +110,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
// verify nonce cookie and state cookie on callback
|
||||
verifyNonce(req, this.options.providerId);
|
||||
|
||||
const { response, refreshToken } = await this.providerHandlers.handler(
|
||||
req,
|
||||
);
|
||||
const { response, refreshToken } = await this.handlers.handler(req);
|
||||
|
||||
if (this.options.persistScopes) {
|
||||
const grantedScopes = this.getScopesFromCookie(
|
||||
@@ -251,7 +167,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.providerHandlers.refresh || this.options.disableRefresh) {
|
||||
if (!this.handlers.refresh || this.options.disableRefresh) {
|
||||
res.send(
|
||||
`Refresh token not supported for provider: ${this.options.providerId}`,
|
||||
);
|
||||
@@ -270,7 +186,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
const scope = req.query.scope?.toString() ?? '';
|
||||
|
||||
// get new access_token
|
||||
const response = await this.providerHandlers.refresh(refreshToken, scope);
|
||||
const response = await this.handlers.refresh(refreshToken, scope);
|
||||
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
@@ -287,19 +203,6 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
identifyEnv(req: express.Request): string | undefined {
|
||||
const reqEnv = req.query.env?.toString();
|
||||
if (reqEnv) {
|
||||
return reqEnv;
|
||||
}
|
||||
const stateParams = req.query.state?.toString();
|
||||
if (!stateParams) {
|
||||
return undefined;
|
||||
}
|
||||
const env = readState(stateParams).env;
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the response from the OAuth provider includes a Backstage identity, we
|
||||
* make sure it's populated with all the information we can derive from the user ID.
|
||||
+55
-33
@@ -15,46 +15,32 @@
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import {
|
||||
AuthProviderRouteHandlers,
|
||||
EnvironmentIdentifierFn,
|
||||
} from '../providers/types';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { readState } from './helpers';
|
||||
import { AuthProviderRouteHandlers } from '../../providers/types';
|
||||
|
||||
export type EnvironmentHandlers = {
|
||||
[key: string]: AuthProviderRouteHandlers;
|
||||
};
|
||||
export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
|
||||
static mapConfig(
|
||||
config: Config,
|
||||
factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers,
|
||||
) {
|
||||
const envs = config.keys();
|
||||
const handlers = new Map<string, AuthProviderRouteHandlers>();
|
||||
|
||||
export class EnvironmentHandler implements AuthProviderRouteHandlers {
|
||||
constructor(
|
||||
private readonly providerId: string,
|
||||
private readonly providers: EnvironmentHandlers,
|
||||
private readonly envIdentifier: EnvironmentIdentifierFn,
|
||||
) {}
|
||||
|
||||
private getProviderForEnv(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
): AuthProviderRouteHandlers | undefined {
|
||||
const env: string | undefined = this.envIdentifier(req);
|
||||
|
||||
if (!env) {
|
||||
throw new InputError(`Must specify 'env' query to select environment`);
|
||||
for (const env of envs) {
|
||||
const envConfig = config.getConfig(env);
|
||||
const handler = factoryFunc(envConfig);
|
||||
handlers.set(env, handler);
|
||||
}
|
||||
|
||||
if (this.providers.hasOwnProperty(env)) {
|
||||
return this.providers[env];
|
||||
}
|
||||
|
||||
res.status(404).send(
|
||||
`Missing configuration.
|
||||
<br>
|
||||
<br>
|
||||
For this flow to work you need to supply a valid configuration for the "${env}" environment of the "${this.providerId}" provider.`,
|
||||
);
|
||||
return undefined;
|
||||
return new OAuthEnvironmentHandler(handlers);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly handlers: Map<string, AuthProviderRouteHandlers>,
|
||||
) {}
|
||||
|
||||
async start(req: express.Request, res: express.Response): Promise<void> {
|
||||
const provider = this.getProviderForEnv(req, res);
|
||||
await provider?.start(req, res);
|
||||
@@ -77,4 +63,40 @@ For this flow to work you need to supply a valid configuration for the "${env}"
|
||||
const provider = this.getProviderForEnv(req, res);
|
||||
await provider?.logout?.(req, res);
|
||||
}
|
||||
|
||||
private getRequestFromEnv(req: express.Request): string | undefined {
|
||||
const reqEnv = req.query.env?.toString();
|
||||
if (reqEnv) {
|
||||
return reqEnv;
|
||||
}
|
||||
const stateParams = req.query.state?.toString();
|
||||
if (!stateParams) {
|
||||
return undefined;
|
||||
}
|
||||
const env = readState(stateParams).env;
|
||||
return env;
|
||||
}
|
||||
|
||||
private getProviderForEnv(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
): AuthProviderRouteHandlers | undefined {
|
||||
const env: string | undefined = this.getRequestFromEnv(req);
|
||||
|
||||
if (!env) {
|
||||
throw new InputError(`Must specify 'env' query to select environment`);
|
||||
}
|
||||
|
||||
if (!this.handlers.has(env)) {
|
||||
res.status(404).send(
|
||||
`Missing configuration.
|
||||
<br>
|
||||
<br>
|
||||
For this flow to work you need to supply a valid configuration for the "${env}" environment of provider.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.handlers.get(env);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { verifyNonce, encodeState } from './helpers';
|
||||
|
||||
describe('OAuthProvider Utils', () => {
|
||||
describe('verifyNonce', () => {
|
||||
it('should throw error if cookie nonce missing', () => {
|
||||
const state = { nonce: 'NONCE', env: 'development' };
|
||||
const mockRequest = ({
|
||||
cookies: {},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
expect(() => {
|
||||
verifyNonce(mockRequest, 'providera');
|
||||
}).toThrowError('Auth response is missing cookie nonce');
|
||||
});
|
||||
|
||||
it('should throw error if state nonce missing', () => {
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
'providera-nonce': 'NONCE',
|
||||
},
|
||||
query: {},
|
||||
} as unknown) as express.Request;
|
||||
expect(() => {
|
||||
verifyNonce(mockRequest, 'providera');
|
||||
}).toThrowError('Invalid state passed via request');
|
||||
});
|
||||
|
||||
it('should throw error if nonce mismatch', () => {
|
||||
const state = { nonce: 'NONCEB', env: 'development' };
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
'providera-nonce': 'NONCEA',
|
||||
},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
expect(() => {
|
||||
verifyNonce(mockRequest, 'providera');
|
||||
}).toThrowError('Invalid nonce');
|
||||
});
|
||||
|
||||
it('should not throw any error if nonce matches', () => {
|
||||
const state = { nonce: 'NONCE', env: 'development' };
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
'providera-nonce': 'NONCE',
|
||||
},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
expect(() => {
|
||||
verifyNonce(mockRequest, 'providera');
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { OAuthState } from './types';
|
||||
|
||||
export const readState = (stateString: string): OAuthState => {
|
||||
const state = Object.fromEntries(
|
||||
new URLSearchParams(decodeURIComponent(stateString)),
|
||||
);
|
||||
if (
|
||||
!state.nonce ||
|
||||
!state.env ||
|
||||
state.nonce?.length === 0 ||
|
||||
state.env?.length === 0
|
||||
) {
|
||||
throw Error(`Invalid state passed via request`);
|
||||
}
|
||||
return {
|
||||
nonce: state.nonce,
|
||||
env: state.env,
|
||||
};
|
||||
};
|
||||
|
||||
export const encodeState = (state: OAuthState): string => {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('nonce', state.nonce);
|
||||
searchParams.append('env', state.env);
|
||||
|
||||
return encodeURIComponent(searchParams.toString());
|
||||
};
|
||||
|
||||
export const verifyNonce = (req: express.Request, providerId: string) => {
|
||||
const cookieNonce = req.cookies[`${providerId}-nonce`];
|
||||
const state: OAuthState = readState(req.query.state?.toString() ?? '');
|
||||
const stateNonce = state.nonce;
|
||||
|
||||
if (!cookieNonce) {
|
||||
throw new Error('Auth response is missing cookie nonce');
|
||||
}
|
||||
if (stateNonce.length === 0) {
|
||||
throw new Error('Auth response is missing state nonce');
|
||||
}
|
||||
if (cookieNonce !== stateNonce) {
|
||||
throw new Error('Invalid nonce');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler';
|
||||
export { OAuthAdapter } from './OAuthAdapter';
|
||||
export type {
|
||||
OAuthHandlers,
|
||||
OAuthProviderInfo,
|
||||
OAuthProviderOptions,
|
||||
OAuthResponse,
|
||||
OAuthState,
|
||||
} from './types';
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { AuthResponse, RedirectInfo } from '../../providers/types';
|
||||
|
||||
/**
|
||||
* Common options for passport.js-based OAuth providers
|
||||
*/
|
||||
export type OAuthProviderOptions = {
|
||||
/**
|
||||
* Client ID of the auth provider.
|
||||
*/
|
||||
clientId: string;
|
||||
/**
|
||||
* Client Secret of the auth provider.
|
||||
*/
|
||||
clientSecret: string;
|
||||
/**
|
||||
* Callback URL to be passed to the auth provider to redirect to after the user signs in.
|
||||
*/
|
||||
callbackUrl: string;
|
||||
};
|
||||
|
||||
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
|
||||
|
||||
export type OAuthProviderInfo = {
|
||||
/**
|
||||
* An access token issued for the signed in user.
|
||||
*/
|
||||
accessToken: string;
|
||||
/**
|
||||
* (Optional) Id token issued for the signed in user.
|
||||
*/
|
||||
idToken?: string;
|
||||
/**
|
||||
* Expiry of the access token in seconds.
|
||||
*/
|
||||
expiresInSeconds?: number;
|
||||
/**
|
||||
* Scopes granted for the access token.
|
||||
*/
|
||||
scope: string;
|
||||
/**
|
||||
* A refresh token issued for the signed in user
|
||||
*/
|
||||
refreshToken?: string;
|
||||
};
|
||||
|
||||
export type OAuthState = {
|
||||
/* A type for the serialized value in the `state` parameter of the OAuth authorization flow
|
||||
*/
|
||||
nonce: string;
|
||||
env: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Any OAuth provider needs to implement this interface which has provider specific
|
||||
* handlers for different methods to perform authentication, get access tokens,
|
||||
* refresh tokens and perform sign out.
|
||||
*/
|
||||
export interface OAuthHandlers {
|
||||
/**
|
||||
* This method initiates a sign in request with an auth provider.
|
||||
* @param {express.Request} req
|
||||
* @param options
|
||||
*/
|
||||
start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo>;
|
||||
|
||||
/**
|
||||
* Handles the redirect from the auth provider when the user has signed in.
|
||||
* @param {express.Request} req
|
||||
*/
|
||||
handler(
|
||||
req: express.Request,
|
||||
): Promise<{
|
||||
response: AuthResponse<OAuthProviderInfo>;
|
||||
refreshToken?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* (Optional) Given a refresh token and scope fetches a new access token from the auth provider.
|
||||
* @param {string} refreshToken
|
||||
* @param {string} scope
|
||||
*/
|
||||
refresh?(
|
||||
refreshToken: string,
|
||||
scope: string,
|
||||
): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
|
||||
/**
|
||||
* (Optional) Sign out of the auth provider.
|
||||
*/
|
||||
logout?(): Promise<void>;
|
||||
}
|
||||
+23
-6
@@ -17,12 +17,13 @@
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import jwtDecoder from 'jwt-decode';
|
||||
import {
|
||||
RedirectInfo,
|
||||
RefreshTokenResponse,
|
||||
ProfileInfo,
|
||||
ProviderStrategy,
|
||||
} from '../providers/types';
|
||||
import { ProfileInfo, RedirectInfo } from '../../providers/types';
|
||||
|
||||
export type PassportDoneCallback<Res, Private = never> = (
|
||||
err?: Error,
|
||||
response?: Res,
|
||||
privateInfo?: Private,
|
||||
) => void;
|
||||
|
||||
export const makeProfileInfo = (
|
||||
profile: passport.Profile,
|
||||
@@ -106,6 +107,18 @@ export const executeFrameHandlerStrategy = async <T, PrivateInfo = never>(
|
||||
);
|
||||
};
|
||||
|
||||
type RefreshTokenResponse = {
|
||||
/**
|
||||
* An access token issued for the signed in user.
|
||||
*/
|
||||
accessToken: string;
|
||||
/**
|
||||
* Optionally, the server can issue a new Refresh Token for the user
|
||||
*/
|
||||
refreshToken?: string;
|
||||
params: any;
|
||||
};
|
||||
|
||||
export const executeRefreshTokenStrategy = async (
|
||||
providerStrategy: passport.Strategy,
|
||||
refreshToken: string,
|
||||
@@ -156,6 +169,10 @@ export const executeRefreshTokenStrategy = async (
|
||||
});
|
||||
};
|
||||
|
||||
type ProviderStrategy = {
|
||||
userProfile(accessToken: string, callback: Function): void;
|
||||
};
|
||||
|
||||
export const executeFetchUserProfileStrategy = async (
|
||||
providerStrategy: passport.Strategy,
|
||||
accessToken: string,
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
executeFetchUserProfileStrategy,
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
} from './PassportStrategyHelper';
|
||||
export type { PassportDoneCallback } from './PassportStrategyHelper';
|
||||
@@ -17,25 +17,22 @@
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import Auth0Strategy from './strategy';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
} from '../../lib/oauth';
|
||||
import {
|
||||
executeFetchUserProfileStrategy,
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
OAuthProviderHandlers,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
RedirectInfo,
|
||||
OAuthProviderOptions,
|
||||
} from '../types';
|
||||
import { Config } from '@backstage/config';
|
||||
} from '../../lib/passport';
|
||||
import { RedirectInfo, AuthProviderFactory } from '../types';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -45,7 +42,7 @@ export type Auth0AuthProviderOptions = OAuthProviderOptions & {
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export class Auth0AuthProvider implements OAuthProviderHandlers {
|
||||
export class Auth0AuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: Auth0Strategy;
|
||||
|
||||
constructor(options: Auth0AuthProviderOptions) {
|
||||
@@ -151,29 +148,28 @@ export class Auth0AuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createAuth0Provider(
|
||||
config: AuthProviderConfig,
|
||||
_: string,
|
||||
envConfig: Config,
|
||||
_logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'auth0';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const domain = envConfig.getString('domain');
|
||||
const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`;
|
||||
export const createAuth0Provider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'auth0';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const domain = envConfig.getString('domain');
|
||||
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
|
||||
const provider = new Auth0AuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
domain,
|
||||
});
|
||||
const provider = new Auth0AuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
domain,
|
||||
});
|
||||
|
||||
return OAuthProvider.fromConfig(config, provider, {
|
||||
disableRefresh: true,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: true,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,16 +25,8 @@ import { createOktaProvider } from './okta';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { createAuth0Provider } from './auth0';
|
||||
import { createMicrosoftProvider } from './microsoft';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
AuthProviderFactory,
|
||||
EnvironmentIdentifierFn,
|
||||
} from './types';
|
||||
import { AuthProviderConfig, AuthProviderFactory } from './types';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../lib/EnvironmentHandler';
|
||||
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
@@ -50,9 +42,9 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
export const createAuthProviderRouter = (
|
||||
providerId: string,
|
||||
globalConfig: AuthProviderConfig,
|
||||
providerConfig: Config,
|
||||
config: Config,
|
||||
logger: Logger,
|
||||
issuer: TokenIssuer,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) => {
|
||||
const factory = factories[providerId];
|
||||
if (!factory) {
|
||||
@@ -60,28 +52,8 @@ export const createAuthProviderRouter = (
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
const envs = providerConfig.keys();
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
let envIdentifier: EnvironmentIdentifierFn | undefined;
|
||||
|
||||
for (const env of envs) {
|
||||
const envConfig = providerConfig.getConfig(env);
|
||||
const provider = factory(globalConfig, env, envConfig, logger, issuer);
|
||||
if (provider) {
|
||||
envProviders[env] = provider;
|
||||
envIdentifier = provider.identifyEnv;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof envIdentifier === 'undefined') {
|
||||
throw Error(`No envIdentifier provided for '${providerId}'`);
|
||||
}
|
||||
|
||||
const handler = new EnvironmentHandler(
|
||||
providerId,
|
||||
envProviders,
|
||||
envIdentifier,
|
||||
);
|
||||
const handler = factory({ globalConfig, config, logger, tokenIssuer });
|
||||
|
||||
router.get('/start', handler.start.bind(handler));
|
||||
router.get('/handler/frame', handler.frameHandler.bind(handler));
|
||||
|
||||
@@ -20,20 +20,17 @@ import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
makeProfileInfo,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
OAuthProviderHandlers,
|
||||
AuthProviderConfig,
|
||||
RedirectInfo,
|
||||
OAuthProviderOptions,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
} from '../../lib/passport';
|
||||
import { RedirectInfo, AuthProviderFactory } from '../types';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export type GithubAuthProviderOptions = OAuthProviderOptions & {
|
||||
tokenUrl?: string;
|
||||
@@ -41,7 +38,7 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & {
|
||||
authorizationUrl?: string;
|
||||
};
|
||||
|
||||
export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
export class GithubAuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: GithubStrategy;
|
||||
|
||||
static transformPassportProfile(rawProfile: any): passport.Profile {
|
||||
@@ -137,43 +134,42 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createGithubProvider(
|
||||
config: AuthProviderConfig,
|
||||
_: string,
|
||||
envConfig: Config,
|
||||
_logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'github';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const enterpriseInstanceUrl = envConfig.getOptionalString(
|
||||
'enterpriseInstanceUrl',
|
||||
);
|
||||
const authorizationUrl = enterpriseInstanceUrl
|
||||
? `${enterpriseInstanceUrl}/login/oauth/authorize`
|
||||
: undefined;
|
||||
const tokenUrl = enterpriseInstanceUrl
|
||||
? `${enterpriseInstanceUrl}/login/oauth/access_token`
|
||||
: undefined;
|
||||
const userProfileUrl = enterpriseInstanceUrl
|
||||
? `${enterpriseInstanceUrl}/api/v3/user`
|
||||
: undefined;
|
||||
const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`;
|
||||
export const createGithubProvider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'github';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const enterpriseInstanceUrl = envConfig.getOptionalString(
|
||||
'enterpriseInstanceUrl',
|
||||
);
|
||||
const authorizationUrl = enterpriseInstanceUrl
|
||||
? `${enterpriseInstanceUrl}/login/oauth/authorize`
|
||||
: undefined;
|
||||
const tokenUrl = enterpriseInstanceUrl
|
||||
? `${enterpriseInstanceUrl}/login/oauth/access_token`
|
||||
: undefined;
|
||||
const userProfileUrl = enterpriseInstanceUrl
|
||||
? `${enterpriseInstanceUrl}/api/v3/user`
|
||||
: undefined;
|
||||
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
|
||||
const provider = new GithubAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
tokenUrl,
|
||||
userProfileUrl,
|
||||
authorizationUrl,
|
||||
});
|
||||
const provider = new GithubAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
tokenUrl,
|
||||
userProfileUrl,
|
||||
authorizationUrl,
|
||||
});
|
||||
|
||||
return OAuthProvider.fromConfig(config, provider, {
|
||||
disableRefresh: true,
|
||||
persistScopes: true,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: true,
|
||||
persistScopes: true,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,26 +20,23 @@ import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
makeProfileInfo,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
OAuthProviderHandlers,
|
||||
AuthProviderConfig,
|
||||
RedirectInfo,
|
||||
OAuthProviderOptions,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
} from '../../lib/passport';
|
||||
import { RedirectInfo, AuthProviderFactory } from '../types';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export type GitlabAuthProviderOptions = OAuthProviderOptions & {
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
export class GitlabAuthProvider implements OAuthProviderHandlers {
|
||||
export class GitlabAuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: GitlabStrategy;
|
||||
|
||||
static transformPassportProfile(rawProfile: any): passport.Profile {
|
||||
@@ -140,30 +137,29 @@ export class GitlabAuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createGitlabProvider(
|
||||
config: AuthProviderConfig,
|
||||
_: string,
|
||||
envConfig: Config,
|
||||
_logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'gitlab';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const audience = envConfig.getString('audience');
|
||||
const baseUrl = audience || 'https://gitlab.com';
|
||||
const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`;
|
||||
export const createGitlabProvider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'gitlab';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const audience = envConfig.getString('audience');
|
||||
const baseUrl = audience || 'https://gitlab.com';
|
||||
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
|
||||
const provider = new GitlabAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
baseUrl,
|
||||
});
|
||||
const provider = new GitlabAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
baseUrl,
|
||||
});
|
||||
|
||||
return OAuthProvider.fromConfig(config, provider, {
|
||||
disableRefresh: true,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: true,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,26 +22,23 @@ import {
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
executeFetchUserProfileStrategy,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import { RedirectInfo, AuthProviderFactory } from '../types';
|
||||
import {
|
||||
OAuthProviderHandlers,
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
OAuthAdapter,
|
||||
OAuthHandlers,
|
||||
OAuthProviderOptions,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
OAuthEnvironmentHandler,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
export class GoogleAuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: GoogleStrategy;
|
||||
|
||||
constructor(options: OAuthProviderOptions) {
|
||||
@@ -148,27 +145,26 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createGoogleProvider(
|
||||
config: AuthProviderConfig,
|
||||
_: string,
|
||||
envConfig: Config,
|
||||
_logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'google';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`;
|
||||
export const createGoogleProvider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'google';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
|
||||
const provider = new GoogleAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
});
|
||||
const provider = new GoogleAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
});
|
||||
|
||||
return OAuthProvider.fromConfig(config, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,21 +24,18 @@ import {
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
executeFetchUserProfileStrategy,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
|
||||
import { RedirectInfo, AuthProviderFactory } from '../types';
|
||||
|
||||
import {
|
||||
OAuthProviderHandlers,
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
OAuthEnvironmentHandler,
|
||||
} from '../../lib/oauth';
|
||||
|
||||
import got from 'got';
|
||||
|
||||
@@ -51,7 +48,7 @@ export type MicrosoftAuthProviderOptions = OAuthProviderOptions & {
|
||||
tokenUrl?: string;
|
||||
};
|
||||
|
||||
export class MicrosoftAuthProvider implements OAuthProviderHandlers {
|
||||
export class MicrosoftAuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: MicrosoftStrategy;
|
||||
|
||||
static transformAuthResponse(
|
||||
@@ -205,34 +202,33 @@ export class MicrosoftAuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createMicrosoftProvider(
|
||||
config: AuthProviderConfig,
|
||||
_: string,
|
||||
envConfig: Config,
|
||||
_logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'microsoft';
|
||||
export const createMicrosoftProvider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'microsoft';
|
||||
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const tenantID = envConfig.getString('tenantId');
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const tenantID = envConfig.getString('tenantId');
|
||||
|
||||
const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`;
|
||||
const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`;
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`;
|
||||
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`;
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`;
|
||||
|
||||
const provider = new MicrosoftAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
tokenUrl,
|
||||
const provider = new MicrosoftAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
tokenUrl,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
|
||||
return OAuthProvider.fromConfig(config, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,25 +17,22 @@
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import { Strategy as OAuth2Strategy } from 'passport-oauth2';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
} from '../../lib/oauth';
|
||||
import {
|
||||
executeFetchUserProfileStrategy,
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderHandlers,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
RedirectInfo,
|
||||
} from '../types';
|
||||
import { Config } from '@backstage/config';
|
||||
} from '../../lib/passport';
|
||||
import { RedirectInfo, AuthProviderFactory } from '../types';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -46,7 +43,7 @@ export type OAuth2AuthProviderOptions = OAuthProviderOptions & {
|
||||
tokenUrl: string;
|
||||
};
|
||||
|
||||
export class OAuth2AuthProvider implements OAuthProviderHandlers {
|
||||
export class OAuth2AuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: OAuth2Strategy;
|
||||
|
||||
constructor(options: OAuth2AuthProviderOptions) {
|
||||
@@ -159,31 +156,30 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createOAuth2Provider(
|
||||
config: AuthProviderConfig,
|
||||
_: string,
|
||||
envConfig: Config,
|
||||
_logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'oauth2';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`;
|
||||
const authorizationUrl = envConfig.getString('authorizationUrl');
|
||||
const tokenUrl = envConfig.getString('tokenUrl');
|
||||
export const createOAuth2Provider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'oauth2';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
const authorizationUrl = envConfig.getString('authorizationUrl');
|
||||
const tokenUrl = envConfig.getString('tokenUrl');
|
||||
|
||||
const provider = new OAuth2AuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
tokenUrl,
|
||||
});
|
||||
const provider = new OAuth2AuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
tokenUrl,
|
||||
});
|
||||
|
||||
return OAuthProvider.fromConfig(config, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import express from 'express';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
} from '../../lib/oauth';
|
||||
import { Strategy as OktaStrategy } from 'passport-okta-oauth';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
@@ -23,19 +29,10 @@ import {
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
executeFetchUserProfileStrategy,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
OAuthProviderHandlers,
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { Logger } from 'winston';
|
||||
} from '../../lib/passport';
|
||||
import { RedirectInfo, AuthProviderFactory } from '../types';
|
||||
import { StateStore } from 'passport-oauth2';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -45,7 +42,7 @@ export type OktaAuthProviderOptions = OAuthProviderOptions & {
|
||||
audience: string;
|
||||
};
|
||||
|
||||
export class OktaAuthProvider implements OAuthProviderHandlers {
|
||||
export class OktaAuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: any;
|
||||
|
||||
/**
|
||||
@@ -170,29 +167,28 @@ export class OktaAuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createOktaProvider(
|
||||
config: AuthProviderConfig,
|
||||
_: string,
|
||||
envConfig: Config,
|
||||
_logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'okta';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const audience = envConfig.getString('audience');
|
||||
const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`;
|
||||
export const createOktaProvider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'okta';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const audience = envConfig.getString('audience');
|
||||
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
|
||||
const provider = new OktaAuthProvider({
|
||||
audience,
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
});
|
||||
const provider = new OktaAuthProvider({
|
||||
audience,
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
});
|
||||
|
||||
return OAuthProvider.fromConfig(config, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,17 +23,15 @@ import {
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
AuthProviderRouteHandlers,
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import {
|
||||
AuthProviderRouteHandlers,
|
||||
ProfileInfo,
|
||||
AuthProviderFactory,
|
||||
} from '../types';
|
||||
import { postMessageResponse } from '../../lib/OAuthProvider';
|
||||
import { Logger } from 'winston';
|
||||
import { postMessageResponse } from '../../lib/flow';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type SamlInfo = {
|
||||
userId: string;
|
||||
@@ -119,15 +117,12 @@ type SAMLProviderOptions = {
|
||||
tokenIssuer: TokenIssuer;
|
||||
};
|
||||
|
||||
export function createSamlProvider(
|
||||
_authProviderConfig: AuthProviderConfig,
|
||||
_env: string,
|
||||
envConfig: Config,
|
||||
_logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const entryPoint = envConfig.getString('entryPoint');
|
||||
const issuer = envConfig.getString('issuer');
|
||||
export const createSamlProvider: AuthProviderFactory = ({
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) => {
|
||||
const entryPoint = config.getString('entryPoint');
|
||||
const issuer = config.getString('issuer');
|
||||
const opts = {
|
||||
entryPoint,
|
||||
issuer,
|
||||
@@ -136,4 +131,4 @@ export function createSamlProvider(
|
||||
};
|
||||
|
||||
return new SamlAuthProvider(opts);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,21 +19,6 @@ import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export type OAuthProviderOptions = {
|
||||
/**
|
||||
* Client ID of the auth provider.
|
||||
*/
|
||||
clientId: string;
|
||||
/**
|
||||
* Client Secret of the auth provider.
|
||||
*/
|
||||
clientSecret: string;
|
||||
/**
|
||||
* Callback URL to be passed to the auth provider to redirect to after the user signs in.
|
||||
*/
|
||||
callbackUrl: string;
|
||||
};
|
||||
|
||||
export type AuthProviderConfig = {
|
||||
/**
|
||||
* The protocol://domain[:port] where the app is hosted. This is used to construct the
|
||||
@@ -47,48 +32,16 @@ export type AuthProviderConfig = {
|
||||
appUrl: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Any OAuth provider needs to implement this interface which has provider specific
|
||||
* handlers for different methods to perform authentication, get access tokens,
|
||||
* refresh tokens and perform sign out.
|
||||
*/
|
||||
export interface OAuthProviderHandlers {
|
||||
export type RedirectInfo = {
|
||||
/**
|
||||
* This method initiates a sign in request with an auth provider.
|
||||
* @param {express.Request} req
|
||||
* @param options
|
||||
* URL to redirect to
|
||||
*/
|
||||
start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo>;
|
||||
|
||||
url: string;
|
||||
/**
|
||||
* Handles the redirect from the auth provider when the user has signed in.
|
||||
* @param {express.Request} req
|
||||
* Status code to use for the redirect
|
||||
*/
|
||||
handler(
|
||||
req: express.Request,
|
||||
): Promise<{
|
||||
response: AuthResponse<OAuthProviderInfo>;
|
||||
refreshToken?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* (Optional) Given a refresh token and scope fetches a new access token from the auth provider.
|
||||
* @param {string} refreshToken
|
||||
* @param {string} scope
|
||||
*/
|
||||
refresh?(
|
||||
refreshToken: string,
|
||||
scope: string,
|
||||
): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
|
||||
/**
|
||||
* (Optional) Sign out of the auth provider.
|
||||
*/
|
||||
logout?(): Promise<void>;
|
||||
}
|
||||
status?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Any Auth provider needs to implement this interface which handles the routes in the
|
||||
@@ -155,24 +108,18 @@ export interface AuthProviderRouteHandlers {
|
||||
* @param {express.Response} res
|
||||
*/
|
||||
logout?(req: express.Request, res: express.Response): Promise<void>;
|
||||
|
||||
/**
|
||||
*(Optional) A method to identify the environment Context of the Request
|
||||
*
|
||||
*Request
|
||||
*- contains the environment context information encoded in the request
|
||||
* @param {express.Request} req
|
||||
*/
|
||||
identifyEnv?(req: express.Request): string | undefined;
|
||||
}
|
||||
|
||||
export type AuthProviderFactoryOptions = {
|
||||
globalConfig: AuthProviderConfig;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
tokenIssuer: TokenIssuer;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
globalConfig: AuthProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
issuer: TokenIssuer,
|
||||
) => AuthProviderRouteHandlers | undefined;
|
||||
options: AuthProviderFactoryOptions,
|
||||
) => AuthProviderRouteHandlers;
|
||||
|
||||
export type AuthResponse<ProviderInfo> = {
|
||||
providerInfo: ProviderInfo;
|
||||
@@ -180,8 +127,6 @@ export type AuthResponse<ProviderInfo> = {
|
||||
backstageIdentity?: BackstageIdentity;
|
||||
};
|
||||
|
||||
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
|
||||
|
||||
export type BackstageIdentity = {
|
||||
/**
|
||||
* The backstage user ID.
|
||||
@@ -194,67 +139,6 @@ export type BackstageIdentity = {
|
||||
idToken?: string;
|
||||
};
|
||||
|
||||
export type OAuthProviderInfo = {
|
||||
/**
|
||||
* An access token issued for the signed in user.
|
||||
*/
|
||||
accessToken: string;
|
||||
/**
|
||||
* (Optional) Id token issued for the signed in user.
|
||||
*/
|
||||
idToken?: string;
|
||||
/**
|
||||
* Expiry of the access token in seconds.
|
||||
*/
|
||||
expiresInSeconds?: number;
|
||||
/**
|
||||
* Scopes granted for the access token.
|
||||
*/
|
||||
scope: string;
|
||||
/**
|
||||
* A refresh token issued for the signed in user
|
||||
*/
|
||||
refreshToken?: string;
|
||||
};
|
||||
|
||||
export type OAuthPrivateInfo = {
|
||||
/**
|
||||
* A refresh token issued for the signed in user.
|
||||
*/
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Payload sent as a post message after the auth request is complete.
|
||||
* If successful then has a valid payload with Auth information else contains an error.
|
||||
*/
|
||||
export type WebMessageResponse =
|
||||
| {
|
||||
type: 'authorization_response';
|
||||
response: AuthResponse<unknown>;
|
||||
}
|
||||
| {
|
||||
type: 'authorization_response';
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export type PassportDoneCallback<Res, Private = never> = (
|
||||
err?: Error,
|
||||
response?: Res,
|
||||
privateInfo?: Private,
|
||||
) => void;
|
||||
|
||||
export type RedirectInfo = {
|
||||
/**
|
||||
* URL to redirect to
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Status code to use for the redirect
|
||||
*/
|
||||
status?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to display login information to user, i.e. sidebar popup.
|
||||
*
|
||||
@@ -276,39 +160,3 @@ export type ProfileInfo = {
|
||||
*/
|
||||
picture?: string;
|
||||
};
|
||||
|
||||
export type RefreshTokenResponse = {
|
||||
/**
|
||||
* An access token issued for the signed in user.
|
||||
*/
|
||||
accessToken: string;
|
||||
/**
|
||||
* Optionally, the server can issue a new Refresh Token for the user
|
||||
*/
|
||||
refreshToken?: string;
|
||||
params: any;
|
||||
};
|
||||
|
||||
export type ProviderStrategy = {
|
||||
userProfile(accessToken: string, callback: Function): void;
|
||||
};
|
||||
|
||||
export type SAMLProviderConfig = {
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
};
|
||||
|
||||
export type SAMLEnvironmentProviderConfig = {
|
||||
[key: string]: SAMLProviderConfig;
|
||||
};
|
||||
|
||||
export type OAuthState = {
|
||||
/* A type for the serialized value in the `state` parameter of the OAuth authorization flow
|
||||
*/
|
||||
nonce: string;
|
||||
env: string;
|
||||
};
|
||||
|
||||
export type EnvironmentIdentifierFn = (
|
||||
req: express.Request,
|
||||
) => string | undefined;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Logger } from 'winston';
|
||||
import { createAuthProviderRouter } from '../providers';
|
||||
import { Config } from '@backstage/config';
|
||||
import { DatabaseKeyStore, TokenFactory, createOidcRouter } from '../identity';
|
||||
import { NotFoundError } from '@backstage/backend-common';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
@@ -88,5 +89,10 @@ export async function createRouter(
|
||||
}),
|
||||
);
|
||||
|
||||
router.use('/:provider/', req => {
|
||||
const { provider } = req.params;
|
||||
throw new NotFoundError(`No auth provider registered for '${provider}'`);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user