From 77159861e85dbd4b26e23ec1f373e06485255199 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 16 Oct 2013 17:42:08 +0100 Subject: [PATCH] Move required actions into separate service --- .../resources/RequiredActionsService.java | 260 ++++++++++++++++++ .../services/resources/TokenService.java | 5 + 2 files changed, 265 insertions(+) create mode 100755 services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java 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()) {