non-browser flow

This commit is contained in:
Bill Burke 2015-07-21 20:56:05 -04:00
parent 548c0db0ae
commit fe9dc4a28d
24 changed files with 625 additions and 137 deletions

View file

@ -18,14 +18,17 @@ public class DefaultAuthenticationFlows {
public static final String REGISTRATION_FLOW = "registration"; public static final String REGISTRATION_FLOW = "registration";
public static final String REGISTRATION_FORM_FLOW = "registration form"; public static final String REGISTRATION_FORM_FLOW = "registration form";
public static final String BROWSER_FLOW = "browser"; public static final String BROWSER_FLOW = "browser";
public static final String DIRECT_GRANT_FLOW = "direct grant";
public static final String LOGIN_FORMS_FLOW = "forms"; public static final String LOGIN_FORMS_FLOW = "forms";
public static void addFlows(RealmModel realm) { public static void addFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm); if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm);
if (realm.getFlowByAlias(DIRECT_GRANT_FLOW) == null) directGrantFlow(realm, false);
if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm); if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
} }
public static void migrateFlows(RealmModel realm) { public static void migrateFlows(RealmModel realm) {
browserFlow(realm, true); browserFlow(realm, true);
directGrantFlow(realm, true);
if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm); if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
} }
@ -123,6 +126,55 @@ public class DefaultAuthenticationFlows {
return false; return false;
} }
public static void directGrantFlow(RealmModel realm, boolean migrate) {
AuthenticationFlowModel grant = new AuthenticationFlowModel();
grant.setAlias(DIRECT_GRANT_FLOW);
grant.setDescription("OpenID Connect Resource Owner Grant");
grant.setProviderId("basic-flow");
grant.setTopLevel(true);
grant.setBuiltIn(true);
grant = realm.addAuthenticationFlow(grant);
// username
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(grant.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("direct-grant-validate-username");
execution.setPriority(10);
execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false);
realm.addAuthenticatorExecution(execution);
// password
execution = new AuthenticationExecutionModel();
execution.setParentFlow(grant.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
if (migrate && !hasCredentialType(realm, RequiredCredentialModel.PASSWORD.getType())) {
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
}
execution.setAuthenticator("direct-grant-validate-password");
execution.setPriority(20);
execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false);
realm.addAuthenticatorExecution(execution);
// otp
execution = new AuthenticationExecutionModel();
execution.setParentFlow(grant.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) {
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
}
execution.setAuthenticator("direct-grant-validate-otp");
execution.setPriority(30);
execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false);
realm.addAuthenticatorExecution(execution);
}
public static void browserFlow(RealmModel realm, boolean migrate) { public static void browserFlow(RealmModel realm, boolean migrate) {
AuthenticationFlowModel browser = new AuthenticationFlowModel(); AuthenticationFlowModel browser = new AuthenticationFlowModel();
browser.setAlias(BROWSER_FLOW); browser.setAlias(BROWSER_FLOW);

View file

@ -3,7 +3,7 @@ package org.keycloak.authentication;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.authentication.authenticators.AbstractFormAuthenticator; import org.keycloak.authentication.authenticators.browser.AbstractFormAuthenticator;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
@ -561,9 +561,18 @@ public class AuthenticationProcessor {
validateUser(authUser); validateUser(authUser);
AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null); AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null);
Response challenge = authenticationFlow.processFlow(); Response challenge = authenticationFlow.processFlow();
if (challenge != null) return challenge; return challenge;
}
public Response attachSessionExecutionRequiredActions() {
attachSession();
return AuthenticationManager.actionRequired(session, userSession, clientSession, connection, request, uriInfo, event);
}
public void attachSession() {
String username = clientSession.getAuthenticatedUser().getUsername(); String username = clientSession.getAuthenticatedUser().getUsername();
String attemptedUsername = clientSession.getNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME);
if (attemptedUsername != null) username = attemptedUsername;
if (userSession == null) { // if no authenticator attached a usersession if (userSession == null) { // if no authenticator attached a usersession
userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", false, null, null); userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", false, null, null);
userSession.setState(UserSessionModel.State.LOGGING_IN); userSession.setState(UserSessionModel.State.LOGGING_IN);
@ -573,8 +582,10 @@ public class AuthenticationProcessor {
event.user(userSession.getUser()) event.user(userSession.getUser())
.detail(Details.USERNAME, username) .detail(Details.USERNAME, username)
.session(userSession); .session(userSession);
}
return AuthenticationManager.actionRequired(session, userSession, clientSession, connection, request, uriInfo, event); public void evaluateRequiredActionTriggers() {
AuthenticationManager.evaluateRequiredActionTriggers(session, userSession, clientSession, connection, request, uriInfo, event, realm, clientSession.getAuthenticatedUser());
} }
public Response finishAuthentication() { public Response finishAuthentication() {

View file

@ -105,6 +105,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
} }
AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator()); AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator());
if (factory == null) {
throw new AuthenticationProcessor.AuthException("Could not find AuthenticatorFactory for: " + model.getAuthenticator(), AuthenticationProcessor.Error.INTERNAL_ERROR);
}
Authenticator authenticator = factory.create(); Authenticator authenticator = factory.create();
AuthenticationProcessor.logger.debugv("authenticator: {0}", factory.getId()); AuthenticationProcessor.logger.debugv("authenticator: {0}", factory.getId());
UserModel authUser = processor.getClientSession().getAuthenticatedUser(); UserModel authUser = processor.getClientSession().getAuthenticatedUser();

View file

@ -1,4 +1,4 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;

View file

@ -1,4 +1,4 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext; import org.keycloak.authentication.AuthenticatorContext;

View file

@ -1,10 +1,9 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;

View file

@ -1,11 +1,10 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext; import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;

View file

@ -1,10 +1,9 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;

View file

@ -1,4 +1,4 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
@ -8,24 +8,19 @@ import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.constants.KerberosConstants; import org.keycloak.constants.KerberosConstants;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.net.URI; import java.net.URI;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static org.keycloak.util.HtmlUtils.escapeAttribute;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $

View file

@ -1,10 +1,9 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;

View file

@ -1,4 +1,4 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
@ -6,7 +6,6 @@ import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext; import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;

View file

@ -1,10 +1,9 @@
package org.keycloak.authentication.authenticators; package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;

View file

@ -0,0 +1,59 @@
package org.keycloak.authentication.authenticators.directgrant;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractDirectGrantAuthenticator implements Authenticator, AuthenticatorFactory {
public Response errorResponse(int status, String error, String errorDescription) {
Map<String, String> e = new HashMap<String, String>();
e.put(OAuth2Constants.ERROR, error);
if (errorDescription != null) {
e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
}
return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build();
}
@Override
public void action(AuthenticatorContext context) {
}
@Override
public Authenticator create() {
return this;
}
@Override
public void close() {
}
@Override
public Authenticator create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
}

View file

@ -0,0 +1,125 @@
package org.keycloak.authentication.authenticators.directgrant;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.CredentialRepresentation;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ValidateOTP extends AbstractDirectGrantAuthenticator {
private static final Logger logger = Logger.getLogger(ValidateOTP.class);
public static final String PROVIDER_ID = "direct-grant-validate-otp";
@Override
public void authenticate(AuthenticatorContext context) {
if (!isConfigured(context.getSession(), context.getRealm(), context.getUser())) {
if (context.getExecution().isOptional()) {
context.attempted();
} else if (context.getExecution().isRequired()) {
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
}
return;
}
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
List<UserCredentialModel> credentials = new LinkedList<>();
String otp = inputData.getFirst(CredentialRepresentation.TOTP);
if (otp == null) {
if (context.getUser() != null) {
context.getEvent().user(context.getUser());
}
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
credentials.add(UserCredentialModel.totp(otp));
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
if (!valid) {
context.getEvent().user(context.getUser());
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
context.success();
}
@Override
public boolean requiresUser() {
return true;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
private boolean isConfigured(KeycloakSession session, RealmModel realm, UserModel user) {
return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp();
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public String getDisplayType() {
return "OTP";
}
@Override
public String getReferenceCategory() {
return null;
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.OPTIONAL,
AuthenticationExecutionModel.Requirement.DISABLED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getHelpText() {
return "Validates the one time password supplied as a 'totp' form parameter in direct grant request";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return new LinkedList<>();
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -0,0 +1,115 @@
package org.keycloak.authentication.authenticators.directgrant;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.authentication.authenticators.browser.AbstractFormAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ValidatePassword extends AbstractDirectGrantAuthenticator {
private static final Logger logger = Logger.getLogger(ValidatePassword.class);
public static final String PROVIDER_ID = "direct-grant-validate-password";
@Override
public void authenticate(AuthenticatorContext context) {
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
List<UserCredentialModel> credentials = new LinkedList<>();
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
if (password == null) {
if (context.getUser() != null) {
context.getEvent().user(context.getUser());
}
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
credentials.add(UserCredentialModel.password(password));
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
if (!valid) {
context.getEvent().user(context.getUser());
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
context.success();
}
@Override
public boolean requiresUser() {
return true;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public String getDisplayType() {
return "Password";
}
@Override
public String getReferenceCategory() {
return null;
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getHelpText() {
return "Validates the password supplied as a 'password' form parameter in direct grant request";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return new LinkedList<>();
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -0,0 +1,144 @@
package org.keycloak.authentication.authenticators.directgrant;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.AbstractFormAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ValidateUsername extends AbstractDirectGrantAuthenticator {
private static final Logger logger = Logger.getLogger(ValidateUsername.class);
public static final String PROVIDER_ID = "direct-grant-validate-username";
@Override
public void authenticate(AuthenticatorContext context) {
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) {
context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Missing parameter: username");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
context.getEvent().detail(Details.USERNAME, username);
context.getClientSession().setNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME, username);
UserModel user = null;
try {
user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
} catch (ModelDuplicateException mde) {
logger.error(mde.getMessage(), mde);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Invalid user credentials");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
if (user == null) {
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
if (!user.isEnabled()) {
context.getEvent().user(user);
context.getEvent().error(Errors.USER_DISABLED);
Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Account disabled");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
if (context.getRealm().isBruteForceProtected()) {
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user.getUsername())) {
context.getEvent().user(user);
context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Account temporarily disabled");
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
}
context.setUser(user);
context.success();
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public String getDisplayType() {
return "Username Validation";
}
@Override
public String getReferenceCategory() {
return null;
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getHelpText() {
return "Validates the username supplied as a 'username' form parameter in direct grant request";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return new LinkedList<>();
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -277,6 +277,9 @@ public class AuthorizationEndpoint {
Response challenge = null; Response challenge = null;
try { try {
challenge = processor.authenticateOnly(); challenge = processor.authenticateOnly();
if (challenge == null) {
challenge = processor.attachSessionExecutionRequiredActions();
}
} catch (Exception e) { } catch (Exception e) {
return processor.handleBrowserException(e); return processor.handleBrowserException(e);
} }

View file

@ -5,11 +5,13 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -17,6 +19,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
@ -307,46 +310,38 @@ public class TokenEndpoint {
public Response buildResourceOwnerPasswordCredentialsGrant() { public Response buildResourceOwnerPasswordCredentialsGrant() {
event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token"); event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
String username = formParams.getFirst(AuthenticationManager.FORM_USERNAME); if (client.isConsentRequired()) {
if (username == null) { throw new ErrorResponseException("invalid_client", "Client requires user consent", Response.Status.BAD_REQUEST);
event.error(Errors.USERNAME_MISSING);
throw new ErrorResponseException("invalid_request", "Missing parameter: username", Response.Status.UNAUTHORIZED);
} }
event.detail(Details.USERNAME, username);
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username);
if (user != null) event.user(user);
AuthenticationManager.AuthenticationStatus authenticationStatus = authManager.authenticateForm(session, clientConnection, realm, formParams);
Map<String, String> err;
switch (authenticationStatus) {
case SUCCESS:
break;
case ACCOUNT_TEMPORARILY_DISABLED:
case ACTIONS_REQUIRED:
event.error(Errors.USER_TEMPORARILY_DISABLED);
throw new ErrorResponseException("invalid_grant", "Account temporarily disabled", Response.Status.BAD_REQUEST);
case ACCOUNT_DISABLED:
event.error(Errors.USER_DISABLED);
throw new ErrorResponseException("invalid_grant", "Account disabled", Response.Status.BAD_REQUEST);
default:
event.error(Errors.INVALID_USER_CREDENTIALS);
throw new ErrorResponseException("invalid_grant", "Invalid user credentials", Response.Status.UNAUTHORIZED);
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE); String scope = formParams.getFirst(OAuth2Constants.SCOPE);
UserSessionProvider sessions = session.sessions(); UserSessionProvider sessions = session.sessions();
UserSessionModel userSession = sessions.createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "oauth_credentials", false, null, null);
event.session(userSession);
ClientSessionModel clientSession = sessions.createClientSession(realm, client); ClientSessionModel clientSession = sessions.createClientSession(realm, client);
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW);
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowId(flowId)
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
Response challenge = processor.authenticateOnly();
if (challenge != null) return challenge;
processor.evaluateRequiredActionTriggers();
UserModel user = clientSession.getAuthenticatedUser();
if (user.getRequiredActions() != null && user.getRequiredActions().size() > 0) {
throw new ErrorResponseException("invalid_grant", "Account is not fully set up", Response.Status.BAD_REQUEST);
TokenManager.attachClientSession(userSession, clientSession); }
processor.attachSession();
UserSessionModel userSession = processor.getUserSession();
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
.generateAccessToken(session, scope, client, user, userSession, clientSession) .generateAccessToken(session, scope, client, user, userSession, clientSession)
@ -354,6 +349,7 @@ public class TokenEndpoint {
.generateIDToken() .generateIDToken()
.build(); .build();
event.success(); event.success();
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();

View file

@ -429,66 +429,7 @@ public class AuthenticationManager {
final UserModel user = userSession.getUser(); final UserModel user = userSession.getUser();
final ClientModel client = clientSession.getClient(); final ClientModel client = clientSession.getClient();
RequiredActionContext context = new RequiredActionContext() { RequiredActionContext context = evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user);
@Override
public EventBuilder getEvent() {
return event;
}
@Override
public UserModel getUser() {
return user;
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public ClientSessionModel getClientSession() {
return clientSession;
}
@Override
public UserSessionModel getUserSession() {
return userSession;
}
@Override
public ClientConnection getConnection() {
return clientConnection;
}
@Override
public UriInfo getUriInfo() {
return uriInfo;
}
@Override
public KeycloakSession getSession() {
return session;
}
@Override
public HttpRequest getHttpRequest() {
return request;
}
@Override
public String generateAccessCode(String action) {
ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession());
code.setAction(action);
return code.getCode();
}
};
// see if any required actions need triggering, i.e. an expired password
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
if (!model.isEnabled()) continue;
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
provider.evaluateTriggers(context);
}
logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired()); logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
@ -554,6 +495,70 @@ public class AuthenticationManager {
} }
public static RequiredActionContext evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
RequiredActionContext context = new RequiredActionContext() {
@Override
public EventBuilder getEvent() {
return event;
}
@Override
public UserModel getUser() {
return user;
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public ClientSessionModel getClientSession() {
return clientSession;
}
@Override
public UserSessionModel getUserSession() {
return userSession;
}
@Override
public ClientConnection getConnection() {
return clientConnection;
}
@Override
public UriInfo getUriInfo() {
return uriInfo;
}
@Override
public KeycloakSession getSession() {
return session;
}
@Override
public HttpRequest getHttpRequest() {
return request;
}
@Override
public String generateAccessCode(String action) {
ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession());
code.setAction(action);
return code.getCode();
}
};
// see if any required actions need triggering, i.e. an expired password
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
if (!model.isEnabled()) continue;
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
provider.evaluateTriggers(context);
}
return context;
}
protected static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, String tokenString, HttpHeaders headers) { protected static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, String tokenString, HttpHeaders headers) {
try { try {

View file

@ -23,14 +23,10 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.InternalServerErrorException;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.authenticators.AbstractFormAuthenticator;
import org.keycloak.authentication.authenticators.UsernamePasswordFormFactory;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
import org.keycloak.events.Details; import org.keycloak.events.Details;
@ -55,8 +51,6 @@ import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
@ -84,7 +78,6 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.Providers;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

View file

@ -5,16 +5,10 @@ import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.ConfigurableAuthenticatorFactory; import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory;
import org.keycloak.authentication.authenticators.SpnegoAuthenticatorFactory;
import org.keycloak.authentication.authenticators.UsernamePasswordFormFactory;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.representations.idm.CredentialRepresentation;
/** /**
* used to set an execution a state based on type. * used to set an execution a state based on type.

View file

@ -1,4 +1,7 @@
org.keycloak.authentication.authenticators.CookieAuthenticatorFactory org.keycloak.authentication.authenticators.browser.CookieAuthenticatorFactory
org.keycloak.authentication.authenticators.UsernamePasswordFormFactory org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory
org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory
org.keycloak.authentication.authenticators.SpnegoAuthenticatorFactory org.keycloak.authentication.authenticators.browser.SpnegoAuthenticatorFactory
org.keycloak.authentication.authenticators.directgrant.ValidateOTP
org.keycloak.authentication.authenticators.directgrant.ValidatePassword
org.keycloak.authentication.authenticators.directgrant.ValidateUsername

View file

@ -18,7 +18,7 @@ import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.authentication.authenticators.SpnegoAuthenticator; import org.keycloak.authentication.authenticators.browser.SpnegoAuthenticator;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.constants.KerberosConstants; import org.keycloak.constants.KerberosConstants;

View file

@ -86,7 +86,6 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.client("resource-owner") .client("resource-owner")
.user(userId) .user(userId)
.session(accessToken.getSessionState()) .session(accessToken.getSessionState())
.detail(Details.AUTH_METHOD, "oauth_credentials")
.detail(Details.RESPONSE_TYPE, "token") .detail(Details.RESPONSE_TYPE, "token")
.detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
@ -123,7 +122,6 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin() events.expectLogin()
.client("resource-owner") .client("resource-owner")
.session(accessToken.getSessionState()) .session(accessToken.getSessionState())
.detail(Details.AUTH_METHOD, "oauth_credentials")
.detail(Details.RESPONSE_TYPE, "token") .detail(Details.RESPONSE_TYPE, "token")
.detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
@ -178,7 +176,6 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin() events.expectLogin()
.client("resource-owner") .client("resource-owner")
.session((String) null) .session((String) null)
.detail(Details.AUTH_METHOD, "oauth_credentials")
.detail(Details.RESPONSE_TYPE, "token") .detail(Details.RESPONSE_TYPE, "token")
.removeDetail(Details.CODE_ID) .removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI) .removeDetail(Details.REDIRECT_URI)
@ -201,7 +198,6 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.client("resource-owner") .client("resource-owner")
.user((String) null) .user((String) null)
.session((String) null) .session((String) null)
.detail(Details.AUTH_METHOD, "oauth_credentials")
.detail(Details.RESPONSE_TYPE, "token") .detail(Details.RESPONSE_TYPE, "token")
.detail(Details.USERNAME, "invalid") .detail(Details.USERNAME, "invalid")
.removeDetail(Details.CODE_ID) .removeDetail(Details.CODE_ID)