diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
new file mode 100755
index 0000000000..ee344b8e92
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -0,0 +1,260 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.services.resources;
+
+import org.jboss.resteasy.jose.jws.JWSInput;
+import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.AccessCodeEntry;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.TokenManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.flows.Flows;
+import org.keycloak.services.resources.flows.FormFlows;
+import org.keycloak.services.validation.Validation;
+import org.picketlink.idm.credential.util.TimeBasedOTP;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.*;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.Providers;
+import java.util.Set;
+
+/**
+ * @author Stian Thorgersen
+ */
+public class RequiredActionsService {
+
+ private RealmModel realm;
+
+ @Context
+ private HttpRequest request;
+
+ @Context
+ protected HttpHeaders headers;
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Context
+ protected Providers providers;
+
+ protected AuthenticationManager authManager = new AuthenticationManager();
+
+ private TokenManager tokenManager;
+
+ public RequiredActionsService(RealmModel realm, TokenManager tokenManager) {
+ this.realm = realm;
+ this.tokenManager = tokenManager;
+ }
+
+ @Path("")
+ @POST
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ public Response updateProfile(final MultivaluedMap formData) {
+ AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE);
+ if (accessCode == null) {
+ return forwardToErrorPage();
+ }
+
+ UserModel user = getUser(accessCode);
+ user.setFirstName(formData.getFirst("firstName"));
+ user.setLastName(formData.getFirst("lastName"));
+ user.setEmail(formData.getFirst("email"));
+
+ user.removeRequiredAction(RequiredAction.UPDATE_PROFILE);
+ accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PROFILE);
+
+ return redirectOauth(user, accessCode);
+ }
+
+ @Path("totp")
+ @POST
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ public Response configureTotp(final MultivaluedMap formData) {
+ AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP);
+ if (accessCode == null) {
+ return forwardToErrorPage();
+ }
+
+ UserModel user = getUser(accessCode);
+
+ String totp = formData.getFirst("totp");
+ String totpSecret = formData.getFirst("totpSecret");
+
+ FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
+ if (Validation.isEmpty(totp)) {
+ return forms.setError(Messages.MISSING_TOTP).forwardToAction(RequiredAction.CONFIGURE_TOTP);
+ } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
+ return forms.setError(Messages.INVALID_TOTP).forwardToAction(RequiredAction.CONFIGURE_TOTP);
+ }
+
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(CredentialRepresentation.TOTP);
+ credentials.setValue(formData.getFirst("totpSecret"));
+ realm.updateCredential(user, credentials);
+
+ user.setTotp(true);
+
+ user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP);
+ accessCode.getRequiredActions().remove(RequiredAction.CONFIGURE_TOTP);
+
+ return redirectOauth(user, accessCode);
+ }
+
+
+ @Path("email-verification")
+ @GET
+ public Response emailVerification() {
+ if (uriInfo.getQueryParameters().containsKey("key")) {
+ AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key"));
+ if (accessCode == null || accessCode.isExpired()
+ || !accessCode.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL)) {
+ return forwardToErrorPage();
+ }
+
+ UserModel user = getUser(accessCode);
+ user.setEmailVerified(true);
+
+ user.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
+ accessCode.getRequiredActions().remove(RequiredAction.VERIFY_EMAIL);
+
+ return redirectOauth(user, accessCode);
+ } else {
+ AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL);
+ if (accessCode == null) {
+ return forwardToErrorPage();
+ }
+
+ return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(accessCode.getUser())
+ .forwardToAction(RequiredAction.VERIFY_EMAIL);
+ }
+ }
+
+ @Path("password")
+ @POST
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ public Response resetPassword(final MultivaluedMap formData) {
+ AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD);
+ if (accessCode == null) {
+ return forwardToErrorPage();
+ }
+
+ UserModel user = getUser(accessCode);
+
+ String password = formData.getFirst("password");
+ String passwordNew = formData.getFirst("password-new");
+ String passwordConfirm = formData.getFirst("password-confirm");
+
+ FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
+ if (Validation.isEmpty(passwordNew)) {
+ forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+ } else if (!passwordNew.equals(passwordConfirm)) {
+ forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+ }
+
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(CredentialRepresentation.PASSWORD);
+ credentials.setValue(passwordNew);
+
+ realm.updateCredential(user, credentials);
+
+ user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD);
+ if (accessCode != null) {
+ accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD);
+ }
+
+ if (accessCode != null) {
+ return redirectOauth(user, accessCode);
+ } else {
+ return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
+ }
+ }
+
+ private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) {
+ String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE);
+ if (code == null) {
+ return null;
+ }
+
+ JWSInput input = new JWSInput(code, providers);
+ boolean verifiedCode = false;
+ try {
+ verifiedCode = RSAProvider.verify(input, realm.getPublicKey());
+ } catch (Exception ignored) {
+ return null;
+ }
+
+ if (!verifiedCode) {
+ return null;
+ }
+
+ String key = input.readContent(String.class);
+ AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
+ if (accessCodeEntry == null) {
+ return null;
+ }
+
+ if (accessCodeEntry.isExpired()) {
+ return null;
+ }
+
+ if (accessCodeEntry.getRequiredActions() == null || !accessCodeEntry.getRequiredActions().contains(requiredAction)) {
+ return null;
+ }
+
+ return accessCodeEntry;
+ }
+
+ private UserModel getUser(AccessCodeEntry accessCode) {
+ return realm.getUser(accessCode.getUser().getLoginName());
+ }
+
+ private Response redirectOauth(UserModel user, AccessCodeEntry accessCode) {
+ if (accessCode == null) {
+ return null;
+ }
+
+ Set requiredActions = user.getRequiredActions();
+ if (!requiredActions.isEmpty()) {
+ return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
+ .forwardToAction(requiredActions.iterator().next());
+ } else {
+ accessCode.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan());
+ return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
+ accessCode.getState(), accessCode.getRedirectUri());
+ }
+ }
+
+ private Response forwardToErrorPage() {
+ return Flows.forms(realm, request, uriInfo).forwardToErrorPage();
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index 89742bf752..c8bfd0ec8b 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -217,6 +217,11 @@ public class TokenService {
}
}
+ @Path("auth/request/login-actions")
+ public RequiredActionsService getRequiredActionsService() {
+ return new RequiredActionsService(realm, tokenManager);
+ }
+
private void isTotpConfigurationRequired(UserModel user) {
for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
if (c.getType().equals(CredentialRepresentation.TOTP) && !user.isTotp()) {