packages/runtime-tools-components/src/common/utils/KeycloakClient.ts (167 lines of code) (raw):
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import axios, { InternalAxiosRequestConfig } from "axios";
import Keycloak from "keycloak-js";
import { ANONYMOUS_USER, KeycloakUserContext, User, UserContext } from "../contexts/KogitoAppContext";
export interface KogitoConsolesKeycloakEnv {
KOGITO_CONSOLES_KEYCLOAK_DISABLE_HEALTH_CHECK?: boolean;
KOGITO_CONSOLES_KEYCLOAK_UPDATE_TOKEN_VALIDITY?: number;
KOGITO_CONSOLES_KEYCLOAK_HEALTH_CHECK_URL?: string;
KOGITO_CONSOLES_KEYCLOAK_REALM?: string;
KOGITO_CONSOLES_KEYCLOAK_URL?: string;
KOGITO_CONSOLES_KEYCLOAK_CLIENT_ID?: string;
}
export const isAuthEnabled = (): boolean => {
return window["KOGITO_ENV_MODE"] !== "DEV";
};
export const isKeycloakHealthCheckDisabled = (): boolean => {
return window["KOGITO_CONSOLES_KEYCLOAK_DISABLE_HEALTH_CHECK"];
};
export const getUpdateTokenValidity = (): number => {
const updateTokenValidity = window["KOGITO_CONSOLES_KEYCLOAK_UPDATE_TOKEN_VALIDITY"];
if (typeof updateTokenValidity !== "number") {
return 30;
}
return updateTokenValidity;
};
let currentSecurityContext: UserContext | undefined;
let keycloak: Keycloak.KeycloakInstance;
export const getLoadedSecurityContext = (): UserContext => {
/* istanbul ignore if */
if (!currentSecurityContext) {
/* istanbul ignore if */
if (isAuthEnabled()) {
throw Error("Cannot load security context! Please reload screen and log in again.");
}
currentSecurityContext = getNonAuthUserContext();
}
return currentSecurityContext;
};
export const checkAuthServerHealth = () => {
return new Promise<void>((resolve, reject) => {
fetch(window["KOGITO_CONSOLES_KEYCLOAK_HEALTH_CHECK_URL"])
.then((response) => {
/* istanbul ignore else */
if (response.status === 200) {
resolve();
}
})
.catch(() => {
reject();
});
});
};
export const getKeycloakClient = (): Keycloak => {
return new Keycloak({
realm: window["KOGITO_CONSOLES_KEYCLOAK_REALM"],
url: window["KOGITO_CONSOLES_KEYCLOAK_URL"],
clientId: window["KOGITO_CONSOLES_KEYCLOAK_CLIENT_ID"],
});
};
export const initializeKeycloak = (onloadSuccess: () => void): Promise<void> => {
keycloak = getKeycloakClient();
return keycloak
.init({
onLoad: "login-required",
})
.then((authenticated) => {
/* istanbul ignore else */
if (authenticated) {
currentSecurityContext = new KeycloakUserContext({
userName: keycloak.tokenParsed!["preferred_username"],
roles: keycloak.tokenParsed!["groups"],
token: keycloak.token!,
tokenMinValidity: getUpdateTokenValidity(),
logout: () => handleLogout(),
});
onloadSuccess();
}
});
};
export const loadSecurityContext = (onloadSuccess: () => void, onLoadFailure: () => void): Promise<void> => {
if (isAuthEnabled()) {
if (isKeycloakHealthCheckDisabled()) {
return initializeKeycloak(onloadSuccess);
} else {
return checkAuthServerHealth()
.then(() => {
return initializeKeycloak(onloadSuccess);
})
.catch((_error) => {
onLoadFailure();
});
}
} else {
currentSecurityContext = getNonAuthUserContext();
onloadSuccess();
return Promise.resolve();
}
};
const getNonAuthUserContext = (): UserContext => {
return {
getCurrentUser(): User {
return ANONYMOUS_USER;
},
};
};
export const getToken = () => {
if (isAuthEnabled()) {
const ctx = getLoadedSecurityContext() as KeycloakUserContext;
return ctx.getToken();
}
};
export const updateKeycloakToken = () => {
if (!isAuthEnabled()) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
const ctx = getLoadedSecurityContext() as KeycloakUserContext;
keycloak
.updateToken(getUpdateTokenValidity())
.then(() => {
ctx.setToken(keycloak.token!);
resolve();
})
.catch((error) => {
reject(error);
});
});
};
export const setBearerToken = (config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> => {
if (!isAuthEnabled()) {
return Promise.resolve(config);
}
return new Promise<InternalAxiosRequestConfig>((resolve, reject) => {
updateKeycloakToken()
.then(() => {
config.headers!.Authorization = "Bearer " + keycloak.token;
resolve(config);
})
.catch((error) => reject(error));
});
};
export const appRenderWithAxiosInterceptorConfig = async (
appRender: (ctx: UserContext) => void,
onLoadFailure: () => void
): Promise<void> => {
await loadSecurityContext(() => {
appRender(getLoadedSecurityContext());
}, onLoadFailure);
if (isAuthEnabled()) {
axios.interceptors.response.use(
(response) => response,
(error) => {
/* istanbul ignore else */
if (error.response.status === 401) {
// if token expired - log the user out
handleLogout();
}
return Promise.reject(error);
}
);
axios.interceptors.request.use(
(config) => setBearerToken(config),
(error) => {
/* tslint:disable:no-floating-promises */
Promise.reject(error);
/* tslint:enable:no-floating-promises */
}
);
}
};
export const handleLogout = (): void => {
currentSecurityContext = undefined;
/* istanbul ignore else */
if (keycloak) {
keycloak.logout();
}
};