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}