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

Add JWT authentication type to MultipleAuthentication #2107

Merged
merged 18 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
31 changes: 14 additions & 17 deletions public/apps/login/login-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,22 +168,24 @@ export function LoginPage(props: LoginPageDeps) {
const formOptions = (options: string | string[]) => {
let formBody = [];
const formBodyOp = [];
let authOpts = [];
let authOpts: string[] = [];

// Convert auth options to a usable array
if (typeof options === 'string') {
if (options === '') {
authOpts.push(AuthType.BASIC);
} else {
if (options !== '') {
authOpts.push(options.toLowerCase());
}
} else {
if (options && options.length === 1 && options[0] === '') {
authOpts.push(AuthType.BASIC);
} else {
authOpts = [...options];
}
} else if (!(options && options.length === 1 && options[0] === '')) {
authOpts = [...options];
}
if (authOpts.length === 0) {
authOpts.push(AuthType.BASIC);
}

// Remove proxy and jwt from the list because they do not have a login button
// The count of visible options determines if a separator gets added
authOpts = authOpts.filter((auth) => auth !== AuthType.PROXY && auth !== AuthType.JWT);
merlinz01 marked this conversation as resolved.
Show resolved Hide resolved

for (let i = 0; i < authOpts.length; i++) {
switch (authOpts[i].toLowerCase()) {
case AuthType.BASIC: {
Expand Down Expand Up @@ -237,10 +239,8 @@ export function LoginPage(props: LoginPageDeps) {
);
}

if (
authOpts.length > 1 &&
!(authOpts.includes(AuthType.PROXY) && authOpts.length === 2)
) {
if (authOpts.length > 1) {
// Add a separator between the username/password form and the other login options
formBody.push(<EuiSpacer size="xs" />);
formBody.push(<EuiHorizontalRule size="full" margin="xl" />);
formBody.push(<EuiSpacer size="xs" />);
Expand All @@ -261,9 +261,6 @@ export function LoginPage(props: LoginPageDeps) {
formBodyOp.push(renderLoginButton(AuthType.SAML, samlAuthLoginUrl, samlConfig));
break;
}
case AuthType.PROXY: {
break;
}
default: {
setloginFailed(true);
setloginError(
Expand Down
35 changes: 17 additions & 18 deletions server/auth/types/authentication_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,77 +96,76 @@ export abstract class AuthenticationType implements IAuthenticationType {
}

public authHandler: AuthenticationHandler = async (request, response, toolkit) => {
// skip auth for APIs that do not require auth
// Skip authentication for APIs that do not require it
if (this.authNotRequired(request)) {
return toolkit.authenticated();
}

const authState: OpenSearchDashboardsAuthState = {};

// if browser request, auth logic is:
merlinz01 marked this conversation as resolved.
Show resolved Hide resolved
// 1. check if request includes auth header or parameter(e.g. jwt in url params) is present, if so, authenticate with auth header.
// 2. if auth header not present, check if auth cookie is present, if no cookie, send to authentication workflow
// 3. verify whether auth cookie is valid, if not valid, send to authentication workflow
// 4. if cookie is valid, pass to route handlers
const authHeaders = {};
let cookie: SecuritySessionCookie | null | undefined;
let authInfo: any | undefined;
// if this is an REST API call, suppose the request includes necessary auth header

// If the request contains authentication data (e.g. Authorization header or JWT in url parameters), use that to authenticate the request.
if (this.requestIncludesAuthInfo(request)) {
try {
// Build the auth headers from the request
const additionalAuthHeader = await this.getAdditionalAuthHeader(request);
Object.assign(authHeaders, additionalAuthHeader);
authInfo = await this.securityClient.authinfo(request, additionalAuthHeader);
cookie = this.getCookie(request, authInfo);

// set tenant from cookie if exist
// Set the tenant from the cookie
const browserCookie = await this.sessionStorageFactory.asScoped(request).get();
if (browserCookie && isValidTenant(browserCookie.tenant)) {
cookie.tenant = browserCookie.tenant;
}

// Save the cookie
this.sessionStorageFactory.asScoped(request).set(cookie);
} catch (error: any) {
return response.unauthorized({
body: error.message,
});
}
} else {
// no auth header in request, try cookie
// If the request does not contain authentication data, check for a stored cookie.
try {
cookie = await this.sessionStorageFactory.asScoped(request).get();
} catch (error: any) {
this.logger.error(`Error parsing cookie: ${error.message}`);
cookie = undefined;
}

// If the cookie is not valid, clear the cookie and send the request to the authentication workflow
if (!cookie || !(await this.isValidCookie(cookie, request))) {
// clear cookie
// Clear the cookie
this.sessionStorageFactory.asScoped(request).clear();

// for assets, we can still pass it to resource handler as notHandled.
// marking it as authenticated may result in login pop up when auth challenge
// For assets, we can still pass it to resource handler as notHandled.
// Marking it as authenticated may result in login pop up when auth challenge
// is enabled.
if (request.url.pathname && request.url.pathname.startsWith('/bundles/')) {
return toolkit.notHandled();
}

// allow optional authentication
// Allow optional authentication
if (this.authOptional(request)) {
return toolkit.authenticated();
}

// send to auth workflow
// Send the request to the authentication workflow
return this.handleUnauthedRequest(request, response, toolkit);
}

// extend session expiration time
// If the cookie is still valid, update the cookie with a new expiry time.
if (this.config.session.keepalive) {
cookie!.expiryTime = this.getKeepAliveExpiry(cookie!, request);
this.sessionStorageFactory.asScoped(request).set(cookie!);
}
// cookie is valid
// build auth header
// At this point we have a valid cookie.

// Build the auth headers from the cookie.
const authHeadersFromCookie = this.buildAuthHeaderFromCookie(cookie!, request);
Object.assign(authHeaders, authHeadersFromCookie);
const additionalAuthHeader = await this.getAdditionalAuthHeader(request);
Expand Down
1 change: 1 addition & 0 deletions server/auth/types/jwt/jwt_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export class JwtAuthentication extends AuthenticationType {
request: OpenSearchDashboardsRequest<unknown, unknown, unknown, any>,
authInfo: any
): SecuritySessionCookie {
// TODO: This logic is only applicable for JWT auth type
cwperks marked this conversation as resolved.
Show resolved Hide resolved
setExtraAuthStorage(
request,
this.getBearerToken(request) || '',
Expand Down
26 changes: 25 additions & 1 deletion server/auth/types/multiple/multi_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
OpenIdAuthentication,
ProxyAuthentication,
SamlAuthentication,
JwtAuthentication,
} from '../../types';

export class MultipleAuthentication extends AuthenticationType {
Expand Down Expand Up @@ -111,6 +112,19 @@ export class MultipleAuthentication extends AuthenticationType {
this.authHandlers.set(AuthType.PROXY, ProxyAuth);
break;
}
case AuthType.JWT: {
const JwtAuth = new JwtAuthentication(
this.config,
this.sessionStorageFactory,
this.router,
this.esClient,
this.coreSetup,
this.logger
);
await JwtAuth.init();
this.authHandlers.set(AuthType.JWT, JwtAuth);
break;
}
default: {
throw new Error(`Unsupported authentication type: ${this.authTypes[i]}`);
}
Expand Down Expand Up @@ -140,11 +154,21 @@ export class MultipleAuthentication extends AuthenticationType {
if (reqAuthType && this.authHandlers.has(reqAuthType)) {
return await this.authHandlers.get(reqAuthType)!.getAdditionalAuthHeader(request);
} else {
return {};
merlinz01 marked this conversation as resolved.
Show resolved Hide resolved
const authHeaders: any = {};
for (const handler of this.authHandlers.values()) {
Object.assign(authHeaders, await handler.getAdditionalAuthHeader(request));
}
return authHeaders;
}
}

getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie {
// TODO: This logic is only applicable for JWT auth type
for (const handler of this.authHandlers.values()) {
if (handler.requestIncludesAuthInfo(request)) {
return handler.getCookie(request, authInfo);
}
}
return {};
}

Expand Down
2 changes: 2 additions & 0 deletions test/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ export const AUTHORIZATION_HEADER_NAME: string = 'Authorization';
export const PROXY_USER: string = 'x-proxy-user';
export const PROXY_ROLE: string = 'x-proxy-roles';
export const PROXY_ADMIN_ROLE: string = 'admin';

export const JWT_ADMIN_ROLE: string = 'admin';
Loading
Loading