Skip to content

Commit

Permalink
login timeouts, verify email
Browse files Browse the repository at this point in the history
  • Loading branch information
patriot1burke committed Jul 25, 2015
1 parent 6826336 commit 33f0100
Show file tree
Hide file tree
Showing 21 changed files with 417 additions and 64 deletions.
Expand Up @@ -63,7 +63,7 @@ public static byte[] sign(byte[] data, Algorithm algorithm, SecretKey key) {

public static boolean verify(JWSInput input, SecretKey key) {
try {
byte[] signature = sign(input.getContent(), input.getHeader().getAlgorithm(), key);
byte[] signature = sign(input.getEncodedSignatureInput().getBytes("UTF-8"), input.getHeader().getAlgorithm(), key);
String x = Base64Url.encode(signature);
return x.equals(input.getEncodedSignature());
} catch (Exception e) {
Expand Down
1 change: 1 addition & 0 deletions core/src/test/java/org/keycloak/RSAVerifierTest.java
Expand Up @@ -96,6 +96,7 @@ public void testSimpleVerification() throws Exception {
String encoded = new JWSBuilder()
.jsonContent(token)
.rsa256(idpPair.getPrivate());
System.out.print("encoded size: " + encoded.length());
AccessToken token = verifySkeletonKeyToken(encoded);
Assert.assertTrue(token.getResourceAccess("service").getRoles().contains("admin"));
Assert.assertEquals("CN=Client", token.getSubject());
Expand Down
27 changes: 27 additions & 0 deletions core/src/test/java/org/keycloak/jose/HmacTest.java
@@ -0,0 +1,27 @@
package org.keycloak.jose;

import org.junit.Assert;
import org.junit.Test;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.HMACProvider;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.UUID;

/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class HmacTest {

@Test
public void testHmacSignatures() throws Exception {
SecretKey secret = new SecretKeySpec(UUID.randomUUID().toString().getBytes(), "HmacSHA256");
String encoded = new JWSBuilder().content("hello world".getBytes())
.hmac256(secret);
JWSInput input = new JWSInput(encoded);
Assert.assertTrue(HMACProvider.verify(input, secret));
}
}
1 change: 1 addition & 0 deletions events/api/src/main/java/org/keycloak/events/Details.java
Expand Up @@ -27,6 +27,7 @@ public interface Details {
String REVOKED_CLIENT = "revoked_client";
String CLIENT_SESSION_STATE = "client_session_state";
String CLIENT_SESSION_HOST = "client_session_host";
String RESTART_AFTER_TIMEOUT = "restart_after_timeout";

String CONSENT = "consent";
String CONSENT_VALUE_NO_CONSENT_REQUIRED = "no_consent_required"; // No consent is required by client
Expand Down
2 changes: 2 additions & 0 deletions events/api/src/main/java/org/keycloak/events/Errors.java
Expand Up @@ -13,6 +13,8 @@ public interface Errors {
String CLIENT_DISABLED = "client_disabled";
String INVALID_CLIENT_CREDENTIALS = "invalid_client_credentials";
String INVALID_CLIENT = "invalid_client";
String CONSENT_DENIED = "consent_denied";
String RESOLVE_REQUIRED_ACTIONS = "resolve_required_actions";

String USER_NOT_FOUND = "user_not_found";
String USER_DISABLED = "user_disabled";
Expand Down
Expand Up @@ -22,6 +22,7 @@
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;
import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper;
import org.keycloak.protocol.saml.mappers.SAMLRoleListMapper;
Expand Down Expand Up @@ -141,6 +142,7 @@ public SamlProtocol setEventBuilder(EventBuilder event) {

@Override
public Response cancelLogin(ClientSessionModel clientSession) {
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
Map<String, String> params = new HashMap<>();
Expand Down Expand Up @@ -443,6 +445,7 @@ public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMa

@Override
public Response consentDenied(ClientSessionModel clientSession) {
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
session.sessions().removeClientSession(realm, clientSession);
return ErrorPage.error(session, Messages.CONSENT_DENIED);
Expand Down
Expand Up @@ -26,6 +26,7 @@
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
Expand Down Expand Up @@ -513,6 +514,7 @@ protected Response newBrowserAuthentication(ClientSessionModel clientSession) {
.setRequest(request);

try {
RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
return processor.authenticate();
} catch (Exception e) {
return processor.handleBrowserException(e);
Expand Down
Expand Up @@ -498,6 +498,7 @@ public Response authenticate() throws AuthException {
}

public static void resetFlow(ClientSessionModel clientSession) {
clientSession.setTimestamp(Time.currentTime());
clientSession.setAuthenticatedUser(null);
clientSession.clearExecutionStatus();
clientSession.clearUserSessionNotes();
Expand Down Expand Up @@ -574,7 +575,8 @@ public void attachSession() {
String attemptedUsername = clientSession.getNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME);
if (attemptedUsername != null) username = attemptedUsername;
if (userSession == null) { // if no authenticator attached a usersession
userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", false, null, null);
boolean remember = "true".equals(clientSession.getNote(Details.REMEMBER_ME));
userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", remember, null, null);
userSession.setState(UserSessionModel.State.LOGGING_IN);
userSessionCreated = true;
}
Expand Down
Expand Up @@ -10,6 +10,7 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.managers.AuthenticationManager;

Expand Down
Expand Up @@ -20,10 +20,6 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
protected static Logger logger = Logger.getLogger(UpdateProfile.class);
@Override
public void evaluateTriggers(RequiredActionContext context) {
if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {
context.getUser().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
logger.debug("User is required to verify email");
}
}

@Override
Expand Down
Expand Up @@ -28,29 +28,11 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
protected static Logger logger = Logger.getLogger(VerifyEmail.class);
@Override
public void evaluateTriggers(RequiredActionContext context) {
int daysToExpirePassword = context.getRealm().getPasswordPolicy().getDaysToExpirePassword();
if(daysToExpirePassword != -1) {
for (UserCredentialValueModel entity : context.getUser().getCredentialsDirectly()) {
if (entity.getType().equals(UserCredentialModel.PASSWORD)) {

if(entity.getCreatedDate() == null) {
context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
logger.debug("User is required to update password");
} else {
long timeElapsed = Time.toMillis(Time.currentTime()) - entity.getCreatedDate();
long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword);

if(timeElapsed > timeToExpire) {
context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
logger.debug("User is required to update password");
}
}
break;
}
}
if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {
context.getUser().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
logger.debug("User is required to verify email");
}
}

@Override
public Response invokeRequiredAction(RequiredActionContext context) {
if (Validation.isBlank(context.getUser().getEmail())) {
Expand Down
175 changes: 175 additions & 0 deletions services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
@@ -0,0 +1,175 @@
package org.keycloak.protocol;

import org.codehaus.jackson.annotate.JsonProperty;
import org.jboss.logging.Logger;
import org.keycloak.ClientConnection;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.CookieHelper;

import javax.crypto.SecretKey;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.UriInfo;
import java.util.HashMap;
import java.util.Map;

/**
* This is an an encoded token that is stored as a cookie so that if there is a client timeout, then the client session
* can be restarted.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RestartLoginCookie {
private static final Logger logger = Logger.getLogger(RestartLoginCookie.class);
public static final String KC_RESTART = "KC_RESTART";
@JsonProperty("cs")
protected String clientSession;

@JsonProperty("cid")
protected String clientId;

@JsonProperty("pty")
protected String authMethod;

@JsonProperty("ruri")
protected String redirectUri;

@JsonProperty("act")
protected String action;

@JsonProperty("notes")
protected Map<String, String> notes = new HashMap<>();

public String getClientSession() {
return clientSession;
}

public void setClientSession(String clientSession) {
this.clientSession = clientSession;
}

public Map<String, String> getNotes() {
return notes;
}

public void setNotes(Map<String, String> notes) {
this.notes = notes;
}

public String getClientId() {
return clientId;
}

public void setClientId(String clientId) {
this.clientId = clientId;
}

public String getAuthMethod() {
return authMethod;
}

public void setAuthMethod(String authMethod) {
this.authMethod = authMethod;
}

public String getRedirectUri() {
return redirectUri;
}

public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}

public String getAction() {
return action;
}

public void setAction(String action) {
this.action = action;
}

public String encode(RealmModel realm) {
JWSBuilder builder = new JWSBuilder();
return builder.jsonContent(this)
.hmac256((SecretKey)realm.getCodeSecretKey());
//.rsa256(realm.getPrivateKey());

}

public RestartLoginCookie() {
}
public RestartLoginCookie(ClientSessionModel clientSession) {
this.action = clientSession.getAction();
this.clientId = clientSession.getClient().getClientId();
this.authMethod = clientSession.getAuthMethod();
this.redirectUri = clientSession.getRedirectUri();
this.clientSession = clientSession.getId();
for (Map.Entry<String, String> entry : clientSession.getNotes().entrySet()) {
notes.put(entry.getKey(), entry.getValue());
}
}

public static void setRestartCookie(RealmModel realm, ClientConnection connection, UriInfo uriInfo, ClientSessionModel clientSession) {
RestartLoginCookie restart = new RestartLoginCookie(clientSession);
String encoded = restart.encode(realm);
int keySize = realm.getCodeSecret().length();
int size = encoded.length();
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
boolean secureOnly = realm.getSslRequired().isRequired(connection);
CookieHelper.addCookie(KC_RESTART, encoded, path, null, null, -1, secureOnly, true);
}

public static void expireRestartCookie(RealmModel realm, ClientConnection connection, UriInfo uriInfo) {
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
boolean secureOnly = realm.getSslRequired().isRequired(connection);
CookieHelper.addCookie(KC_RESTART, "", path, null, null, 0, secureOnly, true);
}

public static ClientSessionModel restartSession(KeycloakSession session, RealmModel realm, String code) throws Exception {
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
if (cook == null) {
logger.debug("KC_RESTART cookie doesn't exist");
return null;
}
String encodedCookie = cook.getValue();
JWSInput input = new JWSInput(encodedCookie);
/*
if (!RSAProvider.verify(input, realm.getPublicKey())) {
logger.debug("Failed to verify encoded RestartLoginCookie");
return null;
}
*/
if (!HMACProvider.verify(input, (SecretKey)realm.getCodeSecretKey())) {
logger.debug("Failed to verify encoded RestartLoginCookie");
return null;
}
RestartLoginCookie cookie = input.readJsonContent(RestartLoginCookie.class);
String[] parts = code.split("\\.");
String clientSessionId = parts[1];
if (!clientSessionId.equals(cookie.getClientSession())) {
logger.debug("RestartLoginCookie clientSession does not match code's clientSession");
return null;
}

ClientModel client = realm.getClientByClientId(cookie.getClientId());
if (client == null) return null;

ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAuthMethod(cookie.getAuthMethod());
clientSession.setRedirectUri(cookie.getRedirectUri());
clientSession.setAction(cookie.getAction());
for (Map.Entry<String, String> entry : cookie.getNotes().entrySet()) {
clientSession.setNote(entry.getKey(), entry.getValue());
}

return clientSession;
}
}
Expand Up @@ -32,6 +32,7 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;

Expand Down Expand Up @@ -124,6 +125,7 @@ public Response cancelLogin(ClientSessionModel clientSession) {
redirectUri.queryParam(OAuth2Constants.STATE, state);
}
session.sessions().removeClientSession(realm, clientSession);
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
return Response.status(302).location(redirectUri.build()).build();
}

Expand All @@ -149,6 +151,7 @@ public Response consentDenied(ClientSessionModel clientSession) {
if (state != null)
redirectUri.queryParam(OAuth2Constants.STATE, state);
session.sessions().removeClientSession(realm, clientSession);
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
return location.build();
}
Expand Down
Expand Up @@ -19,6 +19,7 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.ErrorPageException;
Expand Down Expand Up @@ -261,6 +262,7 @@ protected Response browserAuthentication(String accessCode) {
}
clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);


AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
Expand Down Expand Up @@ -295,6 +297,7 @@ protected Response browserAuthentication(String accessCode) {
if (challenge == null) {
return processor.finishAuthentication();
} else {
RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
return challenge;
}
}
Expand Down

0 comments on commit 33f0100

Please sign in to comment.