/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.keycloak.protocol; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.common.ClientConnection; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.AuthenticationFlowURLHelper; import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; /** * Common base class for Authorization REST endpoints implementation, which have to be implemented by each protocol. * * @author Vlastimil Elias (velias at redhat dot com) */ public abstract class AuthorizationEndpointBase { private static final Logger logger = Logger.getLogger(AuthorizationEndpointBase.class); public static final String APP_INITIATED_FLOW = "APP_INITIATED_FLOW"; protected RealmModel realm; protected EventBuilder event; protected AuthenticationManager authManager; @Context protected UriInfo uriInfo; @Context protected HttpHeaders headers; @Context protected HttpRequest request; @Context protected KeycloakSession session; @Context protected ClientConnection clientConnection; public AuthorizationEndpointBase(RealmModel realm, EventBuilder event) { this.realm = realm; this.event = event; } protected AuthenticationProcessor createProcessor(AuthenticationSessionModel authSession, String flowId, String flowPath) { AuthenticationProcessor processor = new AuthenticationProcessor(); processor.setAuthenticationSession(authSession) .setFlowPath(flowPath) .setFlowId(flowId) .setBrowserFlow(true) .setConnection(clientConnection) .setEventBuilder(event) .setRealm(realm) .setSession(session) .setUriInfo(uriInfo) .setRequest(request); authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath); return processor; } /** * Common method to handle browser authentication request in protocols unified way. * * @param authSession for current request * @param protocol handler for protocol used to initiate login * @param isPassive set to true if login should be passive (without login screen shown) * @param redirectToAuthentication if true redirect to flow url. If initial call to protocol is a POST, you probably want to do this. This is so we can disable the back button on browser * @return response to be returned to the browser */ protected Response handleBrowserAuthenticationRequest(AuthenticationSessionModel authSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) { AuthenticationFlowModel flow = getAuthenticationFlow(); String flowId = flow.getId(); AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH); event.detail(Details.CODE_ID, authSession.getId()); if (isPassive) { // OIDC prompt == NONE or SAML 2 IsPassive flag // This means that client is just checking if the user is already completely logged in. // We cancel login if any authentication action or required action is required try { if (processor.authenticateOnly() == null) { // processor.attachSession(); } else { Response response = protocol.sendError(authSession, Error.PASSIVE_LOGIN_REQUIRED); return response; } AuthenticationManager.setRolesAndMappersInSession(authSession); if (processor.nextRequiredAction() != null) { Response response = protocol.sendError(authSession, Error.PASSIVE_INTERACTION_REQUIRED); return response; } // Attach session once no requiredActions or other things are required processor.attachSession(); } catch (Exception e) { return processor.handleBrowserException(e); } return processor.finishAuthentication(protocol); } else { try { RestartLoginCookie.setRestartCookie(session, realm, clientConnection, uriInfo, authSession); if (redirectToAuthentication) { return processor.redirectToFlow(); } return processor.authenticate(); } catch (Exception e) { return processor.handleBrowserException(e); } } } protected AuthenticationFlowModel getAuthenticationFlow() { return realm.getBrowserFlow(); } protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) { AuthenticationSessionManager manager = new AuthenticationSessionManager(session); String authSessionId = manager.getCurrentAuthenticationSessionId(realm); AuthenticationSessionModel authSession = authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId); if (authSession != null) { ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession); if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) { logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", authSession.getId()); authSession.restartSession(realm, client); return new AuthorizationEndpointChecks(authSession); } else if (isNewRequest(authSession, client, requestState)) { // Check if we have lastProcessedExecution and restart the session just if yes. Otherwise update just client information from the AuthorizationEndpoint request. // This difference is needed, because of logout from JS applications in multiple browser tabs. if (hasProcessedExecution(authSession)) { logger.debug("New request from application received, but authentication session already exists. Restart existing authentication session"); authSession.restartSession(realm, client); } else { logger.debug("New request from application received, but authentication session already exists. Update client information in existing authentication session"); authSession.clearClientNotes(); // update client data authSession.updateClient(client); } return new AuthorizationEndpointChecks(authSession); } else { logger.debug("Re-sent some previous request to Authorization endpoint. Likely browser 'back' or 'refresh' button."); // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form if (!shouldShowExpirePage(authSession)) { return new AuthorizationEndpointChecks(authSession); } else { CacheControlUtil.noBackButtonCacheControlHeader(); Response response = new AuthenticationFlowURLHelper(session, realm, uriInfo) .showPageExpired(authSession); return new AuthorizationEndpointChecks(response); } } } UserSessionModel userSession = authSessionId==null ? null : session.sessions().getUserSession(realm, authSessionId); if (userSession != null) { logger.debugf("Sent request to authz endpoint. We don't have authentication session with ID '%s' but we have userSession. Will re-create authentication session with same ID", authSessionId); authSession = session.authenticationSessions().createAuthenticationSession(authSessionId, realm, client); } else { authSession = manager.createAuthenticationSession(realm, client, true); logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", authSession.getId()); } return new AuthorizationEndpointChecks(authSession); } private boolean hasProcessedExecution(AuthenticationSessionModel authSession) { String lastProcessedExecution = authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION); return (lastProcessedExecution != null); } // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form private boolean shouldShowExpirePage(AuthenticationSessionModel authSession) { if (hasProcessedExecution(authSession)) { return true; } String initialFlow = authSession.getClientNote(APP_INITIATED_FLOW); if (initialFlow == null) { initialFlow = LoginActionsService.AUTHENTICATE_PATH; } String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH); // Check if we transitted between flows (eg. clicking "register" on login screen and then clicking browser 'back', which showed this page) if (!initialFlow.equals(lastFlow) && AuthenticationSessionModel.Action.AUTHENTICATE.toString().equals(authSession.getAction())) { logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", initialFlow, lastFlow); authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, initialFlow); authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); return false; } return false; } // Try to see if it is new request from the application, or refresh of some previous request protected abstract boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestState); protected static class AuthorizationEndpointChecks { public final AuthenticationSessionModel authSession; public final Response response; private AuthorizationEndpointChecks(Response response) { this.authSession = null; this.response = response; } private AuthorizationEndpointChecks(AuthenticationSessionModel authSession) { this.authSession = authSession; this.response = null; } } }