KEYCLOAK-704 KEYCLOAK-768 Improvements to access code generation
This commit is contained in:
parent
8adad9dddf
commit
9b0d5acb50
23 changed files with 221 additions and 71 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue