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 tableName="REALM">
<column name="CERTIFICATE" type="VARCHAR(2048)"/>
<column name="CODE_SECRET" type="VARCHAR(255)"/>
</addColumn>
<addColumn tableName="CLIENT">
<column name="NODE_REREG_TIMEOUT" type="INT"/>
<column name="NODE_REREG_TIMEOUT" type="INT" defaultValue="0"/>
</addColumn>
<addPrimaryKey columnNames="CLIENT_ID, NAME" constraintName="CONSTRAINT_3C" tableName="CLIENT_ATTRIBUTES"/>
<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_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"/>
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.AddRealmCodeSecret"/>
</changeSet>
</databaseChangeLog>

View file

@ -1,5 +1,13 @@
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>
*/
@ -14,6 +22,24 @@ public class Update1_1_0_Beta1 extends Update {
public void update() {
deleteEntries("clientSessions");
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 publicKey;
protected String certificate;
protected String codeSecret;
protected RolesRepresentation roles;
protected List<String> defaultRoles;
protected Set<String> requiredCredentials;
@ -229,6 +230,14 @@ public class RealmRepresentation {
this.certificate = certificate;
}
public String getCodeSecret() {
return codeSecret;
}
public void setCodeSecret(String codeSecret) {
this.codeSecret = codeSecret;
}
public Boolean isPasswordCredentialGrantAllowed() {
return passwordCredentialGrantAllowed;
}

View file

@ -50,8 +50,6 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setClient(ClientModel client);
LoginFormsProvider setVerifyCode(String code);
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams);
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 String verifyCode;
private String message;
private String accessCode;
private Response.Status status = Response.Status.OK;
@ -110,8 +109,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case VERIFY_EMAIL:
try {
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
builder.queryParam("code", accessCode);
builder.queryParam("key", verifyCode);
builder.queryParam("key", accessCode);
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
@ -311,12 +309,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return this;
}
@Override
public LoginFormsProvider setVerifyCode(String code) {
this.verifyCode = code;
return this;
}
@Override
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams) {
this.queryParams = queryParams;

View file

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

View file

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

View file

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

View file

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

View file

@ -92,6 +92,12 @@ public class RepresentationToModel {
} else {
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.getAccountTheme() != null) newRealm.setAccountTheme(rep.getAccountTheme());
if (rep.getAdminTheme() != null) newRealm.setAdminTheme(rep.getAdminTheme());

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -709,9 +709,6 @@ public class UsersResource {
try {
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
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();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());

View file

@ -122,7 +122,7 @@ public class RequiredActionEmailVerificationTest {
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());