/*
* (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* François Maturel
*/
package org.nuxeo.ecm.platform.ui.web.keycloak;
import static org.nuxeo.ecm.platform.ui.web.keycloak.KeycloakUserInfo.KeycloakUserInfoBuilder.aKeycloakUserInfo;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.representations.AccessToken;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.usermapper.service.UserMapperService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Authentication plugin for handling auth flow with Keyloack
*
* @since 7.4
*/
public class KeycloakAuthenticationPlugin implements NuxeoAuthenticationPlugin,
NuxeoAuthenticationPluginLogoutExtension {
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationPlugin.class);
private static final String PROTOCOL_CLASSPATH = "classpath:";
public static final String KEYCLOAK_CONFIG_FILE_KEY = "keycloakConfigFilename";
public static final String KEYCLOAK_MAPPING_NAME_KEY = "mappingName";
public static final String DEFAULT_MAPPING_NAME = "keycloak";
private String keycloakConfigFile = PROTOCOL_CLASSPATH + "keycloak.json";
private KeycloakAuthenticatorProvider keycloakAuthenticatorProvider;
protected String mappingName = DEFAULT_MAPPING_NAME;
@Override
public void initPlugin(Map<String, String> parameters) {
LOGGER.info("INITIALIZE KEYCLOAK");
if (parameters.containsKey(KEYCLOAK_CONFIG_FILE_KEY)) {
keycloakConfigFile = PROTOCOL_CLASSPATH + parameters.get(KEYCLOAK_CONFIG_FILE_KEY);
}
if (parameters.containsKey(KEYCLOAK_MAPPING_NAME_KEY)) {
mappingName = parameters.get(KEYCLOAK_MAPPING_NAME_KEY);
}
InputStream is = loadKeycloakConfigFile();
KeycloakDeployment kd = KeycloakNuxeoDeployment.build(is);
keycloakAuthenticatorProvider = new KeycloakAuthenticatorProvider(new AdapterDeploymentContext(kd));
LOGGER.info("Keycloak is using a per-deployment configuration loaded from: " + keycloakConfigFile);
}
@Override
public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
return Boolean.TRUE;
}
@Override
public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
return Boolean.TRUE;
}
@Override
public List<String> getUnAuthenticatedURLPrefix() {
// There are no unauthenticated URLs associated to login prompt.
// If user is not authenticated, this plugin will have to redirect user to the keycloak sso login prompt
return null;
}
@Override
public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
LOGGER.debug("KEYCLOAK will handle identification");
KeycloakRequestAuthenticator authenticator = keycloakAuthenticatorProvider.provide(httpRequest, httpResponse);
KeycloakDeployment deployment = keycloakAuthenticatorProvider.getResolvedDeployment();
String keycloakNuxeoApp = deployment.getResourceName();
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
AccessToken token = (AccessToken) httpRequest.getAttribute(KeycloakRequestAuthenticator.KEYCLOAK_ACCESS_TOKEN);
KeycloakUserInfo keycloakUserInfo = getKeycloakUserInfo(token);
UserMapperService ums = Framework.getService(UserMapperService.class);
keycloakUserInfo.setRoles(getRoles(token, keycloakNuxeoApp));
ums.getOrCreateAndUpdateNuxeoPrincipal(mappingName, keycloakUserInfo);
return keycloakUserInfo;
}
return null;
}
@Override
public Boolean handleLogout(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
LOGGER.debug("KEYCLOAK will handle logout");
String uri = keycloakAuthenticatorProvider.logout(httpRequest, httpResponse);
try {
httpResponse.sendRedirect(uri);
} catch (IOException e) {
String message = "Could note handle logout with URI: " + uri;
LOGGER.error(message);
throw new RuntimeException(message);
}
return Boolean.TRUE;
}
/**
* Get keycloak user's information from authentication token
*
* @param token the keycoak authentication token
* @return keycloak user's information
*/
private KeycloakUserInfo getKeycloakUserInfo(AccessToken token) {
return aKeycloakUserInfo()
// Required
.withUserName(token.getEmail())
// Optional
.withFirstName(token.getGivenName()).withLastName(token.getFamilyName()).withCompany(
token.getPreferredUsername()).withAuthPluginName("KEYCLOAK_AUTH")
// The password is randomly generated has we won't use it
.withPassword(UUID.randomUUID().toString()).build();
}
/**
* Get keycloak user's roles from authentication token
*
* @param token the keycoak authentication token
* @param keycloakNuxeoApp the keycoak resource name
* @return keycloak user's roles
*/
private Set<String> getRoles(AccessToken token, String keycloakNuxeoApp) {
Set<String> allRoles = new HashSet<>();
allRoles.addAll(token.getRealmAccess().getRoles());
AccessToken.Access nuxeoResource = token.getResourceAccess(keycloakNuxeoApp);
if (nuxeoResource != null) {
Set<String> nuxeoRoles = nuxeoResource.getRoles();
allRoles.addAll(nuxeoRoles);
}
return allRoles;
}
/**
* Loads Keycloak from configuration file
*
* @return the configuration file as an {@link InputStream}
*/
private InputStream loadKeycloakConfigFile() {
if (keycloakConfigFile.startsWith(PROTOCOL_CLASSPATH)) {
String classPathLocation = keycloakConfigFile.replace(PROTOCOL_CLASSPATH, "");
LOGGER.debug("Loading config from classpath on location: " + classPathLocation);
// Try current class classloader first
InputStream is = getClass().getClassLoader().getResourceAsStream(classPathLocation);
if (is == null) {
is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation);
}
if (is != null) {
return is;
} else {
String message = "Unable to find config from classpath: " + keycloakConfigFile;
LOGGER.error(message);
throw new RuntimeException(message);
}
} else {
// Fallback to file
try {
LOGGER.debug("Loading config from file: " + keycloakConfigFile);
return new FileInputStream(keycloakConfigFile);
} catch (FileNotFoundException fnfe) {
String message = "Config not found on " + keycloakConfigFile;
LOGGER.error(message);
throw new RuntimeException(message, fnfe);
}
}
}
public void setKeycloakAuthenticatorProvider(KeycloakAuthenticatorProvider keycloakAuthenticatorProvider) {
this.keycloakAuthenticatorProvider = keycloakAuthenticatorProvider;
}
}