Skip to content

Commit

Permalink
More changes
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin committed Jun 15, 2020
1 parent 4af1f3e commit 9e467ed
Show file tree
Hide file tree
Showing 28 changed files with 1,200 additions and 409 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { loginApp } from './login';
import { logoutApp } from './logout';
import { loggedOutApp } from './logged_out';
import { overwrittenSessionApp } from './overwritten_session';
import { captureURLApp } from './capture_url';

interface SetupParams {
application: ApplicationSetup;
Expand Down Expand Up @@ -48,6 +49,7 @@ export class AuthenticationService {
.apiKeysEnabled;

accessAgreementApp.create({ application, getStartServices });
captureURLApp.create({ application, http });
loginApp.create({ application, config, getStartServices, http });
logoutApp.create({ application, http });
loggedOutApp.create({ application, getStartServices, http });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { CoreSetup, HttpSetup } from 'src/core/public';
import { SessionInfo } from '../../../common/types';

interface CreateDeps {
application: CoreSetup['application'];
http: HttpSetup;
}

export const captureURLApp = Object.freeze({
id: 'security_capture_url',
create({ application, http }: CreateDeps) {
http.anonymousPaths.register('/internal/security/capture-url');
application.register({
id: this.id,
title: '',
chromeless: true,
appRoute: '/internal/security/capture-url',
async mount() {
try {
const { provider } = await http.get<SessionInfo>('/internal/security/session');
if (!provider) {
throw new Error('Cannot retrieve current provider.');
}

const { location } = await http.post<{ location: string }>(
'/internal/security/login_with',
{
body: JSON.stringify({
providerType: provider.type,
providerName: provider.name,
currentURL: window.location.href,
}),
}
);

window.location.href = location;
} catch (err) {
console.error(err);
}

return () => {};
},
});
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export { captureURLApp } from './capture_url_app';
53 changes: 40 additions & 13 deletions x-pack/plugins/security/server/authentication/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,10 @@ export class Authenticator {
throw new Error('Current license does not allow access agreement acknowledgement.');
}

await this.session.set(request, { ...existingSessionValue, accessAgreementAcknowledged: true });
await this.session.update(request, {
...existingSessionValue,
accessAgreementAcknowledged: true,
});

this.options.auditLogger.accessAgreementAcknowledged(
currentUser.username,
Expand Down Expand Up @@ -478,13 +481,6 @@ export class Authenticator {
return null;
}

// If authentication succeeds or requires redirect we should automatically extend existing user session,
// unless authentication has been triggered by a system API request. In case provider explicitly returns new
// state we should store it in the session regardless of whether it's a system API request or not.
const sessionCanBeUpdated =
(authenticationResult.succeeded() || authenticationResult.redirected()) &&
(authenticationResult.shouldUpdateState() || !request.isSystemRequest);

// If provider owned the session, but failed to authenticate anyway, that likely means that
// session is not valid and we should clear it. Also provider can specifically ask to clear
// session by setting it to `null` even if authentication attempt didn't fail.
Expand All @@ -496,16 +492,47 @@ export class Authenticator {
return null;
}

if (sessionCanBeUpdated) {
return await this.session.set(request, {
...(existingSessionValue || { provider, lifespanExpiration: null }),
// If authentication succeeds or requires redirect we should automatically extend existing user session,
// unless authentication has been triggered by a system API request. In case provider explicitly returns new
// state we should store it in the session regardless of whether it's a system API request or not.
const sessionCanBeUpdated =
(authenticationResult.succeeded() || authenticationResult.redirected()) &&
(authenticationResult.shouldUpdateState() || !request.isSystemRequest);
if (!sessionCanBeUpdated) {
return existingSessionValue;
}

if (!existingSessionValue) {
return await this.session.create(request, {
username: authenticationResult.user?.username,
provider,
state: authenticationResult.shouldUpdateState() ? authenticationResult.state : null,
});
}

// If provider wishes to update state or username associated with the session has changed we
// should update session. There are two cases when username can change for the existing session:
// 1. Session was unauthenticated (e.g. intermediate session used during SSO handshake) and can
// now be turned into authenticated one.
// 2. Session was already authenticated, but provider decided to re-authenticate user with
// another username (e.g. during IdP initiated SSO login or when client certificate changes and
// PKI providers needs to re-authenticate user). In this case we SHOULD regenerate SID and AAD,
// but we don't this for now.
const username = authenticationResult.user?.username;
if (
authenticationResult.shouldUpdateState() ||
(username && username !== existingSessionValue.username)
) {
return await this.session.update(request, {
...existingSessionValue,
username: authenticationResult.user?.username || existingSessionValue.username,
state: authenticationResult.shouldUpdateState()
? authenticationResult.state
: existingSessionValue?.state,
: existingSessionValue.state,
});
}

return existingSessionValue;
return await this.session.extend(request, existingSessionValue);
}

/**
Expand Down
32 changes: 2 additions & 30 deletions x-pack/plugins/security/server/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { AuthenticatedUser } from '../../common/model';
import { SessionInfo } from '../../common/types';
import { SecurityAuditLogger } from '../audit';
import { ConfigType } from '../config';
import { getErrorStatusCode } from '../errors';
Expand Down Expand Up @@ -46,6 +45,7 @@ interface SetupAuthenticationParams {
config: ConfigType;
license: SecurityLicense;
loggers: LoggerFactory;
session: Session;
}

export type Authentication = UnwrapPromise<ReturnType<typeof setupAuthentication>>;
Expand All @@ -58,6 +58,7 @@ export async function setupAuthentication({
config,
license,
loggers,
session,
}: SetupAuthenticationParams) {
const authLogger = loggers.get('authentication');

Expand All @@ -73,34 +74,6 @@ export async function setupAuthentication({
return (http.auth.get(request).state ?? null) as AuthenticatedUser | null;
};

/**
* Returns session information for the current request.
* @param request Request instance.
*/
const getSessionInfo = async (request: KibanaRequest): Promise<SessionInfo | null> => {
const sessionValue = await session.get(request);
if (!sessionValue) {
return null;
}

// We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return
// the current server time -- that way the client can calculate the relative time to expiration.
return {
now: Date.now(),
idleTimeoutExpiration: sessionValue.idleTimeoutExpiration,
lifespanExpiration: sessionValue.lifespanExpiration,
provider: sessionValue.provider,
};
};

const session = new Session({
auditLogger,
logger: loggers.get('session'),
clusterClient,
config,
http,
});

authLogger.debug('Successfully initialized session.');

const authenticator = new Authenticator({
Expand Down Expand Up @@ -182,7 +155,6 @@ export async function setupAuthentication({
return {
login: authenticator.login.bind(authenticator),
logout: authenticator.logout.bind(authenticator),
getSessionInfo,
isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator),
acknowledgeAccessAgreement: authenticator.acknowledgeAccessAgreement.bind(authenticator),
getCurrentUser,
Expand Down
43 changes: 28 additions & 15 deletions x-pack/plugins/security/server/authentication/providers/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export enum OIDCLogin {
* Describes the parameters that are required by the provider to process the initial login request.
*/
export type ProviderLoginAttempt =
| { type: OIDCLogin.LoginInitiatedByUser; redirectURLPath: string }
| { type: OIDCLogin.LoginInitiatedByUser; redirectURL: string }
| {
type: OIDCLogin.LoginWithImplicitFlow | OIDCLogin.LoginWithAuthorizationCodeFlow;
authenticationResponseURI: string;
Expand All @@ -58,7 +58,7 @@ interface ProviderState extends Partial<TokenPair> {
/**
* URL to redirect user to after successful OpenID Connect handshake.
*/
nextURL?: string;
redirectURL?: string;

/**
* The name of the OpenID Connect realm that was used to establish session.
Expand Down Expand Up @@ -143,11 +143,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {

if (attempt.type === OIDCLogin.LoginInitiatedByUser) {
this.logger.debug(`Login has been initiated by a user.`);
return this.initiateOIDCAuthentication(
request,
{ realm: this.realm },
attempt.redirectURLPath
);
return this.initiateOIDCAuthentication(request, { realm: this.realm }, attempt.redirectURL);
}

if (attempt.type === OIDCLogin.LoginWithImplicitFlow) {
Expand Down Expand Up @@ -200,7 +196,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
// We might already have a state and nonce generated by Elasticsearch (from an unfinished authentication in
// another tab)
return authenticationResult.notHandled() && canStartNewSession(request)
? await this.initiateOIDCAuthentication(request, { realm: this.realm })
? await this.captureRedirectURL(request)
: authenticationResult;
}

Expand Down Expand Up @@ -231,8 +227,11 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
// If it is an authentication response and the users' session state doesn't contain all the necessary information,
// then something unexpected happened and we should fail because Elasticsearch won't be able to validate the
// response.
const { nonce: stateNonce = '', state: stateOIDCState = '', nextURL: stateRedirectURL = '' } =
sessionState || {};
const {
nonce: stateNonce = '',
state: stateOIDCState = '',
redirectURL: stateRedirectURL = '',
} = sessionState || {};
if (!stateNonce || !stateOIDCState || !stateRedirectURL) {
const message =
'Response session state does not have corresponding state or nonce parameters or redirect URL.';
Expand Down Expand Up @@ -272,13 +271,12 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
*
* @param request Request instance.
* @param params OIDC authentication parameters.
* @param [redirectURLPath] Optional URL user is supposed to be redirected to after successful
* login. If not provided the URL of the specified request is used.
* @param redirectURL URL user is supposed to be redirected to after successful login.
*/
private async initiateOIDCAuthentication(
request: KibanaRequest,
params: { realm: string } | { iss: string; login_hint?: string },
redirectURLPath = `${this.options.basePath.get(request)}${request.url.path}`
redirectURL: string
) {
this.logger.debug('Trying to initiate OpenID Connect authentication.');

Expand All @@ -295,7 +293,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
return AuthenticationResult.redirectTo(
redirect,
// Store the state and nonce parameters in the session state of the user
{ state: { state, nonce, nextURL: redirectURLPath, realm: this.realm } }
{ state: { state, nonce, redirectURL, realm: this.realm } }
);
} catch (err) {
this.logger.debug(`Failed to initiate OpenID Connect authentication: ${err.message}`);
Expand Down Expand Up @@ -367,7 +365,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug(
'Both elasticsearch access and refresh tokens are expired. Re-initiating OpenID Connect authentication.'
);
return this.initiateOIDCAuthentication(request, { realm: this.realm });
return this.captureRedirectURL(request);
}

return AuthenticationResult.failed(
Expand Down Expand Up @@ -449,4 +447,19 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
public getHTTPAuthenticationScheme() {
return 'bearer';
}

/**
* Tries to capture full redirect URL (both path and fragment) and initiate OIDC handshake.
* @param request Request instance.
*/
private captureRedirectURL(request: KibanaRequest) {
return AuthenticationResult.redirectTo(
`${
this.options.basePath.serverBasePath
}/internal/security/capture-url?next=${encodeURIComponent(
`${this.options.basePath.get(request)}${request.url.path}`
)}`,
{ state: { realm: this.realm } }
);
}
}
Loading

0 comments on commit 9e467ed

Please sign in to comment.