Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] Add OpenID Connect auth provider (#36201) #36838

Merged
merged 1 commit into from
May 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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