Skip to content

Commit

Permalink
Add support for OpenID Connect implicit authentication flow. (#42069)
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin authored Aug 8, 2019
1 parent 81d7d6c commit 0d31f52
Show file tree
Hide file tree
Showing 21 changed files with 532 additions and 200 deletions.
8 changes: 8 additions & 0 deletions renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,14 @@
'@types/history',
],
},
{
groupSlug: 'jsdom',
groupName: 'jsdom related packages',
packageNames: [
'jsdom',
'@types/jsdom',
],
},
{
groupSlug: 'jsonwebtoken',
groupName: 'jsonwebtoken related packages',
Expand Down
92 changes: 74 additions & 18 deletions x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import Boom from 'boom';
import Joi from 'joi';
import { schema } from '@kbn/config-schema';
import { canRedirectRequest, wrapError } from '../../../../../../../plugins/security/server';
import { canRedirectRequest, wrapError, OIDCAuthenticationFlow } from '../../../../../../../plugins/security/server';
import { KibanaRequest } from '../../../../../../../../src/core/server';
import { createCSPRuleString, generateCSPNonce } from '../../../../../../../../src/legacy/server/csp';

export function initAuthenticateApi({ authc: { login, logout }, config }, server) {

Expand Down Expand Up @@ -82,8 +83,39 @@ export function initAuthenticateApi({ authc: { login, logout }, config }, server
}
});

/**
* The route should be configured as a redirect URI in OP when OpenID Connect implicit flow
* is used, so that we can extract authentication response from URL fragment and send it to
* the `/api/security/v1/oidc` route.
*/
server.route({
method: 'GET',
path: '/api/security/v1/oidc/implicit',
config: { auth: false },
async handler(request, h) {
const legacyConfig = server.config();
const basePath = legacyConfig.get('server.basePath');

const nonce = await generateCSPNonce();
const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules'), nonce);
return h.response(`
<!DOCTYPE html>
<title>Kibana OpenID Connect Login</title>
<script nonce="${nonce}">
window.location.replace(
'${basePath}/api/security/v1/oidc?authenticationResponseURI=' + encodeURIComponent(window.location.href)
);
</script>
`)
.header('cache-control', 'private, no-cache, no-store')
.header('content-security-policy', cspRulesHeader)
.type('text/html');
}
});

server.route({
// POST is only allowed for Third Party initiated authentication
// Consider splitting this route into two (GET and POST) when it's migrated to New Platform.
method: ['GET', 'POST'],
path: '/api/security/v1/oidc',
config: {
Expand All @@ -97,31 +129,55 @@ export function initAuthenticateApi({ authc: { login, logout }, config }, server
error: Joi.string(),
error_description: Joi.string(),
error_uri: Joi.string().uri(),
state: Joi.string()
}).unknown()
state: Joi.string(),
authenticationResponseURI: Joi.string(),
}).unknown(),
}
},
async handler(request, h) {
try {
const query = request.query || {};
const payload = request.payload || {};

// An HTTP GET request with a query parameter named `authenticationResponseURI` that includes URL fragment OpenID
// Connect Provider sent during implicit authentication flow to the Kibana own proxy page that extracted that URL
// fragment and put it into `authenticationResponseURI` query string parameter for this endpoint. See more details
// at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth
let loginAttempt;
if (query.authenticationResponseURI) {
loginAttempt = {
flow: OIDCAuthenticationFlow.Implicit,
authenticationResponseURI: query.authenticationResponseURI,
};
} else if (query.code || query.error) {
// An HTTP GET request with a query parameter named `code` (or `error`) as the response to a successful (or
// failed) authentication from an OpenID Connect Provider during authorization code authentication flow.
// See more details at https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth.
loginAttempt = {
flow: OIDCAuthenticationFlow.AuthorizationCode,
// We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway.
authenticationResponseURI: request.url.path,
};
} else if (query.iss || payload.iss) {
// An HTTP GET request with a query parameter named `iss` or an HTTP POST request with the same parameter in the
// payload as part of a 3rd party initiated authentication. See more details at
// https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin
loginAttempt = {
flow: OIDCAuthenticationFlow.InitiatedBy3rdParty,
iss: query.iss || payload.iss,
loginHint: query.login_hint || payload.login_hint,
};
}

if (!loginAttempt) {
throw Boom.badRequest('Unrecognized login attempt.');
}

// We handle the fact that the user might get redirected to Kibana while already having an session
// Return an error notifying the user they are already logged in.
const authenticationResult = await login(KibanaRequest.from(request), {
provider: 'oidc',
// Checks if the request object represents an HTTP request regarding authentication with OpenID Connect.
// This can be
// - An HTTP GET request with a query parameter named `iss` as part of a 3rd party initiated authentication
// - An HTTP POST request with a parameter named `iss` as part of a 3rd party initiated authentication
// - An HTTP GET request with a query parameter named `code` as the response to a successful authentication from
// an OpenID Connect Provider
// - An HTTP GET request with a query parameter named `error` as the response to a failed authentication from
// an OpenID Connect Provider
value: {
code: request.query && request.query.code,
iss: (request.query && request.query.iss) || (request.payload && request.payload.iss),
loginHint:
(request.query && request.query.login_hint) ||
(request.payload && request.payload.login_hint),
},
value: loginAttempt
});
if (authenticationResult.succeeded()) {
return Boom.forbidden(
Expand Down
2 changes: 2 additions & 0 deletions x-pack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@types/jest": "^24.0.9",
"@types/joi": "^13.4.2",
"@types/js-yaml": "^3.11.1",
"@types/jsdom": "^12.2.4",
"@types/json-stable-stringify": "^1.0.32",
"@types/jsonwebtoken": "^7.2.7",
"@types/lodash": "^3.10.1",
Expand Down Expand Up @@ -110,6 +111,7 @@
"babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"base64-js": "^1.2.1",
"base64url": "^3.0.1",
"chalk": "^2.4.1",
"chance": "1.0.18",
"checksum": "0.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ export class Authenticator {

const sessionStorage = this.options.sessionStorageFactory.asScoped(request);

// If we detect an existing session that belongs to a different provider than the one request to
// perform a login we should clear such session.
// If we detect an existing session that belongs to a different provider than the one requested
// to perform a login we should clear such session.
let existingSession = await this.getSessionValue(sessionStorage);
if (existingSession && existingSession.provider !== attempt.provider) {
this.logger.debug(
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security/server/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export { canRedirectRequest } from './can_redirect_request';
export { Authenticator, ProviderLoginAttempt } from './authenticator';
export { AuthenticationResult } from './authentication_result';
export { DeauthenticationResult } from './deauthentication_result';
export { BasicCredentials } from './providers';
export { BasicCredentials, OIDCAuthenticationFlow } from './providers';

interface SetupAuthenticationParams {
core: CoreSetup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export { BasicAuthenticationProvider, BasicCredentials } from './basic';
export { KerberosAuthenticationProvider } from './kerberos';
export { SAMLAuthenticationProvider, isSAMLRequestQuery } from './saml';
export { TokenAuthenticationProvider } from './token';
export { OIDCAuthenticationProvider } from './oidc';
export { OIDCAuthenticationProvider, OIDCAuthenticationFlow } from './oidc';
Loading

0 comments on commit 0d31f52

Please sign in to comment.