001/* 002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * François Maturel 018 */ 019package org.nuxeo.ecm.platform.ui.web.keycloak; 020 021import static org.nuxeo.ecm.platform.ui.web.keycloak.KeycloakUserInfo.KeycloakUserInfoBuilder.aKeycloakUserInfo; 022 023import java.io.FileInputStream; 024import java.io.FileNotFoundException; 025import java.io.IOException; 026import java.io.InputStream; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Map; 030import java.util.Set; 031import java.util.UUID; 032 033import javax.servlet.http.HttpServletRequest; 034import javax.servlet.http.HttpServletResponse; 035 036import org.keycloak.adapters.AdapterDeploymentContext; 037import org.keycloak.adapters.AuthOutcome; 038import org.keycloak.adapters.KeycloakDeployment; 039import org.keycloak.representations.AccessToken; 040import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo; 041import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin; 042import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension; 043import org.nuxeo.runtime.api.Framework; 044import org.nuxeo.usermapper.service.UserMapperService; 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047 048/** 049 * Authentication plugin for handling auth flow with Keyloack 050 * 051 * @since 7.4 052 */ 053 054public class KeycloakAuthenticationPlugin implements NuxeoAuthenticationPlugin, 055 NuxeoAuthenticationPluginLogoutExtension { 056 057 private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationPlugin.class); 058 059 private static final String PROTOCOL_CLASSPATH = "classpath:"; 060 061 public static final String KEYCLOAK_CONFIG_FILE_KEY = "keycloakConfigFilename"; 062 063 public static final String KEYCLOAK_MAPPING_NAME_KEY = "mappingName"; 064 065 public static final String DEFAULT_MAPPING_NAME = "keycloak"; 066 067 private String keycloakConfigFile = PROTOCOL_CLASSPATH + "keycloak.json"; 068 069 private KeycloakAuthenticatorProvider keycloakAuthenticatorProvider; 070 071 protected String mappingName = DEFAULT_MAPPING_NAME; 072 073 @Override 074 public void initPlugin(Map<String, String> parameters) { 075 LOGGER.info("INITIALIZE KEYCLOAK"); 076 077 if (parameters.containsKey(KEYCLOAK_CONFIG_FILE_KEY)) { 078 keycloakConfigFile = PROTOCOL_CLASSPATH + parameters.get(KEYCLOAK_CONFIG_FILE_KEY); 079 } 080 081 if (parameters.containsKey(KEYCLOAK_MAPPING_NAME_KEY)) { 082 mappingName = parameters.get(KEYCLOAK_MAPPING_NAME_KEY); 083 } 084 085 InputStream is = loadKeycloakConfigFile(); 086 KeycloakDeployment kd = KeycloakNuxeoDeployment.build(is); 087 keycloakAuthenticatorProvider = new KeycloakAuthenticatorProvider(new AdapterDeploymentContext(kd)); 088 LOGGER.info("Keycloak is using a per-deployment configuration loaded from: " + keycloakConfigFile); 089 } 090 091 @Override 092 public Boolean needLoginPrompt(HttpServletRequest httpRequest) { 093 return Boolean.TRUE; 094 } 095 096 @Override 097 public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) { 098 return Boolean.TRUE; 099 } 100 101 @Override 102 public List<String> getUnAuthenticatedURLPrefix() { 103 // There are no unauthenticated URLs associated to login prompt. 104 // If user is not authenticated, this plugin will have to redirect user to the keycloak sso login prompt 105 return null; 106 } 107 108 @Override 109 public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest, 110 HttpServletResponse httpResponse) { 111 LOGGER.debug("KEYCLOAK will handle identification"); 112 113 KeycloakRequestAuthenticator authenticator = keycloakAuthenticatorProvider.provide(httpRequest, httpResponse); 114 KeycloakDeployment deployment = keycloakAuthenticatorProvider.getResolvedDeployment(); 115 String keycloakNuxeoApp = deployment.getResourceName(); 116 117 AuthOutcome outcome = authenticator.authenticate(); 118 119 if (outcome == AuthOutcome.AUTHENTICATED) { 120 AccessToken token = (AccessToken) httpRequest.getAttribute(KeycloakRequestAuthenticator.KEYCLOAK_ACCESS_TOKEN); 121 122 KeycloakUserInfo keycloakUserInfo = getKeycloakUserInfo(token); 123 124 UserMapperService ums = Framework.getService(UserMapperService.class); 125 126 keycloakUserInfo.setRoles(getRoles(token, keycloakNuxeoApp)); 127 128 ums.getOrCreateAndUpdateNuxeoPrincipal(mappingName, keycloakUserInfo); 129 130 return keycloakUserInfo; 131 } 132 return null; 133 } 134 135 @Override 136 public Boolean handleLogout(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 137 LOGGER.debug("KEYCLOAK will handle logout"); 138 139 String uri = keycloakAuthenticatorProvider.logout(httpRequest, httpResponse); 140 try { 141 httpResponse.sendRedirect(uri); 142 } catch (IOException e) { 143 String message = "Could note handle logout with URI: " + uri; 144 LOGGER.error(message); 145 throw new RuntimeException(message); 146 } 147 return Boolean.TRUE; 148 } 149 150 /** 151 * Get keycloak user's information from authentication token 152 * 153 * @param token the keycoak authentication token 154 * @return keycloak user's information 155 */ 156 private KeycloakUserInfo getKeycloakUserInfo(AccessToken token) { 157 return aKeycloakUserInfo() 158 // Required 159 .withUserName(token.getEmail()) 160 // Optional 161 .withFirstName(token.getGivenName()).withLastName(token.getFamilyName()).withCompany( 162 token.getPreferredUsername()).withAuthPluginName("KEYCLOAK_AUTH") 163 // The password is randomly generated has we won't use it 164 .withPassword(UUID.randomUUID().toString()).build(); 165 } 166 167 /** 168 * Get keycloak user's roles from authentication token 169 * 170 * @param token the keycoak authentication token 171 * @param keycloakNuxeoApp the keycoak resource name 172 * @return keycloak user's roles 173 */ 174 private Set<String> getRoles(AccessToken token, String keycloakNuxeoApp) { 175 Set<String> allRoles = new HashSet<>(); 176 allRoles.addAll(token.getRealmAccess().getRoles()); 177 AccessToken.Access nuxeoResource = token.getResourceAccess(keycloakNuxeoApp); 178 if (nuxeoResource != null) { 179 Set<String> nuxeoRoles = nuxeoResource.getRoles(); 180 allRoles.addAll(nuxeoRoles); 181 } 182 return allRoles; 183 } 184 185 /** 186 * Loads Keycloak from configuration file 187 * 188 * @return the configuration file as an {@link InputStream} 189 */ 190 private InputStream loadKeycloakConfigFile() { 191 192 if (keycloakConfigFile.startsWith(PROTOCOL_CLASSPATH)) { 193 String classPathLocation = keycloakConfigFile.replace(PROTOCOL_CLASSPATH, ""); 194 195 LOGGER.debug("Loading config from classpath on location: " + classPathLocation); 196 197 // Try current class classloader first 198 InputStream is = getClass().getClassLoader().getResourceAsStream(classPathLocation); 199 if (is == null) { 200 is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation); 201 } 202 203 if (is != null) { 204 return is; 205 } else { 206 String message = "Unable to find config from classpath: " + keycloakConfigFile; 207 LOGGER.error(message); 208 throw new RuntimeException(message); 209 } 210 } else { 211 // Fallback to file 212 try { 213 LOGGER.debug("Loading config from file: " + keycloakConfigFile); 214 return new FileInputStream(keycloakConfigFile); 215 } catch (FileNotFoundException fnfe) { 216 String message = "Config not found on " + keycloakConfigFile; 217 LOGGER.error(message); 218 throw new RuntimeException(message, fnfe); 219 } 220 } 221 } 222 223 public void setKeycloakAuthenticatorProvider(KeycloakAuthenticatorProvider keycloakAuthenticatorProvider) { 224 this.keycloakAuthenticatorProvider = keycloakAuthenticatorProvider; 225 } 226}