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