KEYCLOAK-2011

This commit is contained in:
mposolda 2015-10-23 22:05:27 +02:00
parent c498b06f68
commit 74924f2f8c
7 changed files with 86 additions and 5 deletions

View file

@ -52,6 +52,8 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setClientSessionCode(String accessCode); public LoginFormsProvider setClientSessionCode(String accessCode);
public LoginFormsProvider setClientSession(ClientSessionModel clientSession);
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappers); public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappers);
public LoginFormsProvider setAccessRequest(String message); public LoginFormsProvider setAccessRequest(String message);

View file

@ -47,6 +47,7 @@ import org.keycloak.login.freemarker.model.TotpBean;
import org.keycloak.login.freemarker.model.UrlBean; import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -138,7 +139,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case VERIFY_EMAIL: case VERIFY_EMAIL:
try { try {
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
builder.queryParam("key", accessCode); builder.queryParam(OAuth2Constants.CODE, accessCode);
builder.queryParam("key", clientSession.getNote(Constants.VERIFY_EMAIL_KEY));
String link = builder.build(realm.getName()).toString(); String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
@ -531,6 +533,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return this; return this;
} }
@Override
public LoginFormsProvider setClientSession(ClientSessionModel clientSession) {
this.clientSession = clientSession;
return this;
}
@Override @Override
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappersRequested) { public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappersRequested) {
this.realmRolesRequested = realmRolesRequested; this.realmRolesRequested = realmRolesRequested;

View file

@ -22,4 +22,6 @@ public interface Constants {
// 30 days // 30 days
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000; int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
} }

View file

@ -8,9 +8,12 @@ import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
@ -44,8 +47,11 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success(); context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success();
LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId()); LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId());
setupKey(context.getClientSession());
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class) LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
.setClientSessionCode(context.generateCode()) .setClientSessionCode(context.generateCode())
.setClientSession(context.getClientSession())
.setUser(context.getUser()); .setUser(context.getUser());
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL); Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
context.challenge(challenge); context.challenge(challenge);
@ -87,4 +93,9 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
public String getId() { public String getId() {
return UserModel.RequiredAction.VERIFY_EMAIL.name(); return UserModel.RequiredAction.VERIFY_EMAIL.name();
} }
public static void setupKey(ClientSessionModel clientSession) {
String secret = HmacOTP.generateSecret(10);
clientSession.setNote(Constants.VERIFY_EMAIL_KEY, secret);
}
} }

View file

@ -23,6 +23,8 @@ 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.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.requiredactions.VerifyEmail;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
@ -49,6 +51,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.RestartLoginCookie;
@ -533,7 +536,7 @@ public class LoginActionsService {
event.event(EventType.VERIFY_EMAIL); event.event(EventType.VERIFY_EMAIL);
if (key != null) { if (key != null) {
Checks checks = new Checks(); Checks checks = new Checks();
if (!checks.verifyCode(key, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) { if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
return checks.response; return checks.response;
} }
ClientSessionCode accessCode = checks.clientCode; ClientSessionCode accessCode = checks.clientCode;
@ -547,11 +550,21 @@ public class LoginActionsService {
UserSessionModel userSession = clientSession.getUserSession(); UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
initEvent(clientSession); initEvent(clientSession);
event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
String keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY);
clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
if (!key.equals(keyFromSession)) {
logger.error("Invalid key for email verification");
event.error(Errors.INVALID_USER_CREDENTIALS);
throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE));
}
user.setEmailVerified(true); user.setEmailVerified(true);
user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); user.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); event.success();
String actionCookieValue = getActionCookie(); String actionCookieValue = getActionCookie();
if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) { if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) {
@ -576,8 +589,11 @@ public class LoginActionsService {
createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); createActionCookie(realm, uriInfo, clientConnection, userSession.getId());
VerifyEmail.setupKey(clientSession);
return session.getProvider(LoginFormsProvider.class) return session.getProvider(LoginFormsProvider.class)
.setClientSessionCode(accessCode.getCode()) .setClientSessionCode(accessCode.getCode())
.setClientSession(clientSession)
.setUser(userSession.getUser()) .setUser(userSession.getUser())
.createResponse(RequiredAction.VERIFY_EMAIL); .createResponse(RequiredAction.VERIFY_EMAIL);
} }

View file

@ -26,7 +26,9 @@ import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.Event; import org.keycloak.events.Event;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -130,7 +132,7 @@ public class RequiredActionEmailVerificationTest {
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]); Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]);
driver.navigate().to(verificationUrl.trim()); driver.navigate().to(verificationUrl.trim());
@ -223,7 +225,7 @@ public class RequiredActionEmailVerificationTest {
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]); Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]);
driver.manage().deleteAllCookies(); driver.manage().deleteAllCookies();
@ -239,6 +241,42 @@ public class RequiredActionEmailVerificationTest {
assertTrue(loginPage.isCurrent()); assertTrue(loginPage.isCurrent());
} }
@Test
public void verifyInvalidKeyOrCode() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");
Assert.assertTrue(verifyEmailPage.isCurrent());
String resendEmailLink = verifyEmailPage.getResendEmailLink();
String keyInsteadCodeURL = resendEmailLink.replace("code=", "key=");
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
Event sendEvent = emailEvent.assertEvent();
String sessionId = sendEvent.getSessionId();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
driver.navigate().to(keyInsteadCodeURL);
events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
.error(Errors.INVALID_CODE)
.client((String)null)
.user((String)null)
.session((String)null)
.clearDetails()
.assertEvent();
String badKeyURL = KeycloakUriBuilder.fromUri(resendEmailLink).queryParam("key", "foo").build().toString();
driver.navigate().to(badKeyURL);
events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
.error(Errors.INVALID_USER_CREDENTIALS)
.session(sessionId)
.detail("email", "test-user@localhost")
.detail(Details.CODE_ID, mailCodeId)
.assertEvent();
}
private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException { private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent(); Multipart multipart = (Multipart) message.getContent();

View file

@ -50,4 +50,8 @@ public class VerifyEmailPage extends AbstractPage {
resendEmailLink.click(); resendEmailLink.click();
} }
public String getResendEmailLink() {
return resendEmailLink.getAttribute("href");
}
} }