diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java index 4c25bb317e..b4b3f16717 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java @@ -45,6 +45,9 @@ public class UrlBean { } public String getLoginAction() { + if (this.actionuri != null) { + return this.actionuri.toString(); + } return Urls.realmLoginAction(baseURI, realm).toString(); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 5920e101f8..1ebfa50abe 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -279,6 +279,10 @@ public class AuthenticationProcessor { super(message, cause, enableSuppression, writableStackTrace); this.error = error; } + + public Error getError() { + return error; + } } public void logUserFailure() { @@ -389,6 +393,9 @@ public class AuthenticationProcessor { if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) throw new AuthException(Error.INVALID_CREDENTIALS); clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.ATTEMPTED); continue; + } else { + logger.error("Unknown result status"); + throw new AuthException(Error.INTERNAL_ERROR); } } return null; @@ -398,7 +405,7 @@ public class AuthenticationProcessor { public void validateUser(UserModel authenticatedUser) { if (authenticatedUser != null) { - if (!clientSession.getAuthenticatedUser().isEnabled()) throw new AuthException(Error.USER_DISABLED); + if (!authenticatedUser.isEnabled()) throw new AuthException(Error.USER_DISABLED); } if (realm.isBruteForceProtected()) { if (protector.isTemporarilyDisabled(session, realm, authenticatedUser.getUsername())) { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticator.java index f2f2ef25dc..c678c4265f 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticator.java @@ -52,6 +52,7 @@ public class LoginFormOTPAuthenticator extends LoginFormUsernameAuthenticator { context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; } + context.success(); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java index bcc4c5e6fd..17419a1368 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java @@ -53,6 +53,7 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; } + context.success(); } @Override @@ -62,7 +63,7 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat @Override public boolean configuredFor(UserModel user) { - return false; + return user.configuredForCredentialType(UserCredentialModel.PASSWORD); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticator.java index 78b9096bee..56707f9a3a 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticator.java @@ -1,6 +1,7 @@ package org.keycloak.authentication.authenticators; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; +import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorContext; @@ -51,7 +52,9 @@ public class LoginFormUsernameAuthenticator implements Authenticator { protected boolean isActionUrl(AuthenticatorContext context) { URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName()); - return expected.getPath().equals(context.getUriInfo().getPath()); + String current = context.getUriInfo().getAbsolutePath().getPath(); + String expectedPath = expected.getPath(); + return expectedPath.equals(current); } @@ -71,7 +74,11 @@ public class LoginFormUsernameAuthenticator implements Authenticator { protected LoginFormsProvider loginForm(AuthenticatorContext context) { ClientSessionCode code = new ClientSessionCode(context.getRealm(), context.getClientSession()); code.setAction(ClientSessionModel.Action.AUTHENTICATE); + URI action = LoginActionsService.authenticationFormProcessor(context.getUriInfo()) + .queryParam(OAuth2Constants.CODE, code.getCode()) + .build(context.getRealm().getName()); return context.getSession().getProvider(LoginFormsProvider.class) + .setActionUri(action) .setClientSessionCode(code.getCode()); } @@ -98,6 +105,7 @@ public class LoginFormUsernameAuthenticator implements Authenticator { UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username); if (invalidUser(context, user)) return; context.setUser(user); + context.success(); } public boolean invalidUser(AuthenticatorContext context, UserModel user) { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java index 7659c92a66..d93836f8c3 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java @@ -58,6 +58,7 @@ public class OTPFormAuthenticator implements Authenticator { context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; } + context.success(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 93bf67bb6c..fdb029eeae 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -5,12 +5,15 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.authentication.authenticators.AuthenticationFlow; import org.keycloak.constants.AdapterConstants; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.IdentityProviderModel; @@ -244,6 +247,32 @@ public class AuthorizationEndpoint { return buildRedirectToIdentityProvider(idpHint, accessCode); } + return oldBrowserAuthentication(accessCode); + } + + protected Response newBrowserAuthentication(String accessCode) { + String flowId = null; + for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) { + if (flow.getAlias().equals("browser")) { + flowId = flow.getId(); + break; + } + } + AuthenticationProcessor processor = new AuthenticationProcessor(); + processor.setClientSession(clientSession) + .setFlowId(flowId) + .setConnection(clientConnection) + .setEventBuilder(event) + .setProtector(authManager.getProtector()) + .setRealm(realm) + .setSession(session) + .setUriInfo(uriInfo) + .setRequest(request); + + return processor.authenticate(); + } + + protected Response oldBrowserAuthentication(String accessCode) { Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event); if (response != null) return response; diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 60a49d5c8a..3ada8798e3 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -78,6 +78,10 @@ public class AuthenticationManager { this.protector = protector; } + public BruteForceProtector getProtector() { + return protector; + } + public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) { if (userSession == null) { logger.debug("No user session"); diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index d1545f2296..4329591394 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -24,6 +24,7 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.events.Details; @@ -32,6 +33,7 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RoleModel; @@ -118,7 +120,7 @@ public class LoginActionsService { } public static UriBuilder authenticationFormProcessor(UriInfo uriInfo) { - return loginActionsBaseUrl(uriInfo).path("auth-form"); + return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "authForm"); } public static UriBuilder loginActionsBaseUrl(UriBuilder baseUriBuilder) { @@ -270,6 +272,116 @@ public class LoginActionsService { .createRegistration(); } + /** + * URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY! + * + * @param code + * @return + */ + @Path("auth-form") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response authForm(@QueryParam("code") String code) { + event.event(EventType.LOGIN); + if (!checkSsl()) { + event.error(Errors.SSL_REQUIRED); + return ErrorPage.error(session, Messages.HTTPS_REQUIRED); + } + + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + return ErrorPage.error(session, Messages.REALM_NOT_ENABLED); + } + ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm); + if (clientCode == null) { + event.error(Errors.INVALID_CODE); + return ErrorPage.error(session, Messages.INVALID_CODE); + } + + ClientSessionModel clientSession = clientCode.getClientSession(); + event.detail(Details.CODE_ID, clientSession.getId()); + + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE) || clientSession.getUserSession() != null) { + clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE); + event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE); + return session.getProvider(LoginFormsProvider.class) + .setError(Messages.EXPIRED_CODE) + .setClientSessionCode(clientCode.getCode()) + .createLogin(); + } + + ClientModel client = clientSession.getClient(); + if (client == null) { + event.error(Errors.CLIENT_NOT_FOUND); + return ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER); + } + if (!client.isEnabled()) { + event.error(Errors.CLIENT_NOT_FOUND); + return ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED); + } + + String flowId = null; + for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) { + if (flow.getAlias().equals("browser")) { + flowId = flow.getId(); + break; + } + } + AuthenticationProcessor processor = new AuthenticationProcessor(); + processor.setClientSession(clientSession) + .setFlowId(flowId) + .setConnection(clientConnection) + .setEventBuilder(event) + .setProtector(authManager.getProtector()) + .setRealm(realm) + .setSession(session) + .setUriInfo(uriInfo) + .setRequest(request); + + try { + return processor.authenticate(); + } catch (AuthenticationProcessor.AuthException e) { + return handleError(e, code); + } catch (Exception e) { + logger.error("failed authentication", e); + return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE); + + } + + } + + protected Response handleError(AuthenticationProcessor.AuthException e, String code) { + logger.error("failed authentication: " + e.getError().toString(), e); + if (e.getError() == AuthenticationProcessor.Error.INVALID_USER) { + event.error(Errors.USER_NOT_FOUND); + return session.getProvider(LoginFormsProvider.class) + .setError(Messages.INVALID_USER) + .setClientSessionCode(code) + .createLogin(); + + } else if (e.getError() == AuthenticationProcessor.Error.USER_DISABLED) { + event.error(Errors.USER_DISABLED); + return session.getProvider(LoginFormsProvider.class) + .setError(Messages.ACCOUNT_DISABLED) + .setClientSessionCode(code) + .createLogin(); + + } else if (e.getError() == AuthenticationProcessor.Error.USER_TEMPORARILY_DISABLED) { + event.error(Errors.USER_TEMPORARILY_DISABLED); + return session.getProvider(LoginFormsProvider.class) + .setError(Messages.ACCOUNT_TEMPORARILY_DISABLED) + .setClientSessionCode(code) + .createLogin(); + + } else { + event.error(Errors.INVALID_USER_CREDENTIALS); + return session.getProvider(LoginFormsProvider.class) + .setError(Messages.INVALID_USER) + .setClientSessionCode(code) + .createLogin(); + } + } + /** * URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY! * diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index a9b5a5438c..d2b5ca78ff 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -2,4 +2,5 @@ org.keycloak.protocol.LoginProtocolSpi org.keycloak.protocol.ProtocolMapperSpi org.keycloak.exportimport.ClientImportSpi org.keycloak.wellknown.WellKnownSpi -org.keycloak.messages.MessagesSpi \ No newline at end of file +org.keycloak.messages.MessagesSpi +org.keycloak.authentication.AuthenticatorSpi \ No newline at end of file