/*
* 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.services.resources;
import java.net.URI;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.BrowserHistoryHelper;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
public class SessionCodeChecks {
private static final Logger logger = Logger.getLogger(SessionCodeChecks.class);
private AuthenticationSessionModel authSession;
private ClientSessionCode<AuthenticationSessionModel> clientCode;
private Response response;
private boolean actionRequest;
private final RealmModel realm;
private final UriInfo uriInfo;
private final ClientConnection clientConnection;
private final KeycloakSession session;
private final EventBuilder event;
private final String code;
private final String execution;
private final String flowPath;
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String flowPath) {
this.realm = realm;
this.uriInfo = uriInfo;
this.clientConnection = clientConnection;
this.session = session;
this.event = event;
this.code = code;
this.execution = execution;
this.flowPath = flowPath;
}
public AuthenticationSessionModel getAuthenticationSession() {
return authSession;
}
private boolean failed() {
return response != null;
}
public Response getResponse() {
return response;
}
public ClientSessionCode<AuthenticationSessionModel> getClientCode() {
return clientCode;
}
public boolean isActionRequest() {
return actionRequest;
}
private boolean checkSsl() {
if (uriInfo.getBaseUri().getScheme().equals("https")) {
return true;
} else {
return !realm.getSslRequired().isRequired(clientConnection);
}
}
public AuthenticationSessionModel initialVerifyAuthSession() {
// Basic realm checks
if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED);
response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
return null;
}
if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED);
response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
return null;
}
// object retrieve
AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, AuthenticationSessionModel.class);
if (authSession != null) {
return authSession;
}
// See if we are already authenticated and userSession with same ID exists.
String sessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm);
if (sessionId != null) {
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
if (userSession != null) {
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ALREADY_LOGGED_IN);
ClientModel client = null;
String lastClientUuid = userSession.getNote(AuthenticationManager.LAST_AUTHENTICATED_CLIENT);
if (lastClientUuid != null) {
client = realm.getClientById(lastClientUuid);
}
if (client != null) {
session.getContext().setClient(client);
} else {
loginForm.setAttribute("skipLink", true);
}
response = loginForm.createInfoPage();
return null;
}
}
// Otherwise just try to restart from the cookie
response = restartAuthenticationSessionFromCookie();
return null;
}
public boolean initialVerify() {
// Basic realm checks and authenticationSession retrieve
authSession = initialVerifyAuthSession();
if (authSession == null) {
return false;
}
// Check cached response from previous action request
response = BrowserHistoryHelper.getInstance().loadSavedResponse(session, authSession);
if (response != null) {
return false;
}
// Client checks
event.detail(Details.CODE_ID, authSession.getId());
ClientModel client = authSession.getClient();
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER);
clientCode.removeExpiredClientSession();
return false;
}
event.client(client);
session.getContext().setClient(client);
if (!client.isEnabled()) {
event.error(Errors.CLIENT_DISABLED);
response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
clientCode.removeExpiredClientSession();
return false;
}
// Check if it's action or not
if (code == null) {
String lastExecFromSession = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
// Check if we transitted between flows (eg. clicking "register" on login screen)
if (execution==null && !flowPath.equals(lastFlow)) {
logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", flowPath, lastFlow);
// Don't allow moving to different flow if I am on requiredActions already
if (ClientSessionModel.Action.AUTHENTICATE.name().equals(authSession.getAction())) {
authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath);
authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
lastExecFromSession = null;
}
}
if (ObjectUtil.isEqualOrBothNull(execution, lastExecFromSession)) {
// Allow refresh of previous page
clientCode = new ClientSessionCode<>(session, realm, authSession);
actionRequest = false;
return true;
} else {
response = showPageExpired(authSession);
return false;
}
} else {
ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, AuthenticationSessionModel.class);
clientCode = result.getCode();
if (clientCode == null) {
// In case that is replayed action, but sent to the same FORM like actual FORM, we just re-render the page
if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
URI redirectUri = getLastExecutionUrl(latestFlowPath, execution);
logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
response = Response.status(Response.Status.FOUND).location(redirectUri).build();
} else {
response = showPageExpired(authSession);
}
return false;
}
actionRequest = true;
if (execution != null) {
authSession.setAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION, execution);
}
return true;
}
}
public boolean verifyActiveAndValidAction(String expectedAction, ClientSessionCode.ActionType actionType) {
if (failed()) {
return false;
}
if (!isActionActive(actionType)) {
return false;
}
if (!clientCode.isValidAction(expectedAction)) {
AuthenticationSessionModel authSession = getAuthenticationSession();
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(authSession.getAction())) {
logger.debugf("Incorrect action '%s' . User authenticated already.", authSession.getAction());
response = showPageExpired(authSession);
return false;
} else {
logger.errorf("Bad action. Expected action '%s', current action '%s'", expectedAction, authSession.getAction());
response = ErrorPage.error(session, Messages.EXPIRED_CODE);
return false;
}
}
return true;
}
private boolean isActionActive(ClientSessionCode.ActionType actionType) {
if (!clientCode.isActionActive(actionType)) {
event.clone().error(Errors.EXPIRED_CODE);
AuthenticationProcessor.resetFlow(authSession, LoginActionsService.AUTHENTICATE_PATH);
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT);
URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null);
logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri);
response = Response.status(Response.Status.FOUND).location(redirectUri).build();
return false;
}
return true;
}
public boolean verifyRequiredAction(String executedAction) {
if (failed()) {
return false;
}
if (!clientCode.isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
logger.debugf("Expected required action, but session action is '%s' . Showing expired page now.", authSession.getAction());
event.error(Errors.INVALID_CODE);
response = showPageExpired(authSession);
return false;
}
if (!isActionActive(ClientSessionCode.ActionType.USER)) {
return false;
}
if (actionRequest) {
String currentRequiredAction = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
if (executedAction == null || !executedAction.equals(currentRequiredAction)) {
logger.debug("required action doesn't match current required action");
response = redirectToRequiredActions(currentRequiredAction);
return false;
}
}
return true;
}
private Response restartAuthenticationSessionFromCookie() {
logger.debug("Authentication session not found. Trying to restart from cookie.");
AuthenticationSessionModel authSession = null;
try {
authSession = RestartLoginCookie.restartSession(session, realm);
} catch (Exception e) {
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
}
if (authSession != null) {
event.clone();
event.detail(Details.RESTART_AFTER_TIMEOUT, "true");
event.error(Errors.EXPIRED_CODE);
String warningMessage = Messages.LOGIN_TIMEOUT;
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, warningMessage);
String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
if (flowPath == null) {
flowPath = LoginActionsService.AUTHENTICATE_PATH;
}
URI redirectUri = getLastExecutionUrl(flowPath, null);
logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri);
return Response.status(Response.Status.FOUND).location(redirectUri).build();
} else {
// Finally need to show error as all the fallbacks failed
event.error(Errors.INVALID_CODE);
return ErrorPage.error(session, Messages.INVALID_CODE);
}
}
private Response redirectToRequiredActions(String action) {
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
.path(LoginActionsService.REQUIRED_ACTION);
if (action != null) {
uriBuilder.queryParam("execution", action);
}
URI redirect = uriBuilder.build(realm.getName());
return Response.status(302).location(redirect).build();
}
private URI getLastExecutionUrl(String flowPath, String executionId) {
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
.getLastExecutionUrl(flowPath, executionId);
}
private Response showPageExpired(AuthenticationSessionModel authSession) {
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
.showPageExpired(authSession);
}
}