/*
* 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 org.keycloak.authentication.actiontoken.DefaultActionToken;
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.TokenVerifier;
import org.keycloak.authentication.actiontoken.*;
import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.exceptions.TokenNotActiveException;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
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.CacheControlUtil;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.services.util.BrowserHistoryHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.net.URI;
import java.util.Map;
import javax.ws.rs.core.*;
import static org.keycloak.authentication.actiontoken.DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginActionsService {
private static final Logger logger = Logger.getLogger(LoginActionsService.class);
public static final String AUTHENTICATE_PATH = "authenticate";
public static final String REGISTRATION_PATH = "registration";
public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
public static final String REQUIRED_ACTION = "required-action";
public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login";
public static final String POST_BROKER_LOGIN_PATH = "post-broker-login";
public static final String RESTART_PATH = "restart";
public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage";
private RealmModel realm;
@Context
private HttpRequest request;
@Context
protected HttpHeaders headers;
@Context
private UriInfo uriInfo;
@Context
private ClientConnection clientConnection;
@Context
protected Providers providers;
@Context
protected KeycloakSession session;
private EventBuilder event;
public static UriBuilder loginActionsBaseUrl(UriInfo uriInfo) {
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return loginActionsBaseUrl(baseUriBuilder);
}
public static UriBuilder authenticationFormProcessor(UriInfo uriInfo) {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "authenticateForm");
}
public static UriBuilder requiredActionProcessor(UriInfo uriInfo) {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "requiredActionPOST");
}
public static UriBuilder actionTokenProcessor(UriInfo uriInfo) {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "executeActionToken");
}
public static UriBuilder registrationFormProcessor(UriInfo uriInfo) {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "processRegister");
}
public static UriBuilder firstBrokerLoginProcessor(UriInfo uriInfo) {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "firstBrokerLoginGet");
}
public static UriBuilder postBrokerLoginProcessor(UriInfo uriInfo) {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "postBrokerLoginGet");
}
public static UriBuilder loginActionsBaseUrl(UriBuilder baseUriBuilder) {
return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getLoginActionsService");
}
public LoginActionsService(RealmModel realm, EventBuilder event) {
this.realm = realm;
this.event = event;
CacheControlUtil.noBackButtonCacheControlHeader();
}
private boolean checkSsl() {
if (uriInfo.getBaseUri().getScheme().equals("https")) {
return true;
} else {
return !realm.getSslRequired().isRequired(clientConnection);
}
}
private SessionCodeChecks checksForCode(String code, String execution, String flowPath) {
SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, code, execution, flowPath);
res.initialVerify();
return res;
}
protected URI getLastExecutionUrl(String flowPath, String executionId) {
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
.getLastExecutionUrl(flowPath, executionId);
}
/**
* protocol independent page for restart of the flow
*
* @return
*/
@Path(RESTART_PATH)
@GET
public Response restartSession() {
event.event(EventType.RESTART_AUTHENTICATION);
SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, null, null, null);
AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
if (authSession == null) {
return checks.getResponse();
}
String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
if (flowPath == null) {
flowPath = AUTHENTICATE_PATH;
}
AuthenticationProcessor.resetFlow(authSession, flowPath);
URI redirectUri = getLastExecutionUrl(flowPath, null);
logger.debugf("Flow restart requested. Redirecting to %s", redirectUri);
return Response.status(Response.Status.FOUND).location(redirectUri).build();
}
/**
* protocol independent login page entry point
*
* @param code
* @return
*/
@Path(AUTHENTICATE_PATH)
@GET
public Response authenticate(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
SessionCodeChecks checks = checksForCode(code, execution, AUTHENTICATE_PATH);
if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse();
}
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
boolean actionRequest = checks.isActionRequest();
return processAuthentication(actionRequest, execution, authSession, null);
}
protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
return processFlow(action, execution, authSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor());
}
protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
processor.setAuthenticationSession(authSession)
.setFlowPath(flowPath)
.setBrowserFlow(true)
.setFlowId(flow.getId())
.setConnection(clientConnection)
.setEventBuilder(event)
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
if (errorMessage != null) {
processor.setForwardedErrorMessage(new FormMessage(null, errorMessage));
}
// Check the forwarded error message, which was set by previous HTTP request
String forwardedErrorMessage = authSession.getAuthNote(FORWARDED_ERROR_MESSAGE_NOTE);
if (forwardedErrorMessage != null) {
authSession.removeAuthNote(FORWARDED_ERROR_MESSAGE_NOTE);
processor.setForwardedErrorMessage(new FormMessage(null, forwardedErrorMessage));
}
Response response;
try {
if (action) {
response = processor.authenticationAction(execution);
} else {
response = processor.authenticate();
}
} catch (WebApplicationException e) {
response = e.getResponse();
authSession = processor.getAuthenticationSession();
} catch (Exception e) {
response = processor.handleBrowserException(e);
authSession = processor.getAuthenticationSession(); // Could be changed (eg. Forked flow)
}
return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action);
}
/**
* URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY!
*
* @param code
* @return
*/
@Path(AUTHENTICATE_PATH)
@POST
public Response authenticateForm(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return authenticate(code, execution);
}
@Path(RESET_CREDENTIALS_PATH)
@POST
public Response resetCredentialsPOST(@QueryParam("code") String code,
@QueryParam("execution") String execution,
@QueryParam(Constants.KEY) String key) {
if (key != null) {
return handleActionToken(key, execution);
}
event.event(EventType.RESET_PASSWORD);
return resetCredentials(code, execution);
}
/**
* Endpoint for executing reset credentials flow. If token is null, a client session is created with the account
* service as the client. Successful reset sends you to the account page. Note, account service must be enabled.
*
* @param code
* @param execution
* @return
*/
@Path(RESET_CREDENTIALS_PATH)
@GET
public Response resetCredentialsGET(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
if (authSession == null && code == null) {
if (!realm.isResetPasswordAllowed()) {
event.event(EventType.RESET_PASSWORD);
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
authSession = createAuthenticationSessionForClient();
return processResetCredentials(false, null, authSession);
}
event.event(EventType.RESET_PASSWORD);
return resetCredentials(code, execution);
}
AuthenticationSessionModel createAuthenticationSessionForClient()
throws UriBuilderException, IllegalArgumentException {
AuthenticationSessionModel authSession;
// set up the account service as the endpoint to call.
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
//authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
authSession.setRedirectUri(redirectUri);
authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
return authSession;
}
/**
* @param code
* @param execution
* @return
*/
protected Response resetCredentials(String code, String execution) {
SessionCodeChecks checks = checksForCode(code, execution, RESET_CREDENTIALS_PATH);
if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.getResponse();
}
final AuthenticationSessionModel authSession = checks.getAuthenticationSession();
if (!realm.isResetPasswordAllowed()) {
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
return processResetCredentials(checks.isActionRequest(), execution, authSession);
}
/**
* Handles a given token using the given token handler. If there is any {@link VerificationException} thrown
* in the handler, it is handled automatically here to reduce boilerplate code.
*
* @param key
* @param execution
* @return
*/
@Path("action-token")
@GET
public Response executeActionToken(@QueryParam("key") String key,
@QueryParam("execution") String execution) {
return handleActionToken(key, execution);
}
protected <T extends DefaultActionToken> Response handleActionToken(String tokenString, String execution) {
T token;
ActionTokenHandler<T> handler;
ActionTokenContext<T> tokenContext;
String eventError = null;
String defaultErrorMessage = null;
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
event.event(EventType.EXECUTE_ACTION_TOKEN);
// First resolve action token handler
try {
if (tokenString == null) {
throw new ExplainedTokenVerificationException(null, Errors.NOT_ALLOWED, Messages.INVALID_REQUEST);
}
TokenVerifier<DefaultActionToken> tokenVerifier = TokenVerifier.create(tokenString, DefaultActionToken.class);
DefaultActionToken aToken = tokenVerifier.getToken();
event
.detail(Details.TOKEN_ID, aToken.getId())
.detail(Details.ACTION, aToken.getActionId())
.user(aToken.getUserId());
handler = resolveActionTokenHandler(aToken.getActionId());
eventError = handler.getDefaultEventError();
defaultErrorMessage = handler.getDefaultErrorMessage();
if (! realm.isEnabled()) {
throw new ExplainedTokenVerificationException(aToken, Errors.REALM_DISABLED, Messages.REALM_NOT_ENABLED);
}
if (! checkSsl()) {
throw new ExplainedTokenVerificationException(aToken, Errors.SSL_REQUIRED, Messages.HTTPS_REQUIRED);
}
tokenVerifier
.withChecks(
// Token introspection checks
TokenVerifier.IS_ACTIVE,
new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())),
ACTION_TOKEN_BASIC_CHECKS
)
.secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
.verify();
token = TokenVerifier.create(tokenString, handler.getTokenClass()).getToken();
} catch (TokenNotActiveException ex) {
if (authSession != null) {
event.clone().error(Errors.EXPIRED_CODE);
String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
if (flowPath == null) {
flowPath = AUTHENTICATE_PATH;
}
AuthenticationProcessor.resetFlow(authSession, flowPath);
return processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT);
}
return handleActionTokenVerificationException(null, ex, Errors.EXPIRED_CODE, defaultErrorMessage);
} catch (ExplainedTokenVerificationException ex) {
return handleActionTokenVerificationException(null, ex, ex.getErrorEvent(), ex.getMessage());
} catch (VerificationException ex) {
return handleActionTokenVerificationException(null, ex, eventError, defaultErrorMessage);
}
// Now proceed with the verification and handle the token
tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
try {
String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token);
if (tokenAuthSessionId != null) {
// This can happen if the token contains ID but user opens the link in a new browser
LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
}
if (authSession == null) {
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
tokenContext.setAuthenticationSession(authSession, true);
} else if (tokenAuthSessionId == null ||
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId)) {
// There exists an authentication session but no auth session ID was received in the action token
logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
tokenContext.setAuthenticationSession(authSession, true);
}
initLoginEvent(authSession);
event.event(handler.eventType());
LoginActionsServiceChecks.checkIsUserValid(token, tokenContext);
LoginActionsServiceChecks.checkIsClientValid(token, tokenContext);
session.getContext().setClient(authSession.getClient());
TokenVerifier.create(token)
.withChecks(handler.getVerifiers(tokenContext))
.verify();
authSession = tokenContext.getAuthenticationSession();
event = tokenContext.getEvent();
event.event(handler.eventType());
if (! handler.canUseTokenRepeatedly(token, tokenContext)) {
LoginActionsServiceChecks.checkTokenWasNotUsedYet(token, tokenContext);
authSession.setAuthNote(AuthenticationManager.INVALIDATE_ACTION_TOKEN, token.serializeKey());
}
authSession.setAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID, token.getUserId());
return handler.handleToken(token, tokenContext);
} catch (ExplainedTokenVerificationException ex) {
return handleActionTokenVerificationException(tokenContext, ex, ex.getErrorEvent(), ex.getMessage());
} catch (LoginActionsServiceException ex) {
Response response = ex.getResponse();
return response == null
? handleActionTokenVerificationException(tokenContext, ex, eventError, defaultErrorMessage)
: response;
} catch (VerificationException ex) {
return handleActionTokenVerificationException(tokenContext, ex, eventError, defaultErrorMessage);
}
}
private <T extends DefaultActionToken> ActionTokenHandler<T> resolveActionTokenHandler(String actionId) throws VerificationException {
if (actionId == null) {
throw new VerificationException("Action token operation not set");
}
ActionTokenHandler<T> handler = session.getProvider(ActionTokenHandler.class, actionId);
if (handler == null) {
throw new VerificationException("Invalid action token operation");
}
return handler;
}
private Response handleActionTokenVerificationException(ActionTokenContext<?> tokenContext, VerificationException ex, String eventError, String errorMessage) {
if (tokenContext != null && tokenContext.getAuthenticationSession() != null) {
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, tokenContext.getAuthenticationSession(), true);
}
event
.detail(Details.REASON, ex == null ? "<unknown>" : ex.getMessage())
.error(eventError == null ? Errors.INVALID_CODE : eventError);
return ErrorPage.error(session, errorMessage == null ? Messages.INVALID_CODE : errorMessage);
}
protected Response processResetCredentials(boolean actionRequest, String execution, AuthenticationSessionModel authSession) {
AuthenticationProcessor authProcessor = new ResetCredentialsActionTokenHandler.ResetCredsAuthenticationProcessor();
return processFlow(actionRequest, execution, authSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), null, authProcessor);
}
protected Response processRegistration(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
return processFlow(action, execution, authSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage, new AuthenticationProcessor());
}
/**
* protocol independent registration page entry point
*
* @param code
* @return
*/
@Path(REGISTRATION_PATH)
@GET
public Response registerPage(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return registerRequest(code, execution, false);
}
/**
* Registration
*
* @param code
* @return
*/
@Path(REGISTRATION_PATH)
@POST
public Response processRegister(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return registerRequest(code, execution, true);
}
private Response registerRequest(String code, String execution, boolean isPostRequest) {
event.event(EventType.REGISTER);
if (!realm.isRegistrationAllowed()) {
event.error(Errors.REGISTRATION_DISABLED);
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
}
SessionCodeChecks checks = checksForCode(code, execution, REGISTRATION_PATH);
if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse();
}
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
AuthenticationManager.expireIdentityCookie(realm, uriInfo, clientConnection);
return processRegistration(checks.isActionRequest(), execution, authSession, null);
}
@Path(FIRST_BROKER_LOGIN_PATH)
@GET
public Response firstBrokerLoginGet(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH);
}
@Path(FIRST_BROKER_LOGIN_PATH)
@POST
public Response firstBrokerLoginPost(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH);
}
@Path(POST_BROKER_LOGIN_PATH)
@GET
public Response postBrokerLoginGet(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH);
}
@Path(POST_BROKER_LOGIN_PATH)
@POST
public Response postBrokerLoginPost(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH);
}
protected Response brokerLoginFlow(String code, String execution, String flowPath) {
boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType);
SessionCodeChecks checks = checksForCode(code, execution, flowPath);
if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse();
}
event.detail(Details.CODE_ID, code);
final AuthenticationSessionModel authSession = checks.getAuthenticationSession();
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, noteKey);
if (serializedCtx == null) {
ServicesLogger.LOGGER.notFoundSerializedCtxInClientSession(noteKey);
throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession."));
}
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, authSession);
final String identityProviderAlias = brokerContext.getIdpConfig().getAlias();
String flowId = firstBrokerLogin ? brokerContext.getIdpConfig().getFirstBrokerLoginFlowId() : brokerContext.getIdpConfig().getPostBrokerLoginFlowId();
if (flowId == null) {
ServicesLogger.LOGGER.flowNotConfigForIDP(identityProviderAlias);
throw new WebApplicationException(ErrorPage.error(session, "Flow not configured for identity provider"));
}
AuthenticationFlowModel brokerLoginFlow = realm.getAuthenticationFlowById(flowId);
if (brokerLoginFlow == null) {
ServicesLogger.LOGGER.flowNotFoundForIDP(flowId, identityProviderAlias);
throw new WebApplicationException(ErrorPage.error(session, "Flow not found for identity provider"));
}
event.detail(Details.IDENTITY_PROVIDER, identityProviderAlias)
.detail(Details.IDENTITY_PROVIDER_USERNAME, brokerContext.getUsername());
AuthenticationProcessor processor = new AuthenticationProcessor() {
@Override
protected Response authenticationComplete() {
if (firstBrokerLogin) {
authSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, identityProviderAlias);
} else {
String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + identityProviderAlias;
authSession.setAuthNote(authStateNoteKey, "true");
}
return redirectToAfterBrokerLoginEndpoint(authSession, firstBrokerLogin);
}
};
return processFlow(checks.isActionRequest(), execution, authSession, flowPath, brokerLoginFlow, null, processor);
}
private Response redirectToAfterBrokerLoginEndpoint(AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
return redirectToAfterBrokerLoginEndpoint(session, realm, uriInfo, authSession, firstBrokerLogin);
}
public static Response redirectToAfterBrokerLoginEndpoint(KeycloakSession session, RealmModel realm, UriInfo uriInfo, AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
authSession.setTimestamp(Time.currentTime());
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) :
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) ;
logger.debugf("Redirecting to '%s' ", redirect);
return Response.status(302).location(redirect).build();
}
/**
* OAuth grant page. You should not invoked this directly!
*
* @param formData
* @return
*/
@Path("consent")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processConsent(final MultivaluedMap<String, String> formData) {
event.event(EventType.LOGIN);
String code = formData.getFirst("code");
SessionCodeChecks checks = checksForCode(code, null, REQUIRED_ACTION);
if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) {
return checks.getResponse();
}
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
initLoginEvent(authSession);
UserModel user = authSession.getAuthenticatedUser();
ClientModel client = authSession.getClient();
if (formData.containsKey("cancel")) {
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authSession.getProtocol());
protocol.setRealm(realm)
.setHttpHeaders(headers)
.setUriInfo(uriInfo)
.setEventBuilder(event);
Response response = protocol.sendError(authSession, Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
return response;
}
UserConsentModel grantedConsent = session.users().getConsentByClient(realm, user.getId(), client.getId());
if (grantedConsent == null) {
grantedConsent = new UserConsentModel(client);
session.users().addConsent(realm, user.getId(), grantedConsent);
}
for (RoleModel role : ClientSessionCode.getRequestedRoles(authSession, realm)) {
grantedConsent.addGrantedRole(role);
}
for (ProtocolMapperModel protocolMapper : ClientSessionCode.getRequestedProtocolMappers(authSession.getProtocolMappers(), client)) {
if (protocolMapper.isConsentRequired() && protocolMapper.getConsentText() != null) {
grantedConsent.addGrantedProtocolMapper(protocolMapper);
}
}
session.users().updateConsent(realm, user.getId(), grantedConsent);
event.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED);
event.success();
AuthenticatedClientSessionModel clientSession = AuthenticationProcessor.attachSession(authSession, null, session, realm, clientConnection, event);
return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol());
}
private void initLoginEvent(AuthenticationSessionModel authSession) {
String responseType = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
if (responseType == null) {
responseType = "code";
}
String respMode = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
event.event(EventType.LOGIN).client(authSession.getClient())
.detail(Details.CODE_ID, authSession.getId())
.detail(Details.REDIRECT_URI, authSession.getRedirectUri())
.detail(Details.AUTH_METHOD, authSession.getProtocol())
.detail(Details.RESPONSE_TYPE, responseType)
.detail(Details.RESPONSE_MODE, responseMode.toString().toLowerCase());
UserModel authenticatedUser = authSession.getAuthenticatedUser();
if (authenticatedUser != null) {
event.user(authenticatedUser)
.detail(Details.USERNAME, authenticatedUser.getUsername());
}
String attemptedUsername = authSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
if (attemptedUsername != null) {
event.detail(Details.USERNAME, attemptedUsername);
}
String rememberMe = authSession.getAuthNote(Details.REMEMBER_ME);
if (rememberMe==null || !rememberMe.equalsIgnoreCase("true")) {
rememberMe = "false";
}
event.detail(Details.REMEMBER_ME, rememberMe);
Map<String, String> userSessionNotes = authSession.getUserSessionNotes();
String identityProvider = userSessionNotes.get(Details.IDENTITY_PROVIDER);
if (identityProvider != null) {
event.detail(Details.IDENTITY_PROVIDER, identityProvider)
.detail(Details.IDENTITY_PROVIDER_USERNAME, userSessionNotes.get(Details.IDENTITY_PROVIDER_USERNAME));
}
}
@Path(REQUIRED_ACTION)
@POST
public Response requiredActionPOST(@QueryParam("code") final String code,
@QueryParam("execution") String action) {
return processRequireAction(code, action);
}
@Path(REQUIRED_ACTION)
@GET
public Response requiredActionGET(@QueryParam("code") final String code,
@QueryParam("execution") String action) {
return processRequireAction(code, action);
}
private Response processRequireAction(final String code, String action) {
event.event(EventType.CUSTOM_REQUIRED_ACTION);
SessionCodeChecks checks = checksForCode(code, action, REQUIRED_ACTION);
if (!checks.verifyRequiredAction(action)) {
return checks.getResponse();
}
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
if (!checks.isActionRequest()) {
initLoginEvent(authSession);
event.event(EventType.CUSTOM_REQUIRED_ACTION);
return AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, uriInfo, event);
}
initLoginEvent(authSession);
event.event(EventType.CUSTOM_REQUIRED_ACTION);
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, action);
if (factory == null) {
ServicesLogger.LOGGER.actionProviderNull();
event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE));
}
RequiredActionProvider provider = factory.create(session);
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
@Override
public void ignore() {
throw new RuntimeException("Cannot call ignore within processAction()");
}
};
Response response;
provider.processAction(context);
if (action != null) {
authSession.setAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION, action);
}
if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
event.clone().success();
initLoginEvent(authSession);
event.event(EventType.LOGIN);
authSession.removeRequiredAction(factory.getId());
authSession.getAuthenticatedUser().removeRequiredAction(factory.getId());
authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
response = AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, uriInfo, event);
} else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
response = context.getChallenge();
} else if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo())
.setEventBuilder(event);
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
response = protocol.sendError(authSession, Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
} else {
throw new RuntimeException("Unreachable");
}
return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true);
}
}