Skip to content

Commit

Permalink
Add OpenID Connect auth provider (elastic#36201)
Browse files Browse the repository at this point in the history
The OpenID Connect authProvider is the accompanying authProvider for the OpenID Connect authentication realm in Elasticsearch. This is very similar to the saml authProvider in most ways with three noticeable differences:

- We require explicit configuration regarding the Elasticsearch realm name instead of trying to build an environment aware string (like ACS URL in saml) and pass that to Elasticsearch for it to resolve the realm.
- We do not support multiple values for the realm specific nonces (state and nonce) as we do with requestId in the SAML realm. Instead if an existing value ( for state and nonce) is present in the user's session, we pass that to Elasticsearch to be reused. The end goal is the same, allow a better UX for users attempting many requests over different tabs in the same browser context.
- IDP initiated SSO ( Third Party initiated authentication in OIDC-speak ) is implemented but starts as an unsolicited request to initiate the handshake, instead of an unsolicited request with an authentication response (which is not supported here)

This change also adds a fake plugin named oidc_provider to be used in integration tests for mocking calls to the token and userinfo endpoint of an OpenID Connect Provider

This does not support the OpenID Connect Implicit flow as that depends on fragment handling/processing as described for instance in the spec

Co-Authored-By: Brandon Kobel <[email protected]>
  • Loading branch information
2 people authored and kobelb committed May 21, 2019
1 parent d2c198d commit 0297c78
Show file tree
Hide file tree
Showing 23 changed files with 2,184 additions and 30 deletions.
6 changes: 6 additions & 0 deletions x-pack/plugins/security/__snapshots__/index.test.js.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions x-pack/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ export const security = (kibana) => new kibana.Plugin({
audit: Joi.object({
enabled: Joi.boolean().default(false)
}).default(),
authc: Joi.object({})
.when('authProviders', {
is: Joi.array().items(Joi.string().valid('oidc').required(), Joi.string()),
then: Joi.object({
oidc: Joi.object({
realm: Joi.string().required(),
}).default()
}).default()
})
}).default();
},

Expand Down
76 changes: 68 additions & 8 deletions x-pack/plugins/security/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,76 @@
import { security } from './index';
import { getConfigSchema } from '../../test_utils';

const describeWithContext = describe.each([
[{ dist: false }],
[{ dist: true }]
]);
const describeWithContext = describe.each([[{ dist: false }], [{ dist: true }]]);

describeWithContext('config schema with context %j', (context) => {
describeWithContext('config schema with context %j', context => {
it('produces correct config', async () => {
const schema = await getConfigSchema(security);
await expect(
schema.validate({}, { context })
).resolves.toMatchSnapshot();
await expect(schema.validate({}, { context })).resolves.toMatchSnapshot();
});
});

describe('config schema', () => {
describe('authc', () => {
describe('oidc', () => {
describe('realm', () => {
it(`returns a validation error when authProviders is "['oidc']" and realm is unspecified`, async () => {
const schema = await getConfigSchema(security);
const validationResult = schema.validate({
authProviders: ['oidc'],
});
expect(validationResult.error).toMatchSnapshot();
});

it(`is valid when authProviders is "['oidc']" and realm is specified`, async () => {
const schema = await getConfigSchema(security);
const validationResult = schema.validate({
authProviders: ['oidc'],
authc: {
oidc: {
realm: 'realm-1',
},
},
});
expect(validationResult.error).toBeNull();
expect(validationResult.value).toHaveProperty('authc.oidc.realm', 'realm-1');
});

it(`returns a validation error when authProviders is "['oidc', 'basic']" and realm is unspecified`, async () => {
const schema = await getConfigSchema(security);
const validationResult = schema.validate({
authProviders: ['oidc', 'basic'],
});
expect(validationResult.error).toMatchSnapshot();
});

it(`is valid when authProviders is "['oidc', 'basic']" and realm is specified`, async () => {
const schema = await getConfigSchema(security);
const validationResult = schema.validate({
authProviders: ['oidc', 'basic'],
authc: {
oidc: {
realm: 'realm-1',
},
},
});
expect(validationResult.error).toBeNull();
expect(validationResult.value).toHaveProperty('authc.oidc.realm', 'realm-1');
});

it(`realm is not allowed when authProviders is "['basic']"`, async () => {
const schema = await getConfigSchema(security);
const validationResult = schema.validate({
authProviders: ['basic'],
authc: {
oidc: {
realm: 'realm-1',
},
},
});
expect(validationResult.error).toMatchSnapshot();
});
});
});
});
});
52 changes: 42 additions & 10 deletions x-pack/plugins/security/server/lib/authentication/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import {
BasicAuthenticationProvider,
SAMLAuthenticationProvider,
TokenAuthenticationProvider,
OIDCAuthenticationProvider,
} from './providers';
import { AuthenticationResult } from './authentication_result';
import { DeauthenticationResult } from './deauthentication_result';
import { Session } from './session';
import { LoginAttempt } from './login_attempt';
import { AuthenticationProviderSpecificOptions } from './providers/base';

interface ProviderSession {
provider: string;
Expand All @@ -29,11 +31,15 @@ interface ProviderSession {
// provider class that can handle specific authentication mechanism.
const providerMap = new Map<
string,
new (options: AuthenticationProviderOptions) => BaseAuthenticationProvider
new (
options: AuthenticationProviderOptions,
providerSpecificOptions: AuthenticationProviderSpecificOptions
) => BaseAuthenticationProvider
>([
['basic', BasicAuthenticationProvider],
['saml', SAMLAuthenticationProvider],
['token', TokenAuthenticationProvider],
['oidc', OIDCAuthenticationProvider],
]);

function assertRequest(request: Legacy.Request) {
Expand Down Expand Up @@ -62,18 +68,44 @@ function getProviderOptions(server: Legacy.Server) {
};
}

/**
* Prepares options object that is specific only to an authentication provider.
* @param server Server instance.
* @param providerType the type of the provider to get the options for.
*/
function getProviderSpecificOptions(
server: Legacy.Server,
providerType: string
): AuthenticationProviderSpecificOptions {
const config = server.config();
// we can't use `config.has` here as it doesn't currently work with Joi's "alternatives" syntax which we
// are using to make the provider specific configuration required when the auth provider is specified
const authc = config.get<Record<string, AuthenticationProviderSpecificOptions | undefined>>(
`xpack.security.authc`
);
if (authc && authc[providerType] !== undefined) {
return authc[providerType] as AuthenticationProviderSpecificOptions;
}

return {};
}

/**
* Instantiates authentication provider based on the provider key from config.
* @param providerType Provider type key.
* @param options Options to pass to provider's constructor.
*/
function instantiateProvider(providerType: string, options: AuthenticationProviderOptions) {
function instantiateProvider(
providerType: string,
options: AuthenticationProviderOptions,
providerSpecificOptions: AuthenticationProviderSpecificOptions
) {
const ProviderClassName = providerMap.get(providerType);
if (!ProviderClassName) {
throw new Error(`Unsupported authentication provider name: ${providerType}.`);
}

return new ProviderClassName(options);
return new ProviderClassName(options, providerSpecificOptions);
}

/**
Expand Down Expand Up @@ -117,13 +149,13 @@ class Authenticator {
const providerOptions = Object.freeze(getProviderOptions(server));

this.providers = new Map(
authProviders.map(
providerType =>
[providerType, instantiateProvider(providerType, providerOptions)] as [
string,
BaseAuthenticationProvider
]
)
authProviders.map(providerType => {
const providerSpecificOptions = getProviderSpecificOptions(server, providerType);
return [
providerType,
instantiateProvider(providerType, providerOptions, providerSpecificOptions),
] as [string, BaseAuthenticationProvider];
})
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export interface AuthenticationProviderOptions {
log: (tags: string[], message: string) => void;
}

/**
* Represents available provider specific options.
*/
export type AuthenticationProviderSpecificOptions = Record<string, unknown>;

/**
* Base class that all authentication providers should extend.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { BaseAuthenticationProvider, AuthenticationProviderOptions } from './bas
export { BasicAuthenticationProvider, BasicCredentials } from './basic';
export { SAMLAuthenticationProvider } from './saml';
export { TokenAuthenticationProvider } from './token';
export { OIDCAuthenticationProvider } from './oidc';
Loading

0 comments on commit 0297c78

Please sign in to comment.