KEYCLOAK-704 KEYCLOAK-768 Improvements to access code generation

This commit is contained in:
Stian Thorgersen 2014-10-31 12:45:03 +01:00
parent 8adad9dddf
commit 9b0d5acb50
23 changed files with 221 additions and 71 deletions

View file

@ -0,0 +1,79 @@
package org.keycloak.connections.jpa.updater.liquibase.custom;
import liquibase.change.custom.CustomSqlChange;
import liquibase.database.Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.CustomChangeException;
import liquibase.exception.SetupException;
import liquibase.exception.ValidationErrors;
import liquibase.resource.ResourceAccessor;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.UpdateStatement;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.util.ArrayList;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AddRealmCodeSecret implements CustomSqlChange {
private String confirmationMessage;
@Override
public SqlStatement[] generateStatements(Database database) throws CustomChangeException {
try {
StringBuilder sb = new StringBuilder();
sb.append("Generated codeSecret for realms: ");
Connection connection = ((JdbcConnection) (database.getConnection())).getWrappedConnection();
ResultSet resultSet = connection.createStatement().executeQuery("SELECT ID FROM REALM WHERE CODE_SECRET IS NULL");
ArrayList<SqlStatement> statements = new ArrayList<SqlStatement>();
while (resultSet.next()) {
String id = resultSet.getString(1);
UpdateStatement statement = new UpdateStatement(null, null, "REALM")
.addNewColumnValue("CODE_SECRET", KeycloakModelUtils.generateCodeSecret())
.setWhereClause("ID='" + id + "'");
statements.add(statement);
if (!resultSet.isFirst()) {
sb.append(", ");
}
sb.append(id);
}
if (!statements.isEmpty()) {
confirmationMessage = sb.toString();
}
return statements.toArray(new SqlStatement[statements.size()]);
} catch (Exception e) {
throw new CustomChangeException("Failed to add realm code secret", e);
}
}
@Override
public String getConfirmationMessage() {
return confirmationMessage;
}
@Override
public void setUp() throws SetupException {
}
@Override
public void setFileOpener(ResourceAccessor resourceAccessor) {
}
@Override
public ValidationErrors validate(Database database) {
return null;
}
}

View file

@ -43,9 +43,10 @@
</addColumn> </addColumn>
<addColumn tableName="REALM"> <addColumn tableName="REALM">
<column name="CERTIFICATE" type="VARCHAR(2048)"/> <column name="CERTIFICATE" type="VARCHAR(2048)"/>
<column name="CODE_SECRET" type="VARCHAR(255)"/>
</addColumn> </addColumn>
<addColumn tableName="CLIENT"> <addColumn tableName="CLIENT">
<column name="NODE_REREG_TIMEOUT" type="INT"/> <column name="NODE_REREG_TIMEOUT" type="INT" defaultValue="0"/>
</addColumn> </addColumn>
<addPrimaryKey columnNames="CLIENT_ID, NAME" constraintName="CONSTRAINT_3C" tableName="CLIENT_ATTRIBUTES"/> <addPrimaryKey columnNames="CLIENT_ID, NAME" constraintName="CONSTRAINT_3C" tableName="CLIENT_ATTRIBUTES"/>
<addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTRAINT_5E" tableName="CLIENT_SESSION_NOTE"/> <addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTRAINT_5E" tableName="CLIENT_SESSION_NOTE"/>
@ -53,5 +54,6 @@
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_ATTRIBUTES" constraintName="FK3C47C64BEACCA966" referencedColumnNames="ID" referencedTableName="CLIENT"/> <addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_ATTRIBUTES" constraintName="FK3C47C64BEACCA966" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_NOTE" constraintName="FK5EDFB00FF51C2736" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/> <addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_NOTE" constraintName="FK5EDFB00FF51C2736" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
<addForeignKeyConstraint baseColumnNames="APPLICATION_ID" baseTableName="APP_NODE_REGISTRATIONS" constraintName="FK8454723BA992F594" referencedColumnNames="ID" referencedTableName="CLIENT"/> <addForeignKeyConstraint baseColumnNames="APPLICATION_ID" baseTableName="APP_NODE_REGISTRATIONS" constraintName="FK8454723BA992F594" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.AddRealmCodeSecret"/>
</changeSet> </changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -1,5 +1,13 @@
package org.keycloak.connections.mongo.updater.updates; package org.keycloak.connections.mongo.updater.updates;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.Arrays;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -14,6 +22,24 @@ public class Update1_1_0_Beta1 extends Update {
public void update() { public void update() {
deleteEntries("clientSessions"); deleteEntries("clientSessions");
deleteEntries("sessions"); deleteEntries("sessions");
addRealmCodeSecret();
}
private void addRealmCodeSecret() {
DBCollection realms = db.getCollection("realms");
DBObject query = new QueryBuilder()
.and("codeSecret").is(null).get();
DBCursor objects = realms.find(query);
while (objects.hasNext()) {
DBObject object = objects.next();
object.put("codeSecret", KeycloakModelUtils.generateCodeSecret());
realms.save(object);
log.debugv("Added realm.codeSecret, id={0}", object.get("id"));
}
} }
} }

View file

@ -44,6 +44,7 @@ public class RealmRepresentation {
protected String privateKey; protected String privateKey;
protected String publicKey; protected String publicKey;
protected String certificate; protected String certificate;
protected String codeSecret;
protected RolesRepresentation roles; protected RolesRepresentation roles;
protected List<String> defaultRoles; protected List<String> defaultRoles;
protected Set<String> requiredCredentials; protected Set<String> requiredCredentials;
@ -229,6 +230,14 @@ public class RealmRepresentation {
this.certificate = certificate; this.certificate = certificate;
} }
public String getCodeSecret() {
return codeSecret;
}
public void setCodeSecret(String codeSecret) {
this.codeSecret = codeSecret;
}
public Boolean isPasswordCredentialGrantAllowed() { public Boolean isPasswordCredentialGrantAllowed() {
return passwordCredentialGrantAllowed; return passwordCredentialGrantAllowed;
} }

View file

@ -50,8 +50,6 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setClient(ClientModel client); public LoginFormsProvider setClient(ClientModel client);
LoginFormsProvider setVerifyCode(String code);
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams); public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams);
public LoginFormsProvider setFormData(MultivaluedMap<String, String> formData); public LoginFormsProvider setFormData(MultivaluedMap<String, String> formData);

View file

@ -51,7 +51,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class); private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class);
private String verifyCode;
private String message; private String message;
private String accessCode; private String accessCode;
private Response.Status status = Response.Status.OK; private Response.Status status = Response.Status.OK;
@ -110,8 +109,7 @@ 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("code", accessCode); builder.queryParam("key", accessCode);
builder.queryParam("key", verifyCode);
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());
@ -311,12 +309,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return this; return this;
} }
@Override
public LoginFormsProvider setVerifyCode(String code) {
this.verifyCode = code;
return this;
}
@Override @Override
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams) { public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams) {
this.queryParams = queryParams; this.queryParams = queryParams;

View file

@ -97,6 +97,10 @@ public interface RealmModel extends RoleContainerModel {
void setPublicKey(PublicKey publicKey); void setPublicKey(PublicKey publicKey);
String getCodeSecret();
void setCodeSecret(String codeSecret);
X509Certificate getCertificate(); X509Certificate getCertificate();
void setCertificate(X509Certificate certificate); void setCertificate(X509Certificate certificate);
String getCertificatePem(); String getCertificatePem();

View file

@ -41,6 +41,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private String publicKeyPem; private String publicKeyPem;
private String privateKeyPem; private String privateKeyPem;
private String certificatePem; private String certificatePem;
private String codeSecret;
private String loginTheme; private String loginTheme;
private String accountTheme; private String accountTheme;
@ -271,6 +272,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
this.privateKeyPem = privateKeyPem; this.privateKeyPem = privateKeyPem;
} }
public String getCodeSecret() {
return codeSecret;
}
public void setCodeSecret(String codeSecret) {
this.codeSecret = codeSecret;
}
public String getLoginTheme() { public String getLoginTheme() {
return loginTheme; return loginTheme;
} }

View file

@ -119,6 +119,8 @@ public final class KeycloakModelUtils {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
realm.setCertificate(certificate); realm.setCertificate(certificate);
realm.setCodeSecret(generateCodeSecret());
} }
public static void generateRealmCertificate(RealmModel realm) { public static void generateRealmCertificate(RealmModel realm) {
@ -161,6 +163,10 @@ public final class KeycloakModelUtils {
return secret; return secret;
} }
public static String generateCodeSecret() {
return UUID.randomUUID().toString();
}
public static ApplicationModel createApplication(RealmModel realm, String name) { public static ApplicationModel createApplication(RealmModel realm, String name) {
ApplicationModel app = realm.addApplication(name); ApplicationModel app = realm.addApplication(name);
generateSecret(app); generateSecret(app);

View file

@ -91,6 +91,7 @@ public class ModelToRepresentation {
KeycloakModelUtils.generateRealmCertificate(realm); KeycloakModelUtils.generateRealmCertificate(realm);
} }
rep.setCertificate(realm.getCertificatePem()); rep.setCertificate(realm.getCertificatePem());
rep.setCodeSecret(realm.getCodeSecret());
rep.setPasswordCredentialGrantAllowed(realm.isPasswordCredentialGrantAllowed()); rep.setPasswordCredentialGrantAllowed(realm.isPasswordCredentialGrantAllowed());
rep.setRegistrationAllowed(realm.isRegistrationAllowed()); rep.setRegistrationAllowed(realm.isRegistrationAllowed());
rep.setRememberMe(realm.isRememberMe()); rep.setRememberMe(realm.isRememberMe());

View file

@ -92,6 +92,12 @@ public class RepresentationToModel {
} else { } else {
newRealm.setCertificatePem(rep.getCertificate()); newRealm.setCertificatePem(rep.getCertificate());
} }
if (rep.getCodeSecret() == null) {
newRealm.setCodeSecret(KeycloakModelUtils.generateCodeSecret());
} else {
newRealm.setCodeSecret(rep.getCodeSecret());
}
if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme()); if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme());
if (rep.getAccountTheme() != null) newRealm.setAccountTheme(rep.getAccountTheme()); if (rep.getAccountTheme() != null) newRealm.setAccountTheme(rep.getAccountTheme());
if (rep.getAdminTheme() != null) newRealm.setAdminTheme(rep.getAdminTheme()); if (rep.getAdminTheme() != null) newRealm.setAdminTheme(rep.getAdminTheme());

View file

@ -374,7 +374,16 @@ public class RealmAdapter implements RealmModel {
setPrivateKeyPem(privateKeyPem); setPrivateKeyPem(privateKeyPem);
} }
@Override
public String getCodeSecret() {
return updated != null ? updated.getCodeSecret() : cached.getCodeSecret();
}
@Override
public void setCodeSecret(String codeSecret) {
getDelegateForUpdate();
updated.setCodeSecret(codeSecret);
}
@Override @Override
public List<RequiredCredentialModel> getRequiredCredentials() { public List<RequiredCredentialModel> getRequiredCredentials() {

View file

@ -57,6 +57,7 @@ public class CachedRealm {
private String publicKeyPem; private String publicKeyPem;
private String privateKeyPem; private String privateKeyPem;
private String certificatePem; private String certificatePem;
private String codeSecret;
private String loginTheme; private String loginTheme;
private String accountTheme; private String accountTheme;
@ -115,6 +116,7 @@ public class CachedRealm {
publicKeyPem = model.getPublicKeyPem(); publicKeyPem = model.getPublicKeyPem();
privateKeyPem = model.getPrivateKeyPem(); privateKeyPem = model.getPrivateKeyPem();
certificatePem = model.getCertificatePem(); certificatePem = model.getCertificatePem();
codeSecret = model.getCodeSecret();
loginTheme = model.getLoginTheme(); loginTheme = model.getLoginTheme();
accountTheme = model.getAccountTheme(); accountTheme = model.getAccountTheme();
@ -267,6 +269,10 @@ public class CachedRealm {
return privateKeyPem; return privateKeyPem;
} }
public String getCodeSecret() {
return codeSecret;
}
public List<RequiredCredentialModel> getRequiredCredentials() { public List<RequiredCredentialModel> getRequiredCredentials() {
return requiredCredentials; return requiredCredentials;
} }

View file

@ -434,6 +434,16 @@ public class RealmAdapter implements RealmModel {
setPrivateKeyPem(privateKeyPem); setPrivateKeyPem(privateKeyPem);
} }
@Override
public String getCodeSecret() {
return realm.getCodeSecret();
}
@Override
public void setCodeSecret(String codeSecret) {
realm.setCodeSecret(codeSecret);
}
protected RequiredCredentialModel initRequiredCredentialModel(String type) { protected RequiredCredentialModel initRequiredCredentialModel(String type) {
RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type); RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type);
if (model == null) { if (model == null) {

View file

@ -82,6 +82,8 @@ public class RealmEntity {
protected String privateKeyPem; protected String privateKeyPem;
@Column(name="CERTIFICATE", length = 2048) @Column(name="CERTIFICATE", length = 2048)
protected String certificatePem; protected String certificatePem;
@Column(name="CODE_SECRET", length = 255)
protected String codeSecret;
@Column(name="LOGIN_THEME") @Column(name="LOGIN_THEME")
protected String loginTheme; protected String loginTheme;
@ -284,6 +286,14 @@ public class RealmEntity {
this.privateKeyPem = privateKeyPem; this.privateKeyPem = privateKeyPem;
} }
public String getCodeSecret() {
return codeSecret;
}
public void setCodeSecret(String codeSecret) {
this.codeSecret = codeSecret;
}
public Collection<RequiredCredentialEntity> getRequiredCredentials() { public Collection<RequiredCredentialEntity> getRequiredCredentials() {
return requiredCredentials; return requiredCredentials;
} }

View file

@ -419,6 +419,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
setPrivateKeyPem(privateKeyPem); setPrivateKeyPem(privateKeyPem);
} }
@Override
public String getCodeSecret() {
return realm.getCodeSecret();
}
@Override
public void setCodeSecret(String codeSecret) {
realm.setCodeSecret(codeSecret);
updateRealm();
}
@Override @Override
public String getLoginTheme() { public String getLoginTheme() {
return realm.getLoginTheme(); return realm.getLoginTheme();

View file

@ -16,6 +16,7 @@ import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OpenIDConnectService; import org.keycloak.protocol.oidc.OpenIDConnectService;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
@ -201,6 +202,7 @@ public class SamlService {
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirect); clientSession.setRedirectUri(redirect);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
clientSession.setNote(SamlProtocol.SAML_BINDING, getBindingType()); clientSession.setNote(SamlProtocol.SAML_BINDING, getBindingType());
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
clientSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID()); clientSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());

View file

@ -550,7 +550,7 @@ public class OpenIDConnectService {
String[] parts = code.split("\\."); String[] parts = code.split("\\.");
if (parts.length == 2) { if (parts.length == 2) {
try { try {
event.detail(Details.CODE_ID, new String(Base64Url.decode(parts[1]))); event.detail(Details.CODE_ID, new String(parts[1]));
} catch (Throwable t) { } catch (Throwable t) {
} }
} }
@ -776,6 +776,7 @@ public class OpenIDConnectService {
clientSession.setAuthMethod(OpenIDConnect.LOGIN_PROTOCOL); clientSession.setAuthMethod(OpenIDConnect.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirect); clientSession.setRedirectUri(redirect);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
clientSession.setNote(OpenIDConnect.STATE_PARAM, state); clientSession.setNote(OpenIDConnect.STATE_PARAM, state);
if (scopeParam != null) clientSession.setNote(OpenIDConnect.SCOPE_PARAM, scopeParam); if (scopeParam != null) clientSession.setNote(OpenIDConnect.SCOPE_PARAM, scopeParam);
if (responseType != null) clientSession.setNote(OpenIDConnect.RESPONSE_TYPE_PARAM, responseType); if (responseType != null) clientSession.setNote(OpenIDConnect.RESPONSE_TYPE_PARAM, responseType);

View file

@ -290,9 +290,6 @@ public class AuthenticationManager {
LoginFormsProvider loginFormsProvider = Flows.forms(session, realm, client, uriInfo).setClientSessionCode(accessCode.getCode()).setUser(user); LoginFormsProvider loginFormsProvider = Flows.forms(session, realm, client, uriInfo).setClientSessionCode(accessCode.getCode()).setUser(user);
if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL)) { if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL)) {
String key = UUID.randomUUID().toString();
clientSession.setNote("key", key);
loginFormsProvider.setVerifyCode(key);
event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success();
} }

View file

@ -1,8 +1,5 @@
package org.keycloak.services.managers; package org.keycloak.services.managers;
import org.keycloak.OAuthErrorException;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -11,11 +8,10 @@ import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.util.Base64Url; import org.keycloak.util.Base64Url;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import java.nio.ByteBuffer;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.Signature;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.UUID;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -23,6 +19,10 @@ import java.util.Set;
*/ */
public class ClientSessionCode { public class ClientSessionCode {
public static final String ACTION_KEY = "action_key";
private static final byte[] HASH_SEPERATOR = "//".getBytes();
private final RealmModel realm; private final RealmModel realm;
private final ClientSessionModel clientSession; private final ClientSessionModel clientSession;
@ -34,14 +34,14 @@ public class ClientSessionCode {
public static ClientSessionCode parse(String code, KeycloakSession session) { public static ClientSessionCode parse(String code, KeycloakSession session) {
try { try {
String[] parts = code.split("\\."); String[] parts = code.split("\\.");
String id = new String(Base64Url.decode(parts[1])); String id = parts[1];
ClientSessionModel clientSession = session.sessions().getClientSession(id); ClientSessionModel clientSession = session.sessions().getClientSession(id);
if (clientSession == null) { if (clientSession == null) {
return null; return null;
} }
String hash = createSignatureHash(clientSession.getRealm(), clientSession); String hash = createHash(clientSession.getRealm(), clientSession);
if (!hash.equals(parts[0])) { if (!hash.equals(parts[0])) {
return null; return null;
} }
@ -56,14 +56,14 @@ public class ClientSessionCode {
public static ClientSessionCode parse(String code, KeycloakSession session, RealmModel realm) { public static ClientSessionCode parse(String code, KeycloakSession session, RealmModel realm) {
try { try {
String[] parts = code.split("\\."); String[] parts = code.split("\\.");
String id = new String(Base64Url.decode(parts[1])); String id = parts[1];
ClientSessionModel clientSession = session.sessions().getClientSession(realm, id); ClientSessionModel clientSession = session.sessions().getClientSession(realm, id);
if (clientSession == null) { if (clientSession == null) {
return null; return null;
} }
String hash = createSignatureHash(realm, clientSession); String hash = createHash(realm, clientSession);
if (!hash.equals(parts[0])) { if (!hash.equals(parts[0])) {
return null; return null;
} }
@ -111,6 +111,7 @@ public class ClientSessionCode {
public void setAction(ClientSessionModel.Action action) { public void setAction(ClientSessionModel.Action action) {
clientSession.setAction(action); clientSession.setAction(action);
clientSession.setNote(ACTION_KEY, UUID.randomUUID().toString());
clientSession.setTimestamp(Time.currentTime()); clientSession.setTimestamp(Time.currentTime());
} }
@ -138,29 +139,24 @@ public class ClientSessionCode {
} }
private static String generateCode(RealmModel realm, ClientSessionModel clientSession) { private static String generateCode(RealmModel realm, ClientSessionModel clientSession) {
String hash = createSignatureHash(realm, clientSession); String hash = createHash(realm, clientSession);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(hash); sb.append(hash);
sb.append("."); sb.append(".");
sb.append(Base64Url.encode(clientSession.getId().getBytes())); sb.append(clientSession.getId());
return sb.toString(); return sb.toString();
} }
private static String createSignatureHash(RealmModel realm, ClientSessionModel clientSession) { private static String createHash(RealmModel realm, ClientSessionModel clientSession) {
try { try {
Signature signature = Signature.getInstance(RSAProvider.getJavaAlgorithm(Algorithm.RS256)); MessageDigest digest = MessageDigest.getInstance("sha-256");
signature.initSign(realm.getPrivateKey()); digest.update(realm.getCodeSecret().getBytes());
signature.update(clientSession.getId().getBytes()); digest.update(HASH_SEPERATOR);
signature.update(ByteBuffer.allocate(4).putInt(clientSession.getTimestamp())); digest.update(clientSession.getId().getBytes());
if (clientSession.getAction() != null) { digest.update(HASH_SEPERATOR);
signature.update(clientSession.getAction().toString().getBytes()); digest.update(clientSession.getNote(ACTION_KEY).getBytes());
}
byte[] sign = signature.sign();
MessageDigest digest = MessageDigest.getInstance("sha-1");
digest.update(sign);
return Base64Url.encode(digest.digest()); return Base64Url.encode(digest.digest());
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);

View file

@ -714,22 +714,17 @@ public class LoginActionsService {
@Path("email-verification") @Path("email-verification")
@GET @GET
public Response emailVerification(@QueryParam("code") String code) { public Response emailVerification(@QueryParam("code") String code, @QueryParam("key") String key) {
event.event(EventType.VERIFY_EMAIL); event.event(EventType.VERIFY_EMAIL);
if (uriInfo.getQueryParameters().containsKey("key")) { if (key != null) {
Checks checks = new Checks(); Checks checks = new Checks();
if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL)) { if (!checks.check(key, ClientSessionModel.Action.VERIFY_EMAIL)) {
return checks.response; return checks.response;
} }
ClientSessionCode accessCode = checks.clientCode; ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession(); ClientSessionModel clientSession = accessCode.getClientSession();
UserSessionModel userSession = clientSession.getUserSession(); UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
String key = uriInfo.getQueryParameters().getFirst("key");
String keyNote = clientSession.getNote("key");
if (key == null || !key.equals(keyNote)) {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Somebody is trying to illegally change your email.");
}
initEvent(clientSession); initEvent(clientSession);
user.setEmailVerified(true); user.setEmailVerified(true);
@ -745,16 +740,11 @@ public class LoginActionsService {
} }
ClientSessionCode accessCode = checks.clientCode; ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession(); ClientSessionModel clientSession = accessCode.getClientSession();
String verifyCode = UUID.randomUUID().toString();
clientSession.setNote("key", verifyCode);
UserSessionModel userSession = clientSession.getUserSession(); UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser();
initEvent(clientSession); initEvent(clientSession);
return Flows.forms(session, realm, null, uriInfo) return Flows.forms(session, realm, null, uriInfo)
.setClientSessionCode(accessCode.getCode()) .setClientSessionCode(accessCode.getCode())
.setVerifyCode(verifyCode)
.setUser(userSession.getUser()) .setUser(userSession.getUser())
.createResponse(RequiredAction.VERIFY_EMAIL); .createResponse(RequiredAction.VERIFY_EMAIL);
} }
@ -762,22 +752,14 @@ public class LoginActionsService {
@Path("password-reset") @Path("password-reset")
@GET @GET
public Response passwordReset(@QueryParam("code") String code) { public Response passwordReset(@QueryParam("code") String code, @QueryParam("key") String key) {
event.event(EventType.SEND_RESET_PASSWORD); event.event(EventType.SEND_RESET_PASSWORD);
if (uriInfo.getQueryParameters().containsKey("key")) { if (key != null) {
Checks checks = new Checks(); Checks checks = new Checks();
if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD)) { if (!checks.check(key, ClientSessionModel.Action.UPDATE_PASSWORD)) {
return checks.response; return checks.response;
} }
ClientSessionCode accessCode = checks.clientCode; ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession();
UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser();
String key = uriInfo.getQueryParameters().getFirst("key");
String keyNote = clientSession.getNote("key");
if (key == null || !key.equals(keyNote)) {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Somebody is trying to illegally change your password.");
}
return Flows.forms(session, realm, null, uriInfo) return Flows.forms(session, realm, null, uriInfo)
.setClientSessionCode(accessCode.getCode()) .setClientSessionCode(accessCode.getCode())
.createResponse(RequiredAction.UPDATE_PASSWORD); .createResponse(RequiredAction.UPDATE_PASSWORD);
@ -842,10 +824,7 @@ public class LoginActionsService {
try { try {
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
builder.queryParam("code", accessCode.getCode()); builder.queryParam("key", accessCode.getCode());
String verifyCode = UUID.randomUUID().toString();
clientSession.setNote("key", verifyCode);
builder.queryParam("key", verifyCode);
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());

View file

@ -709,9 +709,6 @@ public class UsersResource {
try { try {
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
builder.queryParam("code", accessCode.getCode()); builder.queryParam("code", accessCode.getCode());
String key = UUID.randomUUID().toString();
clientSession.setNote("key", key);
builder.queryParam("key", 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());

View file

@ -122,7 +122,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]); Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]);
driver.navigate().to(verificationUrl.trim()); driver.navigate().to(verificationUrl.trim());