001/* 002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * François Maturel 016 */ 017package org.nuxeo.ecm.platform.ui.web.keycloak; 018 019import java.lang.reflect.Method; 020import java.security.Principal; 021import java.util.List; 022 023import javax.servlet.http.HttpServletResponse; 024import javax.servlet.http.HttpSession; 025 026import org.apache.catalina.authenticator.FormAuthenticator; 027import org.apache.catalina.connector.Request; 028import org.apache.catalina.deploy.LoginConfig; 029import org.apache.catalina.realm.GenericPrincipal; 030import org.keycloak.KeycloakPrincipal; 031import org.keycloak.adapters.AdapterTokenStore; 032import org.keycloak.adapters.AuthChallenge; 033import org.keycloak.adapters.AuthOutcome; 034import org.keycloak.adapters.KeycloakDeployment; 035import org.keycloak.adapters.OAuthRequestAuthenticator; 036import org.keycloak.adapters.RefreshableKeycloakSecurityContext; 037import org.keycloak.adapters.RequestAuthenticator; 038import org.keycloak.adapters.tomcat.CatalinaCookieTokenStore; 039import org.keycloak.adapters.tomcat.CatalinaHttpFacade; 040import org.keycloak.adapters.tomcat.CatalinaSessionTokenStore; 041import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement; 042import org.keycloak.adapters.tomcat.GenericPrincipalFactory; 043import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve; 044import org.keycloak.enums.TokenStore; 045import org.keycloak.representations.AccessToken; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049/** 050 * @since 7.4 051 */ 052 053public class KeycloakRequestAuthenticator extends RequestAuthenticator { 054 055 private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakRequestAuthenticator.class); 056 057 public static final String KEYCLOAK_ACCESS_TOKEN = "KEYCLOAK_ACCESS_TOKEN"; 058 059 private CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); 060 061 protected Request request; 062 063 protected HttpServletResponse response; 064 065 protected LoginConfig loginConfig; 066 067 public KeycloakRequestAuthenticator(Request request, HttpServletResponse response, CatalinaHttpFacade facade, 068 KeycloakDeployment deployment) { 069 super(facade, deployment); 070 this.request = request; 071 this.response = response; 072 tokenStore = getTokenStore(); 073 sslRedirectPort = request.getConnector().getRedirectPort(); 074 } 075 076 @Override 077 public AuthOutcome authenticate() { 078 AuthOutcome outcome = super.authenticate(); 079 if (outcome == AuthOutcome.AUTHENTICATED) { 080 return AuthOutcome.AUTHENTICATED; 081 } 082 AuthChallenge challenge = getChallenge(); 083 if (challenge != null) { 084 if (loginConfig == null) { 085 loginConfig = request.getContext().getLoginConfig(); 086 } 087 if (challenge.errorPage()) { 088 if (forwardToErrorPageInternal(request, response, loginConfig)) { 089 return AuthOutcome.FAILED; 090 } 091 } 092 challenge.challenge(facade); 093 } 094 return AuthOutcome.FAILED; 095 } 096 097 protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) { 098 if (loginConfig == null) { 099 return false; 100 } 101 LoginConfig config = (LoginConfig) loginConfig; 102 if (config.getErrorPage() == null) { 103 return false; 104 } 105 try { 106 Method method = FormAuthenticator.class.getDeclaredMethod("forwardToErrorPage", Request.class, 107 HttpServletResponse.class, LoginConfig.class); 108 method.setAccessible(true); 109 method.invoke(this, request, response, config); 110 } catch (Exception e) { 111 String message = "Error occurred during Keycloak authentication"; 112 LOGGER.error(message, e); 113 throw new RuntimeException(message, e); 114 } 115 return true; 116 } 117 118 protected GenericPrincipalFactory createPrincipalFactory() { 119 return new GenericPrincipalFactory() { 120 @Override 121 protected GenericPrincipal createPrincipal(Principal userPrincipal, List<String> roles) { 122 return new GenericPrincipal(userPrincipal.getName(), null, roles, userPrincipal, null); 123 } 124 }; 125 } 126 127 protected AdapterTokenStore getTokenStore() { 128 final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; 129 130 AdapterTokenStore store = (AdapterTokenStore) request.getNote(TOKEN_STORE_NOTE); 131 if (store != null) { 132 return store; 133 } 134 135 if (deployment.getTokenStore() == TokenStore.SESSION) { 136 store = new CatalinaSessionTokenStore(request, deployment, userSessionManagement, createPrincipalFactory(), 137 new KeycloakAuthenticatorValve()); 138 } else { 139 store = new CatalinaCookieTokenStore(request, facade, deployment, createPrincipalFactory()); 140 } 141 142 request.setNote(TOKEN_STORE_NOTE, store); 143 return store; 144 } 145 146 @Override 147 protected OAuthRequestAuthenticator createOAuthAuthenticator() { 148 return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); 149 } 150 151 @Override 152 protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) { 153 final AccessToken token = skp.getKeycloakSecurityContext().getToken(); 154 request.setAttribute(KEYCLOAK_ACCESS_TOKEN, token); 155 } 156 157 @Override 158 protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp, String method) { 159 completeOAuthAuthentication(skp); 160 } 161 162 @Override 163 protected String getHttpSessionId(boolean create) { 164 HttpSession session = request.getSession(create); 165 return session != null ? session.getId() : null; 166 } 167 168}