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 java.lang.reflect.Method;
022import java.security.Principal;
023import java.util.List;
024
025import javax.servlet.http.HttpServletResponse;
026import javax.servlet.http.HttpSession;
027
028import org.apache.catalina.authenticator.FormAuthenticator;
029import org.apache.catalina.connector.Request;
030import org.apache.catalina.deploy.LoginConfig;
031import org.apache.catalina.realm.GenericPrincipal;
032import org.keycloak.KeycloakPrincipal;
033import org.keycloak.adapters.AdapterTokenStore;
034import org.keycloak.adapters.AuthChallenge;
035import org.keycloak.adapters.AuthOutcome;
036import org.keycloak.adapters.KeycloakDeployment;
037import org.keycloak.adapters.OAuthRequestAuthenticator;
038import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
039import org.keycloak.adapters.RequestAuthenticator;
040import org.keycloak.adapters.tomcat.CatalinaCookieTokenStore;
041import org.keycloak.adapters.tomcat.CatalinaHttpFacade;
042import org.keycloak.adapters.tomcat.CatalinaSessionTokenStore;
043import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement;
044import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
045import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
046import org.keycloak.enums.TokenStore;
047import org.keycloak.representations.AccessToken;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051/**
052 * @since 7.4
053 */
054
055public class KeycloakRequestAuthenticator extends RequestAuthenticator {
056
057    private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakRequestAuthenticator.class);
058
059    public static final String KEYCLOAK_ACCESS_TOKEN = "KEYCLOAK_ACCESS_TOKEN";
060
061    private CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
062
063    protected Request request;
064
065    protected HttpServletResponse response;
066
067    protected LoginConfig loginConfig;
068
069    public KeycloakRequestAuthenticator(Request request, HttpServletResponse response, CatalinaHttpFacade facade,
070            KeycloakDeployment deployment) {
071        super(facade, deployment);
072        this.request = request;
073        this.response = response;
074        tokenStore = getTokenStore();
075        sslRedirectPort = request.getConnector().getRedirectPort();
076    }
077
078    @Override
079    public AuthOutcome authenticate() {
080        AuthOutcome outcome = super.authenticate();
081        if (outcome == AuthOutcome.AUTHENTICATED) {
082            return AuthOutcome.AUTHENTICATED;
083        }
084        AuthChallenge challenge = getChallenge();
085        if (challenge != null) {
086            if (loginConfig == null) {
087                loginConfig = request.getContext().getLoginConfig();
088            }
089            if (challenge.errorPage()) {
090                if (forwardToErrorPageInternal(request, response, loginConfig)) {
091                    return AuthOutcome.FAILED;
092                }
093            }
094            challenge.challenge(facade);
095        }
096        return AuthOutcome.FAILED;
097    }
098
099    protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) {
100        if (loginConfig == null) {
101            return false;
102        }
103        LoginConfig config = (LoginConfig) loginConfig;
104        if (config.getErrorPage() == null) {
105            return false;
106        }
107        try {
108            Method method = FormAuthenticator.class.getDeclaredMethod("forwardToErrorPage", Request.class,
109                    HttpServletResponse.class, LoginConfig.class);
110            method.setAccessible(true);
111            method.invoke(this, request, response, config);
112        } catch (Exception e) {
113            String message = "Error occurred during Keycloak authentication";
114            LOGGER.error(message, e);
115            throw new RuntimeException(message, e);
116        }
117        return true;
118    }
119
120    protected GenericPrincipalFactory createPrincipalFactory() {
121        return new GenericPrincipalFactory() {
122            @Override
123            protected GenericPrincipal createPrincipal(Principal userPrincipal, List<String> roles) {
124                return new GenericPrincipal(userPrincipal.getName(), null, roles, userPrincipal, null);
125            }
126        };
127    }
128
129    protected AdapterTokenStore getTokenStore() {
130        final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
131
132        AdapterTokenStore store = (AdapterTokenStore) request.getNote(TOKEN_STORE_NOTE);
133        if (store != null) {
134            return store;
135        }
136
137        if (deployment.getTokenStore() == TokenStore.SESSION) {
138            store = new CatalinaSessionTokenStore(request, deployment, userSessionManagement, createPrincipalFactory(),
139                    new KeycloakAuthenticatorValve());
140        } else {
141            store = new CatalinaCookieTokenStore(request, facade, deployment, createPrincipalFactory());
142        }
143
144        request.setNote(TOKEN_STORE_NOTE, store);
145        return store;
146    }
147
148    @Override
149    protected OAuthRequestAuthenticator createOAuthAuthenticator() {
150        return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore);
151    }
152
153    @Override
154    protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
155        final AccessToken token = skp.getKeycloakSecurityContext().getToken();
156        request.setAttribute(KEYCLOAK_ACCESS_TOKEN, token);
157    }
158
159    @Override
160    protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp, String method) {
161        completeOAuthAuthentication(skp);
162    }
163
164    @Override
165    protected String getHttpSessionId(boolean create) {
166        HttpSession session = request.getSession(create);
167        return session != null ? session.getId() : null;
168    }
169
170}