Move required actions into separate service
This commit is contained in:
parent
e1161c8eb0
commit
77159861e8
2 changed files with 265 additions and 0 deletions
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
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<String, String> 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<String, String> 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<String, String> 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<RequiredAction> 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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()) {
|
||||
|
|
Loading…
Reference in a new issue