Rework logic to fetch IDPs for the login page so that IDPs are fetched from the provider and not filtered in code.
Closes #32090 Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
parent
b6a82964ed
commit
f82159cf65
29 changed files with 607 additions and 304 deletions
|
@ -54,8 +54,10 @@ public class IdentityProviderRepresentation {
|
||||||
protected boolean addReadTokenRoleOnCreate;
|
protected boolean addReadTokenRoleOnCreate;
|
||||||
protected boolean authenticateByDefault;
|
protected boolean authenticateByDefault;
|
||||||
protected boolean linkOnly;
|
protected boolean linkOnly;
|
||||||
|
protected boolean hideOnLogin;
|
||||||
protected String firstBrokerLoginFlowAlias;
|
protected String firstBrokerLoginFlowAlias;
|
||||||
protected String postBrokerLoginFlowAlias;
|
protected String postBrokerLoginFlowAlias;
|
||||||
|
protected String organizationId;
|
||||||
protected Map<String, String> config = new HashMap<>();
|
protected Map<String, String> config = new HashMap<>();
|
||||||
|
|
||||||
public String getInternalId() {
|
public String getInternalId() {
|
||||||
|
@ -106,6 +108,14 @@ public class IdentityProviderRepresentation {
|
||||||
this.linkOnly = linkOnly;
|
this.linkOnly = linkOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isHideOnLogin() {
|
||||||
|
return this.hideOnLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHideOnLogin(boolean hideOnLogin) {
|
||||||
|
this.hideOnLogin = hideOnLogin;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Deprecated because replaced by {@link #updateProfileFirstLoginMode}. Kept here to allow import of old realms.
|
* Deprecated because replaced by {@link #updateProfileFirstLoginMode}. Kept here to allow import of old realms.
|
||||||
|
@ -194,4 +204,12 @@ public class IdentityProviderRepresentation {
|
||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOrganizationId() {
|
||||||
|
return this.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrganizationId(String organizationId) {
|
||||||
|
this.organizationId = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,3 +147,11 @@ If you wish to disable placeholder replacement for the `import` command, add the
|
||||||
You can still override automatic detection by specifying the `https-key-store-type` and `https-trust-store-type` explicitly. The same applies to the management interface and its `https-management-key-store-type`. Restrictions for the FIPS strict mode stay unchanged.
|
You can still override automatic detection by specifying the `https-key-store-type` and `https-trust-store-type` explicitly. The same applies to the management interface and its `https-management-key-store-type`. Restrictions for the FIPS strict mode stay unchanged.
|
||||||
|
|
||||||
NOTE: The `+spi-truststore-file-*+` options and the truststore related options `+https-trust-store-*+` are deprecated, we strongly recommend to use System Truststore. For more details refer to the relevant https://www.keycloak.org/server/keycloak-truststore[guide].
|
NOTE: The `+spi-truststore-file-*+` options and the truststore related options `+https-trust-store-*+` are deprecated, we strongly recommend to use System Truststore. For more details refer to the relevant https://www.keycloak.org/server/keycloak-truststore[guide].
|
||||||
|
|
||||||
|
= Improving performance for selection of identity providers
|
||||||
|
|
||||||
|
New indexes were added to the `IDENTITY_PROVIDER` table to improve the performance of queries that fetch the IDPs associated with an organization, and fetch IDPs that are available for login (those that are `enabled`, not `link_only`, not marked as `hide_on_login`).
|
||||||
|
|
||||||
|
If the table currently contains more than 300.000 entries,
|
||||||
|
{project_name} will skip the creation of the indexes by default during the automatic schema migration, and will instead log the SQL statements
|
||||||
|
on the console during migration. In this case, the statements must be run manually in the DB after {project_name}'s startup.
|
||||||
|
|
|
@ -76,7 +76,7 @@ const OrganizationLink = (identityProvider: IdentityProviderRepresentation) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
|
|
||||||
if (!identityProvider.config?.["kc.org"]) {
|
if (!identityProvider?.organizationId) {
|
||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ const OrganizationLink = (identityProvider: IdentityProviderRepresentation) => {
|
||||||
key={identityProvider.providerId}
|
key={identityProvider.providerId}
|
||||||
to={toEditOrganization({
|
to={toEditOrganization({
|
||||||
realm,
|
realm,
|
||||||
id: identityProvider.config["kc.org"],
|
id: identityProvider.organizationId,
|
||||||
tab: "identityProviders",
|
tab: "identityProviders",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -299,7 +299,7 @@ export default function IdentityProvidersSection() {
|
||||||
cellFormatters: [upperCaseFormatter()],
|
cellFormatters: [upperCaseFormatter()],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "config['kc.org']",
|
name: "organizationId",
|
||||||
displayKey: "linkedOrganization",
|
displayKey: "linkedOrganization",
|
||||||
cellRenderer: OrganizationLink,
|
cellRenderer: OrganizationLink,
|
||||||
},
|
},
|
||||||
|
|
|
@ -150,7 +150,11 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
|
||||||
label="accountLinkingOnly"
|
label="accountLinkingOnly"
|
||||||
fieldType="boolean"
|
fieldType="boolean"
|
||||||
/>
|
/>
|
||||||
<SwitchField field="config.hideOnLoginPage" label="hideOnLoginPage" />
|
<SwitchField
|
||||||
|
field="hideOnLogin"
|
||||||
|
label="hideOnLoginPage"
|
||||||
|
fieldType="boolean"
|
||||||
|
/>
|
||||||
|
|
||||||
{(!isSAML || isOIDC) && (
|
{(!isSAML || isOIDC) && (
|
||||||
<FormGroupField label="filteredByClaim">
|
<FormGroupField label="filteredByClaim">
|
||||||
|
|
|
@ -11,8 +11,10 @@ export default interface IdentityProviderRepresentation {
|
||||||
firstBrokerLoginFlowAlias?: string;
|
firstBrokerLoginFlowAlias?: string;
|
||||||
internalId?: string;
|
internalId?: string;
|
||||||
linkOnly?: boolean;
|
linkOnly?: boolean;
|
||||||
|
hideOnLogin?: boolean;
|
||||||
postBrokerLoginFlowAlias?: string;
|
postBrokerLoginFlowAlias?: string;
|
||||||
providerId?: string;
|
providerId?: string;
|
||||||
storeToken?: boolean;
|
storeToken?: boolean;
|
||||||
trustEmail?: boolean;
|
trustEmail?: boolean;
|
||||||
|
organizationId?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,7 +143,7 @@ public class InfinispanIDPProvider implements IDPProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<IdentityProviderModel> getAllStream(Map<String, String> attrs, Integer first, Integer max) {
|
public Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Integer first, Integer max) {
|
||||||
return idpDelegate.getAllStream(attrs, first, max);
|
return idpDelegate.getAllStream(attrs, first, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,10 +58,10 @@ public class InfinispanOrganizationProviderFactory implements OrganizationProvid
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerOrganizationInvalidation(KeycloakSession session, IdentityProviderModel idp) {
|
private void registerOrganizationInvalidation(KeycloakSession session, IdentityProviderModel idp) {
|
||||||
if (idp.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE) != null) {
|
if (idp.getOrganizationId() != null) {
|
||||||
InfinispanOrganizationProvider orgProvider = (InfinispanOrganizationProvider) session.getProvider(OrganizationProvider.class, getId());
|
InfinispanOrganizationProvider orgProvider = (InfinispanOrganizationProvider) session.getProvider(OrganizationProvider.class, getId());
|
||||||
if (orgProvider != null) {
|
if (orgProvider != null) {
|
||||||
OrganizationModel organization = orgProvider.getById(idp.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE));
|
OrganizationModel organization = orgProvider.getById(idp.getOrganizationId());
|
||||||
orgProvider.registerOrganizationInvalidation(organization);
|
orgProvider.registerOrganizationInvalidation(organization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.connections.jpa.updater.liquibase.custom;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
|
||||||
|
import liquibase.exception.CustomChangeException;
|
||||||
|
import liquibase.statement.core.DeleteStatement;
|
||||||
|
import liquibase.statement.core.UpdateStatement;
|
||||||
|
import liquibase.structure.core.Table;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom SQL change to migrate the organization ID and the hide on login page config from the IDP config table to the
|
||||||
|
* IDP table.
|
||||||
|
*/
|
||||||
|
public class JpaUpdate26_0_0_IdentityProviderAttributesMigration extends CustomKeycloakTask {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void generateStatementsImpl() throws CustomChangeException {
|
||||||
|
|
||||||
|
// move the organization id from the config to the IDP.
|
||||||
|
try (PreparedStatement ps = connection.prepareStatement("SELECT c.IDENTITY_PROVIDER_ID, c.VALUE" +
|
||||||
|
" FROM " + getTableName("IDENTITY_PROVIDER_CONFIG") + " c WHERE c.NAME = '" + OrganizationModel.ORGANIZATION_ATTRIBUTE + "'");
|
||||||
|
ResultSet resultSet = ps.executeQuery()
|
||||||
|
) {
|
||||||
|
while (resultSet.next()) {
|
||||||
|
String id = resultSet.getString(1);
|
||||||
|
String value = resultSet.getString(2);
|
||||||
|
statements.add(new UpdateStatement(null, null, database.correctObjectName("IDENTITY_PROVIDER", Table.class))
|
||||||
|
.addNewColumnValue("ORGANIZATION_ID", value)
|
||||||
|
.setWhereClause("INTERNAL_ID=?")
|
||||||
|
.addWhereParameter(id));
|
||||||
|
}
|
||||||
|
statements.add(new DeleteStatement(null, null, database.correctObjectName("IDENTITY_PROVIDER_CONFIG", Table.class))
|
||||||
|
.setWhere("NAME=?")
|
||||||
|
.addWhereParameter(OrganizationModel.ORGANIZATION_ATTRIBUTE));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// move hide on login page from the config to the IDP.
|
||||||
|
try (PreparedStatement ps = connection.prepareStatement("SELECT c.IDENTITY_PROVIDER_ID, c.VALUE" +
|
||||||
|
" FROM " + getTableName("IDENTITY_PROVIDER_CONFIG") + " c WHERE c.NAME = '" + IdentityProviderModel.LEGACY_HIDE_ON_LOGIN_ATTR + "'");
|
||||||
|
ResultSet resultSet = ps.executeQuery()
|
||||||
|
) {
|
||||||
|
while (resultSet.next()) {
|
||||||
|
String id = resultSet.getString(1);
|
||||||
|
String value = resultSet.getString(2);
|
||||||
|
statements.add(new UpdateStatement(null, null, database.correctObjectName("IDENTITY_PROVIDER", Table.class))
|
||||||
|
.addNewColumnValue("HIDE_ON_LOGIN", Boolean.parseBoolean(value))
|
||||||
|
.setWhereClause("INTERNAL_ID=?")
|
||||||
|
.addWhereParameter(id));
|
||||||
|
}
|
||||||
|
statements.add(new DeleteStatement(null, null, database.correctObjectName("IDENTITY_PROVIDER_CONFIG", Table.class))
|
||||||
|
.setWhere("NAME=?")
|
||||||
|
.addWhereParameter(IdentityProviderModel.LEGACY_HIDE_ON_LOGIN_ATTR));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getTaskId() {
|
||||||
|
return "Migrate kc.org and hideOnLoginPage from the IDP config to the IDP itself";
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,6 @@ import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
import jakarta.persistence.criteria.MapJoin;
|
import jakarta.persistence.criteria.MapJoin;
|
||||||
import jakarta.persistence.criteria.Predicate;
|
import jakarta.persistence.criteria.Predicate;
|
||||||
import jakarta.persistence.criteria.Root;
|
import jakarta.persistence.criteria.Root;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.hibernate.Session;
|
import org.hibernate.Session;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
|
@ -51,6 +50,9 @@ import static org.keycloak.models.IdentityProviderModel.ALIAS;
|
||||||
import static org.keycloak.models.IdentityProviderModel.AUTHENTICATE_BY_DEFAULT;
|
import static org.keycloak.models.IdentityProviderModel.AUTHENTICATE_BY_DEFAULT;
|
||||||
import static org.keycloak.models.IdentityProviderModel.ENABLED;
|
import static org.keycloak.models.IdentityProviderModel.ENABLED;
|
||||||
import static org.keycloak.models.IdentityProviderModel.FIRST_BROKER_LOGIN_FLOW_ID;
|
import static org.keycloak.models.IdentityProviderModel.FIRST_BROKER_LOGIN_FLOW_ID;
|
||||||
|
import static org.keycloak.models.IdentityProviderModel.HIDE_ON_LOGIN;
|
||||||
|
import static org.keycloak.models.IdentityProviderModel.LINK_ONLY;
|
||||||
|
import static org.keycloak.models.IdentityProviderModel.ORGANIZATION_ID;
|
||||||
import static org.keycloak.models.IdentityProviderModel.POST_BROKER_LOGIN_FLOW_ID;
|
import static org.keycloak.models.IdentityProviderModel.POST_BROKER_LOGIN_FLOW_ID;
|
||||||
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
|
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
|
||||||
import static org.keycloak.utils.StreamsUtil.closing;
|
import static org.keycloak.utils.StreamsUtil.closing;
|
||||||
|
@ -92,8 +94,10 @@ public class JpaIDPProvider implements IDPProvider {
|
||||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||||
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||||
entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId());
|
entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId());
|
||||||
|
entity.setOrganizationId(identityProvider.getOrganizationId());
|
||||||
entity.setConfig(identityProvider.getConfig());
|
entity.setConfig(identityProvider.getConfig());
|
||||||
entity.setLinkOnly(identityProvider.isLinkOnly());
|
entity.setLinkOnly(identityProvider.isLinkOnly());
|
||||||
|
entity.setHideOnLogin(identityProvider.isHideOnLogin());
|
||||||
em.persist(entity);
|
em.persist(entity);
|
||||||
// flush so that constraint violations are flagged and converted into model exception now rather than at the end of the tx.
|
// flush so that constraint violations are flagged and converted into model exception now rather than at the end of the tx.
|
||||||
em.flush();
|
em.flush();
|
||||||
|
@ -113,10 +117,12 @@ public class JpaIDPProvider implements IDPProvider {
|
||||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||||
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||||
entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId());
|
entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId());
|
||||||
|
entity.setOrganizationId(identityProvider.getOrganizationId());
|
||||||
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
||||||
entity.setStoreToken(identityProvider.isStoreToken());
|
entity.setStoreToken(identityProvider.isStoreToken());
|
||||||
entity.setConfig(identityProvider.getConfig());
|
entity.setConfig(identityProvider.getConfig());
|
||||||
entity.setLinkOnly(identityProvider.isLinkOnly());
|
entity.setLinkOnly(identityProvider.isLinkOnly());
|
||||||
|
entity.setHideOnLogin(identityProvider.isHideOnLogin());
|
||||||
|
|
||||||
// flush so that constraint violations are flagged and converted into model exception now rather than at the end of the tx.
|
// flush so that constraint violations are flagged and converted into model exception now rather than at the end of the tx.
|
||||||
em.flush();
|
em.flush();
|
||||||
|
@ -218,7 +224,7 @@ public class JpaIDPProvider implements IDPProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<IdentityProviderModel> getAllStream(Map<String, String> attrs, Integer first, Integer max) {
|
public Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Integer first, Integer max) {
|
||||||
CriteriaBuilder builder = em.getCriteriaBuilder();
|
CriteriaBuilder builder = em.getCriteriaBuilder();
|
||||||
CriteriaQuery<IdentityProviderEntity> query = builder.createQuery(IdentityProviderEntity.class);
|
CriteriaQuery<IdentityProviderEntity> query = builder.createQuery(IdentityProviderEntity.class);
|
||||||
Root<IdentityProviderEntity> idp = query.from(IdentityProviderEntity.class);
|
Root<IdentityProviderEntity> idp = query.from(IdentityProviderEntity.class);
|
||||||
|
@ -227,19 +233,27 @@ public class JpaIDPProvider implements IDPProvider {
|
||||||
predicates.add(builder.equal(idp.get("realmId"), getRealm().getId()));
|
predicates.add(builder.equal(idp.get("realmId"), getRealm().getId()));
|
||||||
|
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
for (Map.Entry<String, String> entry : attrs.entrySet()) {
|
for (Map.Entry<String, Object> entry : attrs.entrySet()) {
|
||||||
String key = entry.getKey();
|
String key = entry.getKey();
|
||||||
String value = entry.getValue();
|
Object value = entry.getValue();
|
||||||
if (StringUtil.isBlank(key)) {
|
if (StringUtil.isBlank(key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
switch(key) {
|
switch(key) {
|
||||||
|
case AUTHENTICATE_BY_DEFAULT:
|
||||||
case ENABLED:
|
case ENABLED:
|
||||||
case AUTHENTICATE_BY_DEFAULT: {
|
case HIDE_ON_LOGIN:
|
||||||
predicates.add(builder.equal(idp.get(key), Boolean.valueOf(value)));
|
case LINK_ONLY: {
|
||||||
|
if (Boolean.parseBoolean(value.toString())) {
|
||||||
|
predicates.add(builder.isTrue(idp.get(key)));
|
||||||
|
} else {
|
||||||
|
predicates.add(builder.isFalse(idp.get(key)));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
} case FIRST_BROKER_LOGIN_FLOW_ID: {
|
}
|
||||||
if (StringUtils.isBlank(value)) {
|
case FIRST_BROKER_LOGIN_FLOW_ID:
|
||||||
|
case ORGANIZATION_ID: {
|
||||||
|
if (value == null || value.toString().isEmpty()) {
|
||||||
predicates.add(builder.isNull(idp.get(key)));
|
predicates.add(builder.isNull(idp.get(key)));
|
||||||
} else {
|
} else {
|
||||||
predicates.add(builder.equal(idp.get(key), value));
|
predicates.add(builder.equal(idp.get(key), value));
|
||||||
|
@ -377,10 +391,12 @@ public class JpaIDPProvider implements IDPProvider {
|
||||||
identityProviderModel.setConfig(config);
|
identityProviderModel.setConfig(config);
|
||||||
identityProviderModel.setEnabled(entity.isEnabled());
|
identityProviderModel.setEnabled(entity.isEnabled());
|
||||||
identityProviderModel.setLinkOnly(entity.isLinkOnly());
|
identityProviderModel.setLinkOnly(entity.isLinkOnly());
|
||||||
|
identityProviderModel.setHideOnLogin(entity.isHideOnLogin());
|
||||||
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
||||||
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
||||||
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
|
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
|
||||||
identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId());
|
identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId());
|
||||||
|
identityProviderModel.setOrganizationId(entity.getOrganizationId());
|
||||||
identityProviderModel.setStoreToken(entity.isStoreToken());
|
identityProviderModel.setStoreToken(entity.isStoreToken());
|
||||||
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,9 @@ public class IdentityProviderEntity {
|
||||||
@Column(name="LINK_ONLY")
|
@Column(name="LINK_ONLY")
|
||||||
private boolean linkOnly;
|
private boolean linkOnly;
|
||||||
|
|
||||||
|
@Column(name="HIDE_ON_LOGIN")
|
||||||
|
private boolean hideOnLogin;
|
||||||
|
|
||||||
@Column(name="ADD_TOKEN_ROLE")
|
@Column(name="ADD_TOKEN_ROLE")
|
||||||
protected boolean addReadTokenRoleOnCreate;
|
protected boolean addReadTokenRoleOnCreate;
|
||||||
|
|
||||||
|
@ -77,6 +80,9 @@ public class IdentityProviderEntity {
|
||||||
@Column(name="POST_BROKER_LOGIN_FLOW_ID")
|
@Column(name="POST_BROKER_LOGIN_FLOW_ID")
|
||||||
private String postBrokerLoginFlowId;
|
private String postBrokerLoginFlowId;
|
||||||
|
|
||||||
|
@Column(name="ORGANIZATION_ID")
|
||||||
|
private String organizationId;
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@MapKeyColumn(name="NAME")
|
@MapKeyColumn(name="NAME")
|
||||||
@Column(name="VALUE", columnDefinition = "TEXT")
|
@Column(name="VALUE", columnDefinition = "TEXT")
|
||||||
|
@ -163,6 +169,22 @@ public class IdentityProviderEntity {
|
||||||
this.postBrokerLoginFlowId = postBrokerLoginFlowId;
|
this.postBrokerLoginFlowId = postBrokerLoginFlowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOrganizationId() {
|
||||||
|
return this.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrganizationId(String organizationId) {
|
||||||
|
this.organizationId = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHideOnLogin() {
|
||||||
|
return this.hideOnLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHideOnLogin(boolean hideOnLogin) {
|
||||||
|
this.hideOnLogin = hideOnLogin;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, String> getConfig() {
|
public Map<String, String> getConfig() {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,4 +73,23 @@
|
||||||
</createIndex>
|
</createIndex>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="keycloak" id="26.0.0-idps-for-login">
|
||||||
|
<addColumn tableName="IDENTITY_PROVIDER">
|
||||||
|
<column name="ORGANIZATION_ID" type="VARCHAR(255)"/>
|
||||||
|
<column name="HIDE_ON_LOGIN" type="BOOLEAN" defaultValueBoolean="false"/>
|
||||||
|
</addColumn>
|
||||||
|
<createIndex indexName="IDX_IDP_REALM_ORG" tableName="IDENTITY_PROVIDER">
|
||||||
|
<column name="REALM_ID"/>
|
||||||
|
<column name="ORGANIZATION_ID"/>
|
||||||
|
</createIndex>
|
||||||
|
<createIndex indexName="IDX_IDP_FOR_LOGIN" tableName="IDENTITY_PROVIDER">
|
||||||
|
<column name="REALM_ID"/>
|
||||||
|
<column name="ENABLED"/>
|
||||||
|
<column name="LINK_ONLY"/>
|
||||||
|
<column name="HIDE_ON_LOGIN"/>
|
||||||
|
<column name="ORGANIZATION_ID"/>
|
||||||
|
</createIndex>
|
||||||
|
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate26_0_0_IdentityProviderAttributesMigration"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -819,6 +819,7 @@ public class ModelToRepresentation {
|
||||||
IdentityProviderRepresentation providerRep = toBriefRepresentation(realm, identityProviderModel);
|
IdentityProviderRepresentation providerRep = toBriefRepresentation(realm, identityProviderModel);
|
||||||
|
|
||||||
providerRep.setLinkOnly(identityProviderModel.isLinkOnly());
|
providerRep.setLinkOnly(identityProviderModel.isLinkOnly());
|
||||||
|
providerRep.setHideOnLogin(identityProviderModel.isHideOnLogin());
|
||||||
providerRep.setStoreToken(identityProviderModel.isStoreToken());
|
providerRep.setStoreToken(identityProviderModel.isStoreToken());
|
||||||
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
|
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
|
||||||
providerRep.setAuthenticateByDefault(identityProviderModel.isAuthenticateByDefault());
|
providerRep.setAuthenticateByDefault(identityProviderModel.isAuthenticateByDefault());
|
||||||
|
@ -849,8 +850,8 @@ public class ModelToRepresentation {
|
||||||
providerRep.setPostBrokerLoginFlowAlias(flow.getAlias());
|
providerRep.setPostBrokerLoginFlowAlias(flow.getAlias());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (export) {
|
if (!export) {
|
||||||
providerRep.getConfig().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
providerRep.setOrganizationId(identityProviderModel.getOrganizationId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return providerRep;
|
return providerRep;
|
||||||
|
|
|
@ -78,7 +78,6 @@ import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.IdentityProviderMapperModel;
|
import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakContext;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.OrganizationDomainModel;
|
import org.keycloak.models.OrganizationDomainModel;
|
||||||
|
@ -867,15 +866,20 @@ public class RepresentationToModel {
|
||||||
identityProviderModel.setProviderId(representation.getProviderId());
|
identityProviderModel.setProviderId(representation.getProviderId());
|
||||||
identityProviderModel.setEnabled(representation.isEnabled());
|
identityProviderModel.setEnabled(representation.isEnabled());
|
||||||
identityProviderModel.setLinkOnly(representation.isLinkOnly());
|
identityProviderModel.setLinkOnly(representation.isLinkOnly());
|
||||||
|
identityProviderModel.setHideOnLogin(representation.isHideOnLogin());
|
||||||
|
// check if the legacy hide on login attribute is present.
|
||||||
|
String hideOnLoginAttr = representation.getConfig().remove(IdentityProviderModel.LEGACY_HIDE_ON_LOGIN_ATTR);
|
||||||
|
if (hideOnLoginAttr != null) identityProviderModel.setHideOnLogin(Boolean.parseBoolean(hideOnLoginAttr));
|
||||||
identityProviderModel.setTrustEmail(representation.isTrustEmail());
|
identityProviderModel.setTrustEmail(representation.isTrustEmail());
|
||||||
identityProviderModel.setAuthenticateByDefault(representation.isAuthenticateByDefault());
|
identityProviderModel.setAuthenticateByDefault(representation.isAuthenticateByDefault());
|
||||||
identityProviderModel.setStoreToken(representation.isStoreToken());
|
identityProviderModel.setStoreToken(representation.isStoreToken());
|
||||||
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
|
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
|
||||||
updateOrganizationBroker(realm, representation, session);
|
updateOrganizationBroker(representation, session);
|
||||||
|
identityProviderModel.setOrganizationId(representation.getOrganizationId());
|
||||||
identityProviderModel.setConfig(removeEmptyString(representation.getConfig()));
|
identityProviderModel.setConfig(removeEmptyString(representation.getConfig()));
|
||||||
|
|
||||||
String flowAlias = representation.getFirstBrokerLoginFlowAlias();
|
String flowAlias = representation.getFirstBrokerLoginFlowAlias();
|
||||||
if (flowAlias == null || flowAlias.trim().length() == 0) {
|
if (flowAlias == null || flowAlias.trim().isEmpty()) {
|
||||||
identityProviderModel.setFirstBrokerLoginFlowId(null);
|
identityProviderModel.setFirstBrokerLoginFlowId(null);
|
||||||
} else {
|
} else {
|
||||||
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
|
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
|
||||||
|
@ -886,7 +890,7 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
flowAlias = representation.getPostBrokerLoginFlowAlias();
|
flowAlias = representation.getPostBrokerLoginFlowAlias();
|
||||||
if (flowAlias == null || flowAlias.trim().length() == 0) {
|
if (flowAlias == null || flowAlias.trim().isEmpty()) {
|
||||||
identityProviderModel.setPostBrokerLoginFlowId(null);
|
identityProviderModel.setPostBrokerLoginFlowId(null);
|
||||||
} else {
|
} else {
|
||||||
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
|
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
|
||||||
|
@ -1639,21 +1643,22 @@ public class RepresentationToModel {
|
||||||
return toModel(representation, authorization, client);
|
return toModel(representation, authorization, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void updateOrganizationBroker(RealmModel realm, IdentityProviderRepresentation representation, KeycloakSession session) {
|
private static void updateOrganizationBroker(IdentityProviderRepresentation representation, KeycloakSession session) {
|
||||||
if (!Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
if (!Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityProviderModel existing = Optional.ofNullable(session.identityProviders().getByAlias(representation.getAlias()))
|
IdentityProviderModel existing = Optional.ofNullable(session.identityProviders().getByAlias(representation.getAlias()))
|
||||||
.orElse(session.identityProviders().getById(representation.getInternalId()));
|
.orElse(session.identityProviders().getById(representation.getInternalId()));
|
||||||
String orgId = existing == null ? representation.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE) : existing.getOrganizationId();
|
String repOrgId = representation.getOrganizationId() != null ? representation.getOrganizationId() :
|
||||||
|
representation.getConfig().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||||
|
String orgId = existing != null ? existing.getOrganizationId() : repOrgId;
|
||||||
|
|
||||||
if (orgId != null) {
|
if (orgId != null) {
|
||||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||||
OrganizationModel org = provider.getById(orgId);
|
OrganizationModel org = provider.getById(orgId);
|
||||||
String newOrgId = representation.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
|
||||||
|
|
||||||
if (org == null || (newOrgId != null && provider.getById(newOrgId) == null)) {
|
if (org == null || (repOrgId != null && provider.getById(repOrgId) == null)) {
|
||||||
throw new IllegalArgumentException("Organization associated with broker does not exist");
|
throw new IllegalArgumentException("Organization associated with broker does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1666,7 +1671,7 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the link to an organization does not change
|
// make sure the link to an organization does not change
|
||||||
representation.getConfig().put(OrganizationModel.ORGANIZATION_ATTRIBUTE, orgId);
|
representation.setOrganizationId(orgId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.models;
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@ -118,7 +119,7 @@ public interface IDPProvider extends Provider {
|
||||||
* @param max the maximum number of results to be returned. Ignored if negative or {@code null}.
|
* @param max the maximum number of results to be returned. Ignored if negative or {@code null}.
|
||||||
* @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria.
|
* @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria.
|
||||||
*/
|
*/
|
||||||
Stream<IdentityProviderModel> getAllStream(Map<String, String> attrs, Integer first, Integer max);
|
Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Integer first, Integer max);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all identity providers associated with the organization with the provided id.
|
* Returns all identity providers associated with the organization with the provided id.
|
||||||
|
@ -129,7 +130,7 @@ public interface IDPProvider extends Provider {
|
||||||
* @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria.
|
* @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria.
|
||||||
*/
|
*/
|
||||||
default Stream<IdentityProviderModel> getByOrganization(String orgId, Integer first, Integer max) {
|
default Stream<IdentityProviderModel> getByOrganization(String orgId, Integer first, Integer max) {
|
||||||
return getAllStream(Map.of(OrganizationModel.ORGANIZATION_ATTRIBUTE, orgId), first, max);
|
return getAllStream(Map.of(IdentityProviderModel.ORGANIZATION_ID, orgId), first, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,6 +148,51 @@ public interface IDPProvider extends Provider {
|
||||||
*/
|
*/
|
||||||
Stream<String> getByFlow(String flowId, String search, Integer first, Integer max);
|
Stream<String> getByFlow(String flowId, String search, Integer first, Integer max);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all identity providers available for login, according to the specified mode. An IDP can be used for login
|
||||||
|
* if it is enabled, is not a link-only IDP, and is not configured to be hidden on login page.
|
||||||
|
* </p>
|
||||||
|
* The mode parameter may narrow the list of IDPs that are available. {@code FETCH_MODE.REALM_ONLY} fetches only realm-level
|
||||||
|
* IDPs (i.e. those not associated with any org). {@code FETCH_MODE.ORG_ONLY} will work together with the {@code organizationId}
|
||||||
|
* parameter. If the latter is set, only the IDPs associated with that org will be returned. Otherwise, the method returns
|
||||||
|
* the IDPs associated with any org. {@code FETCH_MODE.ALL} combines both approaches, returning both the realm-level
|
||||||
|
* IDPs with those associated with organizations (or a specific organization as per the {@code organizationId} param).
|
||||||
|
*
|
||||||
|
* @param mode the fetch mode to be used. Can be {@code REALM_ONLY}, {@code ORG_ONLY}, or {@code ALL}.
|
||||||
|
* @param organizationId an optional organization ID. If present and the mode is not {@code REALM_ONLY}, the param indicates
|
||||||
|
* that only IDPs associated with the specified organization are to be returned.
|
||||||
|
* @return a non-null stream of {@link IdentityProviderModel}s that are suitable for being displayed in the login pages.
|
||||||
|
*/
|
||||||
|
default Stream<IdentityProviderModel> getForLogin(FETCH_MODE mode, String organizationId) {
|
||||||
|
Stream<IdentityProviderModel> result = Stream.of();
|
||||||
|
if (mode == FETCH_MODE.REALM_ONLY || mode == FETCH_MODE.ALL) {
|
||||||
|
// fetch all realm-only IDPs - i.e. those not associated with orgs.
|
||||||
|
Map<String, Object> searchOptions = getBasicSearchOptionsForLogin();
|
||||||
|
searchOptions.put(IdentityProviderModel.ORGANIZATION_ID, null);
|
||||||
|
result = Stream.concat(result, getAllStream(searchOptions, null, null));
|
||||||
|
}
|
||||||
|
if (mode == FETCH_MODE.ORG_ONLY || mode == FETCH_MODE.ALL) {
|
||||||
|
// fetch IDPs associated with organizations.
|
||||||
|
Map<String, Object> searchOptions = getBasicSearchOptionsForLogin();
|
||||||
|
if (organizationId != null) {
|
||||||
|
// we want the IDPs associated with a specific org.
|
||||||
|
searchOptions.put(IdentityProviderModel.ORGANIZATION_ID, organizationId);
|
||||||
|
}
|
||||||
|
searchOptions.put(OrganizationModel.BROKER_PUBLIC, "true");
|
||||||
|
result = Stream.concat(result, getAllStream(searchOptions, null, null));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> getBasicSearchOptionsForLogin() {
|
||||||
|
Map<String, Object> searchOptions = new LinkedHashMap<>();
|
||||||
|
searchOptions.put(IdentityProviderModel.ENABLED, "true");
|
||||||
|
searchOptions.put(IdentityProviderModel.LINK_ONLY, "false");
|
||||||
|
searchOptions.put(IdentityProviderModel.HIDE_ON_LOGIN, "false");
|
||||||
|
return searchOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of IDPs in the realm.
|
* Returns the number of IDPs in the realm.
|
||||||
*
|
*
|
||||||
|
@ -163,4 +209,6 @@ public interface IDPProvider extends Provider {
|
||||||
default boolean isIdentityFederationEnabled() {
|
default boolean isIdentityFederationEnabled() {
|
||||||
return count() > 0;
|
return count() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FETCH_MODE {REALM_ONLY, ORG_ONLY, ALL}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,9 +41,12 @@ public class IdentityProviderModel implements Serializable {
|
||||||
public static final String ENABLED = "enabled";
|
public static final String ENABLED = "enabled";
|
||||||
public static final String FILTERED_BY_CLAIMS = "filteredByClaim";
|
public static final String FILTERED_BY_CLAIMS = "filteredByClaim";
|
||||||
public static final String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId";
|
public static final String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId";
|
||||||
public static final String HIDE_ON_LOGIN = "hideOnLoginPage";
|
public static final String HIDE_ON_LOGIN = "hideOnLogin";
|
||||||
|
public static final String LEGACY_HIDE_ON_LOGIN_ATTR = "hideOnLoginPage";
|
||||||
|
public static final String LINK_ONLY = "linkOnly";
|
||||||
public static final String LOGIN_HINT = "loginHint";
|
public static final String LOGIN_HINT = "loginHint";
|
||||||
public static final String METADATA_DESCRIPTOR_URL = "metadataDescriptorUrl";
|
public static final String METADATA_DESCRIPTOR_URL = "metadataDescriptorUrl";
|
||||||
|
public static final String ORGANIZATION_ID = "organizationId";
|
||||||
public static final String PASS_MAX_AGE = "passMaxAge";
|
public static final String PASS_MAX_AGE = "passMaxAge";
|
||||||
public static final String POST_BROKER_LOGIN_FLOW_ID = "postBrokerLoginFlowId";
|
public static final String POST_BROKER_LOGIN_FLOW_ID = "postBrokerLoginFlowId";
|
||||||
public static final String SYNC_MODE = "syncMode";
|
public static final String SYNC_MODE = "syncMode";
|
||||||
|
@ -80,11 +83,13 @@ public class IdentityProviderModel implements Serializable {
|
||||||
|
|
||||||
private String postBrokerLoginFlowId;
|
private String postBrokerLoginFlowId;
|
||||||
|
|
||||||
|
private String organizationId;
|
||||||
|
|
||||||
private String displayName;
|
private String displayName;
|
||||||
|
|
||||||
private String displayIconClasses;
|
private String displayIconClasses;
|
||||||
|
|
||||||
private IdentityProviderSyncMode syncMode;
|
private boolean hideOnLogin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
|
* <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
|
||||||
|
@ -110,7 +115,9 @@ public class IdentityProviderModel implements Serializable {
|
||||||
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
|
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
|
||||||
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
|
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
|
||||||
this.postBrokerLoginFlowId = model.getPostBrokerLoginFlowId();
|
this.postBrokerLoginFlowId = model.getPostBrokerLoginFlowId();
|
||||||
|
this.organizationId = model.getOrganizationId();
|
||||||
this.displayIconClasses = model.getDisplayIconClasses();
|
this.displayIconClasses = model.getDisplayIconClasses();
|
||||||
|
this.hideOnLogin = model.isHideOnLogin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,11 +232,11 @@ public class IdentityProviderModel implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getOrganizationId() {
|
public String getOrganizationId() {
|
||||||
return getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
return this.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOrganizationId(String organizationId) {
|
public void setOrganizationId(String organizationId) {
|
||||||
getConfig().put(OrganizationModel.ORGANIZATION_ATTRIBUTE, organizationId);
|
this.organizationId = organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -268,11 +275,11 @@ public class IdentityProviderModel implements Serializable {
|
||||||
|
|
||||||
|
|
||||||
public boolean isHideOnLogin() {
|
public boolean isHideOnLogin() {
|
||||||
return Boolean.valueOf(getConfig().get(HIDE_ON_LOGIN));
|
return this.hideOnLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHideOnLogin(boolean hideOnLogin) {
|
public void setHideOnLogin(boolean hideOnLogin) {
|
||||||
getConfig().put(HIDE_ON_LOGIN, String.valueOf(hideOnLogin));
|
this.hideOnLogin = hideOnLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,16 +17,20 @@
|
||||||
|
|
||||||
package org.keycloak.authentication.authenticators.browser;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import java.util.stream.Stream;
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.forms.login.freemarker.LoginFormsUtil;
|
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.MultivaluedMap;
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
public final class UsernameForm extends UsernamePasswordForm {
|
public final class UsernameForm extends UsernamePasswordForm {
|
||||||
|
|
||||||
|
@ -34,9 +38,7 @@ public final class UsernameForm extends UsernamePasswordForm {
|
||||||
public void authenticate(AuthenticationFlowContext context) {
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
if (context.getUser() != null) {
|
if (context.getUser() != null) {
|
||||||
// We can skip the form when user is re-authenticating. Unless current user has some IDP set, so he can re-authenticate with that IDP
|
// We can skip the form when user is re-authenticating. Unless current user has some IDP set, so he can re-authenticate with that IDP
|
||||||
Stream<IdentityProviderModel> identityProviders = LoginFormsUtil
|
if (!this.contextUserHasFederatedIDPs(context)) {
|
||||||
.filterIdentityProviders(context.getRealm().getIdentityProvidersStream(), context.getSession(), context);
|
|
||||||
if (identityProviders.findAny().isEmpty()) {
|
|
||||||
context.success();
|
context.success();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -69,4 +71,28 @@ public final class UsernameForm extends UsernamePasswordForm {
|
||||||
return Messages.INVALID_USERNAME_OR_EMAIL;
|
return Messages.INVALID_USERNAME_OR_EMAIL;
|
||||||
return Messages.INVALID_USERNAME;
|
return Messages.INVALID_USERNAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the context user, if it has been set, is currently linked to any IDPs they could use to authenticate.
|
||||||
|
* If the auth session has an existing IDP in the brokered context, it is filtered out.
|
||||||
|
*
|
||||||
|
* @param context a reference to the {@link AuthenticationFlowContext}
|
||||||
|
* @return {@code true} if the context user has federated IDPs that can be used for authentication; {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
private boolean contextUserHasFederatedIDPs(AuthenticationFlowContext context) {
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
UserModel user = context.getUser();
|
||||||
|
if (user == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||||
|
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||||
|
final IdentityProviderModel existingIdp = (serializedCtx == null) ? null : serializedCtx.deserialize(session, authSession).getIdpConfig();
|
||||||
|
|
||||||
|
return session.users().getFederatedIdentitiesStream(session.getContext().getRealm(), user)
|
||||||
|
.map(fedIdentity -> session.identityProviders().getByAlias(fedIdentity.getIdentityProvider()))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.anyMatch(idpModel -> existingIdp == null || !Objects.equals(existingIdp.getAlias(), idpModel.getAlias()));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,7 +160,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||||
for (AttributeType attribute : entityType.getExtensions().getEntityAttributes().getAttribute()) {
|
for (AttributeType attribute : entityType.getExtensions().getEntityAttributes().getAttribute()) {
|
||||||
if (MACEDIR_ENTITY_CATEGORY.equals(attribute.getName())
|
if (MACEDIR_ENTITY_CATEGORY.equals(attribute.getName())
|
||||||
&& attribute.getAttributeValue().contains(REFEDS_HIDE_FROM_DISCOVERY)) {
|
&& attribute.getAttributeValue().contains(REFEDS_HIDE_FROM_DISCOVERY)) {
|
||||||
samlIdentityProviderConfig.setHideOnLogin(true);
|
samlIdentityProviderConfig.getConfig().put(IdentityProviderModel.LEGACY_HIDE_ON_LOGIN_ATTR, Boolean.TRUE.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,6 @@ import org.keycloak.forms.login.freemarker.model.VerifyProfileBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.X509ConfirmBean;
|
import org.keycloak.forms.login.freemarker.model.X509ConfirmBean;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -101,7 +100,6 @@ import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
|
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
|
||||||
import static org.keycloak.organization.utils.Organizations.resolveOrganization;
|
import static org.keycloak.organization.utils.Organizations.resolveOrganization;
|
||||||
|
@ -480,12 +478,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
if (realm != null) {
|
if (realm != null) {
|
||||||
attributes.put("realm", new RealmBean(realm));
|
attributes.put("realm", new RealmBean(realm));
|
||||||
|
|
||||||
Stream<IdentityProviderModel> identityProviders = LoginFormsUtil
|
IdentityProviderBean idpBean = new IdentityProviderBean(session, realm, baseUriWithCodeAndClientId, context);
|
||||||
.filterIdentityProvidersForTheme(realm.getIdentityProvidersStream(), session, context);
|
|
||||||
IdentityProviderBean idpBean = new IdentityProviderBean(realm, session, identityProviders, baseUriWithCodeAndClientId);
|
|
||||||
|
|
||||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
if (Profile.isFeatureEnabled(Feature.ORGANIZATION) && realm.isOrganizationsEnabled()) {
|
||||||
idpBean = new OrganizationAwareIdentityProviderBean(idpBean, session);
|
idpBean = new OrganizationAwareIdentityProviderBean(idpBean);
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes.put("social", idpBean);
|
attributes.put("social", idpBean);
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.keycloak.forms.login.freemarker;
|
|
||||||
|
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
|
||||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
|
||||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.common.Profile.Feature;
|
|
||||||
import org.keycloak.models.FederatedIdentityModel;
|
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.services.resources.LoginActionsService;
|
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Various util methods, so the logic is not hardcoded in freemarker beans
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
|
||||||
*/
|
|
||||||
public class LoginFormsUtil {
|
|
||||||
|
|
||||||
public static Stream<IdentityProviderModel> filterIdentityProvidersForTheme(Stream<IdentityProviderModel> providers, KeycloakSession session, AuthenticationFlowContext context) {
|
|
||||||
if (context != null) {
|
|
||||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
|
||||||
String currentFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
|
|
||||||
UserModel currentUser = context.getUser();
|
|
||||||
// Fixing #14173
|
|
||||||
// If the current user is not null, then it's a re-auth, and we should filter the possible options with the pre-14173 logic
|
|
||||||
// If the current user is null, then it's one of the following cases:
|
|
||||||
// - either connecting a new IdP to the user's account.
|
|
||||||
// - in this case the currentUser is null AND the current flow is the FIRST_BROKER_LOGIN_PATH
|
|
||||||
// - so we should filter out the one they just used for login, as they need to re-auth themselves with an already linked IdP account
|
|
||||||
// - or we're on the Login page
|
|
||||||
// - in this case the current user is null AND the current flow is NOT the FIRST_BROKER_LOGIN_PATH
|
|
||||||
// - so we should show all the possible IdPs to the user trying to log in (this is the bug in #14173)
|
|
||||||
// - so we're skipping this branch, and returning everything at the end of the method
|
|
||||||
if (currentUser != null || Objects.equals(LoginActionsService.FIRST_BROKER_LOGIN_PATH, currentFlowPath)) {
|
|
||||||
return filterIdentityProviders(providers, session, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filterOrganizationBrokers(providers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Stream<IdentityProviderModel> filterIdentityProviders(Stream<IdentityProviderModel> providers, KeycloakSession session, AuthenticationFlowContext context) {
|
|
||||||
|
|
||||||
if (context != null) {
|
|
||||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
|
||||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
|
||||||
|
|
||||||
final IdentityProviderModel existingIdp = (serializedCtx == null) ? null : serializedCtx.deserialize(session, authSession).getIdpConfig();
|
|
||||||
|
|
||||||
final Set<String> federatedIdentities;
|
|
||||||
UserModel user = context.getUser();
|
|
||||||
if (user != null) {
|
|
||||||
federatedIdentities = session.users().getFederatedIdentitiesStream(session.getContext().getRealm(), user)
|
|
||||||
.map(FederatedIdentityModel::getIdentityProvider)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
} else {
|
|
||||||
federatedIdentities = Set.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
return filterOrganizationBrokers(providers)
|
|
||||||
.filter(p -> { // Filter current IDP during first-broker-login flow. Re-authentication with the "linked" broker should not be possible
|
|
||||||
if (existingIdp == null) return true;
|
|
||||||
return !Objects.equals(p.getAlias(), existingIdp.getAlias());
|
|
||||||
})
|
|
||||||
.filter(idp -> {
|
|
||||||
// user not established in authentication session, he can choose to authenticate using any broker
|
|
||||||
if (user == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (federatedIdentities.isEmpty()) {
|
|
||||||
// user established but not linked to any broker, he can choose to authenticate using any organization broker
|
|
||||||
return idp.getOrganizationId() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// user established, we show just providers already linked to this user
|
|
||||||
return federatedIdentities.contains(idp.getAlias());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return filterOrganizationBrokers(providers);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Stream<IdentityProviderModel> filterOrganizationBrokers(Stream<IdentityProviderModel> providers) {
|
|
||||||
if (!Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
|
||||||
providers = providers.filter(identityProviderModel -> identityProviderModel.getOrganizationId() == null);
|
|
||||||
}
|
|
||||||
return providers;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,22 +16,34 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.forms.login.freemarker.model;
|
package org.keycloak.forms.login.freemarker.model;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
|
import org.keycloak.models.IDPProvider;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.OrderedModel;
|
import org.keycloak.models.OrderedModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
|
import org.keycloak.services.resources.LoginActionsService;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.theme.Theme;
|
import org.keycloak.theme.Theme;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.stream.Stream;
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -42,44 +54,63 @@ public class IdentityProviderBean {
|
||||||
public static OrderedModel.OrderedModelComparator<IdentityProvider> IDP_COMPARATOR_INSTANCE = new OrderedModel.OrderedModelComparator<>();
|
public static OrderedModel.OrderedModelComparator<IdentityProvider> IDP_COMPARATOR_INSTANCE = new OrderedModel.OrderedModelComparator<>();
|
||||||
private static final String ICON_THEME_PREFIX = "kcLogoIdP-";
|
private static final String ICON_THEME_PREFIX = "kcLogoIdP-";
|
||||||
|
|
||||||
private boolean displaySocial;
|
protected AuthenticationFlowContext context;
|
||||||
private List<IdentityProvider> providers;
|
protected List<IdentityProvider> providers;
|
||||||
private RealmModel realm;
|
protected KeycloakSession session;
|
||||||
private final KeycloakSession session;
|
protected RealmModel realm;
|
||||||
|
protected URI baseURI;
|
||||||
|
|
||||||
public IdentityProviderBean() {
|
public IdentityProviderBean(KeycloakSession session, RealmModel realm, URI baseURI, AuthenticationFlowContext context) {
|
||||||
this.session = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IdentityProviderBean(RealmModel realm, KeycloakSession session, Stream<IdentityProviderModel> identityProviders, URI baseURI) {
|
|
||||||
this.realm = realm;
|
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
this.realm = realm;
|
||||||
List<IdentityProvider> orderedList = new ArrayList<>();
|
this.baseURI = baseURI;
|
||||||
|
this.context = context;
|
||||||
identityProviders.forEach(identityProvider -> {
|
|
||||||
if (identityProvider.isEnabled() && !identityProvider.isLinkOnly()) {
|
|
||||||
addIdentityProvider(orderedList, realm, baseURI, identityProvider);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!orderedList.isEmpty()) {
|
|
||||||
orderedList.sort(IDP_COMPARATOR_INSTANCE);
|
|
||||||
providers = orderedList;
|
|
||||||
displaySocial = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addIdentityProvider(List<IdentityProvider> orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) {
|
public List<IdentityProvider> getProviders() {
|
||||||
|
if (this.providers == null) {
|
||||||
|
String existingIDP = this.getExistingIDP(session, context);
|
||||||
|
Set<String> federatedIdentities = this.getFederatedIdentities(session, realm, context);
|
||||||
|
if (federatedIdentities != null) {
|
||||||
|
this.providers = getFederatedIdentityProviders(federatedIdentities, existingIDP);
|
||||||
|
} else {
|
||||||
|
this.providers = searchForIdentityProviders(existingIDP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeycloakSession getSession() {
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmModel getRealm() {
|
||||||
|
return this.realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI getBaseURI() {
|
||||||
|
return this.baseURI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthenticationFlowContext getFlowContext() {
|
||||||
|
return this.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link IdentityProvider} instance from the specified {@link IdentityProviderModel}.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param baseURI the base URI.
|
||||||
|
* @param identityProvider the {@link IdentityProviderModel} from which the freemarker {@link IdentityProvider} is
|
||||||
|
* to be built.
|
||||||
|
* @return the constructed {@link IdentityProvider}.
|
||||||
|
*/
|
||||||
|
protected IdentityProvider createIdentityProvider(RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) {
|
||||||
String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString();
|
String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString();
|
||||||
String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProvider);
|
String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProvider);
|
||||||
Map<String, String> config = identityProvider.getConfig();
|
return new IdentityProvider(identityProvider.getAlias(),
|
||||||
boolean hideOnLoginPage = config != null && Boolean.parseBoolean(config.get("hideOnLoginPage"));
|
|
||||||
if (!hideOnLoginPage) {
|
|
||||||
orderedSet.add(new IdentityProvider(identityProvider.getAlias(),
|
|
||||||
displayName, identityProvider.getProviderId(), loginUrl,
|
displayName, identityProvider.getProviderId(), loginUrl,
|
||||||
config != null ? config.get("guiOrder") : null, getLoginIconClasses(identityProvider)));
|
identityProvider.getConfig().get("guiOrder"), getLoginIconClasses(identityProvider));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get icon classes defined in properties of current theme with key 'kcLogoIdP-{alias}'
|
// Get icon classes defined in properties of current theme with key 'kcLogoIdP-{alias}'
|
||||||
|
@ -97,12 +128,121 @@ public class IdentityProviderBean {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<IdentityProvider> getProviders() {
|
private String getLogoIconClass(IdentityProviderModel identityProvider, Properties themeProperties) throws IOException {
|
||||||
return providers;
|
String iconClass = themeProperties.getProperty(ICON_THEME_PREFIX + identityProvider.getAlias());
|
||||||
|
|
||||||
|
if (iconClass == null) {
|
||||||
|
return themeProperties.getProperty(ICON_THEME_PREFIX + identityProvider.getProviderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDisplayInfo() {
|
return iconClass;
|
||||||
return realm.isRegistrationAllowed() || displaySocial;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an IDP is being connected to the user's account. In this case the currentUser is {@code null} and the current flow
|
||||||
|
* is the {@code FIRST_BROKER_LOGIN_PATH}, so we should retrieve the IDP they used for login and filter it out of the list
|
||||||
|
* of IDPs that are available for login. (GHI #14173).
|
||||||
|
*
|
||||||
|
* @param session a reference to the {@link KeycloakSession}.
|
||||||
|
* @param context a reference to the {@link AuthenticationFlowContext}.
|
||||||
|
* @return the alias of the IDP used for login before linking a new IDP to the user's account (if any).
|
||||||
|
*/
|
||||||
|
protected String getExistingIDP(KeycloakSession session, AuthenticationFlowContext context) {
|
||||||
|
|
||||||
|
String existingIDPAlias = null;
|
||||||
|
if (context != null) {
|
||||||
|
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||||
|
String currentFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
|
||||||
|
UserModel currentUser = context.getUser();
|
||||||
|
|
||||||
|
if (currentUser == null && Objects.equals(LoginActionsService.FIRST_BROKER_LOGIN_PATH, currentFlowPath)) {
|
||||||
|
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||||
|
final IdentityProviderModel existingIdp = (serializedCtx == null) ? null : serializedCtx.deserialize(session, authSession).getIdpConfig();
|
||||||
|
if (existingIdp != null) {
|
||||||
|
existingIDPAlias = existingIdp.getAlias();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return existingIDPAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of IDPs associated with the user's federated identities, if any. In case these IDPs exist, the login
|
||||||
|
* page should show only the IDPs already linked to the user. Returning {@code null} indicates that all public enabled IDPs
|
||||||
|
* should be available.
|
||||||
|
* </p>
|
||||||
|
* Returning an empty set essentially narrows the list of available IDPs to zero, so no IDPs will be shown for login.
|
||||||
|
*
|
||||||
|
* @param session a reference to the {@link KeycloakSession}.
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param context a reference to the {@link AuthenticationFlowContext}.
|
||||||
|
* @return a {@link Set} containing the aliases of the IDPs that should be available for login. An empty set indicates
|
||||||
|
* that no IDPs should be available.
|
||||||
|
*/
|
||||||
|
protected Set<String> getFederatedIdentities(KeycloakSession session, RealmModel realm, AuthenticationFlowContext context) {
|
||||||
|
Set<String> result = null;
|
||||||
|
if (context != null) {
|
||||||
|
UserModel currentUser = context.getUser();
|
||||||
|
if (currentUser != null) {
|
||||||
|
Set<String> federatedIdentities = session.users().getFederatedIdentitiesStream(session.getContext().getRealm(), currentUser)
|
||||||
|
.map(FederatedIdentityModel::getIdentityProvider)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (!federatedIdentities.isEmpty() || organizationsDisabled(realm))
|
||||||
|
// if orgs are enabled, we don't want to return an empty set - we want the organization IDPs to be shown if those are available.
|
||||||
|
result = new HashSet<>(federatedIdentities);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns a list of {@link IdentityProvider} instances from the specified set of federated IDPs. The IDPs
|
||||||
|
* must be enabled, not link-only, and not set to be hidden on login page. If any IDP has an alias that matches the
|
||||||
|
* {@code existingIDP} parameter, it must be filtered out.
|
||||||
|
*
|
||||||
|
* @param federatedProviders a {@link Set} containing the aliases of the federated IDPs that should be considered for login.
|
||||||
|
* @param existingIDP the alias of the IDP that must be filtered out from the result (used when linking a new IDP to a user's account).
|
||||||
|
* @return a {@link List} containing the constructed {@link IdentityProvider}s.
|
||||||
|
*/
|
||||||
|
protected List<IdentityProvider> getFederatedIdentityProviders(Set<String> federatedProviders, String existingIDP) {
|
||||||
|
return federatedProviders.stream()
|
||||||
|
.filter(alias -> !Objects.equals(existingIDP, alias))
|
||||||
|
.map(alias -> session.identityProviders().getByAlias(alias))
|
||||||
|
.filter(federatedProviderPredicate())
|
||||||
|
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
|
||||||
|
.sorted(IDP_COMPARATOR_INSTANCE).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a predicate that can filter out IDPs associated with the current user's federated identities before those
|
||||||
|
* are converted into {@link IdentityProvider}s. Subclasses may use this as a way to further refine the IDPs that are
|
||||||
|
* to be returned.
|
||||||
|
*
|
||||||
|
* @return the custom {@link Predicate} used as a last filter before conversion into {@link IdentityProvider}
|
||||||
|
*/
|
||||||
|
protected Predicate<IdentityProviderModel> federatedProviderPredicate() {
|
||||||
|
return idp -> Objects.nonNull(idp) && idp.isEnabled() && !idp.isLinkOnly() && !idp.isHideOnLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns a list of {@link IdentityProvider} instances that will be available for login. This method goes
|
||||||
|
* to the {@link IDPProvider} to fetch the IDPs that can be used for login (enabled, not link-only and not set to be
|
||||||
|
* hidden on login page).
|
||||||
|
*
|
||||||
|
* @param existingIDP the alias of the IDP that must be filtered out from the result (used when linking a new IDP to a user's account).
|
||||||
|
* @return a {@link List} containing the constructed {@link IdentityProvider}s.
|
||||||
|
*/
|
||||||
|
protected List<IdentityProvider> searchForIdentityProviders(String existingIDP) {
|
||||||
|
return session.identityProviders().getForLogin(IDPProvider.FETCH_MODE.REALM_ONLY, null)
|
||||||
|
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
|
||||||
|
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
|
||||||
|
.sorted(IDP_COMPARATOR_INSTANCE).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean organizationsDisabled(RealmModel realm) {
|
||||||
|
return !Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION) || !realm.isOrganizationsEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class IdentityProvider implements OrderedModel {
|
public static class IdentityProvider implements OrderedModel {
|
||||||
|
@ -153,13 +293,5 @@ public class IdentityProviderBean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getLogoIconClass(IdentityProviderModel identityProvider, Properties themeProperties) throws IOException {
|
|
||||||
String iconClass = themeProperties.getProperty(ICON_THEME_PREFIX + identityProvider.getAlias());
|
|
||||||
|
|
||||||
if (iconClass == null) {
|
|
||||||
return themeProperties.getProperty(ICON_THEME_PREFIX + identityProvider.getProviderId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return iconClass;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
.setAttributeMapper(attributes -> {
|
.setAttributeMapper(attributes -> {
|
||||||
if (hasPublicBrokers(organization)) {
|
if (hasPublicBrokers(organization)) {
|
||||||
attributes.computeIfPresent("social",
|
attributes.computeIfPresent("social",
|
||||||
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true)
|
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, true)
|
||||||
);
|
);
|
||||||
// do not show the self-registration link if there are public brokers available from the organization to force the user to register using a broker
|
// do not show the self-registration link if there are public brokers available from the organization to force the user to register using a broker
|
||||||
attributes.computeIfPresent("realm",
|
attributes.computeIfPresent("realm",
|
||||||
|
@ -188,7 +188,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
attributes.computeIfPresent("social",
|
attributes.computeIfPresent("social",
|
||||||
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, false, true)
|
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, false, true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
LoginFormsProvider form = context.form()
|
LoginFormsProvider form = context.form()
|
||||||
.setAttributeMapper(attributes -> {
|
.setAttributeMapper(attributes -> {
|
||||||
attributes.computeIfPresent("social",
|
attributes.computeIfPresent("social",
|
||||||
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, false, true)
|
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, false, true)
|
||||||
);
|
);
|
||||||
attributes.computeIfPresent("auth",
|
attributes.computeIfPresent("auth",
|
||||||
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
|
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
|
||||||
|
|
|
@ -18,73 +18,88 @@
|
||||||
package org.keycloak.organization.forms.login.freemarker.model;
|
package org.keycloak.organization.forms.login.freemarker.model;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Objects;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
||||||
|
import org.keycloak.models.IDPProvider;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.organization.utils.Organizations;
|
import org.keycloak.organization.utils.Organizations;
|
||||||
|
|
||||||
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
|
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final OrganizationModel organization;
|
||||||
private final List<IdentityProvider> providers;
|
private final boolean onlyRealmBrokers;
|
||||||
|
private final boolean onlyOrganizationBrokers;
|
||||||
|
|
||||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers) {
|
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate) {
|
||||||
this(delegate, session, onlyOrganizationBrokers, false);
|
this(delegate, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers, boolean onlyRealmBrokers) {
|
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, boolean onlyOrganizationBrokers) {
|
||||||
this.session = session;
|
this(delegate, onlyOrganizationBrokers, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, boolean onlyOrganizationBrokers, boolean onlyRealmBrokers) {
|
||||||
|
super(delegate.getSession(), delegate.getRealm(), delegate.getBaseURI(), delegate.getFlowContext());
|
||||||
|
this.organization = Organizations.resolveOrganization(super.session);
|
||||||
|
this.onlyRealmBrokers = onlyRealmBrokers;
|
||||||
|
this.onlyOrganizationBrokers = onlyOrganizationBrokers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<IdentityProvider> searchForIdentityProviders(String existingIDP) {
|
||||||
if (onlyRealmBrokers) {
|
if (onlyRealmBrokers) {
|
||||||
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
// we only want the realm-level IDPs - i.e. those not associated with any orgs.
|
||||||
.filter(Predicate.not(this::isPublicOrganizationBroker))
|
return session.identityProviders().getForLogin(IDPProvider.FETCH_MODE.REALM_ONLY, null)
|
||||||
|
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
|
||||||
|
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
|
||||||
|
.sorted(IDP_COMPARATOR_INSTANCE).toList();
|
||||||
|
}
|
||||||
|
if (onlyOrganizationBrokers) {
|
||||||
|
// we already have the organization, just fetch the organization's public enabled IDPs.
|
||||||
|
if (this.organization != null) {
|
||||||
|
return organization.getIdentityProviders()
|
||||||
|
.filter(idp -> idp.isEnabled() && !idp.isLinkOnly() && !idp.isHideOnLogin()
|
||||||
|
&& Boolean.parseBoolean(idp.getConfig().get(OrganizationModel.BROKER_PUBLIC)))
|
||||||
|
.map(idp -> createIdentityProvider(super.realm, super.baseURI, idp))
|
||||||
.toList();
|
.toList();
|
||||||
|
}
|
||||||
|
// we don't have a specific organization - fetch public enabled IDPs linked to any org.
|
||||||
|
return session.identityProviders().getForLogin(IDPProvider.FETCH_MODE.ORG_ONLY, null)
|
||||||
|
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
|
||||||
|
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
|
||||||
|
.sorted(IDP_COMPARATOR_INSTANCE).toList();
|
||||||
|
}
|
||||||
|
return session.identityProviders().getForLogin(IDPProvider.FETCH_MODE.ALL, this.organization != null ? this.organization.getId() : null)
|
||||||
|
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
|
||||||
|
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
|
||||||
|
.sorted(IDP_COMPARATOR_INSTANCE).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Predicate<IdentityProviderModel> federatedProviderPredicate() {
|
||||||
|
// use the predicate from the superclass combined with the organization filter.
|
||||||
|
return super.federatedProviderPredicate().and(idp -> {
|
||||||
|
if (onlyRealmBrokers) {
|
||||||
|
return idp.getOrganizationId() == null;
|
||||||
} else if (onlyOrganizationBrokers) {
|
} else if (onlyOrganizationBrokers) {
|
||||||
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
return isPublicOrganizationBroker(idp);
|
||||||
.filter(this::isPublicOrganizationBroker)
|
|
||||||
.toList();
|
|
||||||
} else {
|
} else {
|
||||||
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
return idp.getOrganizationId() == null || isPublicOrganizationBroker(idp);
|
||||||
.filter(p -> isRealmBroker(p) || isPublicOrganizationBroker(p))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session) {
|
|
||||||
this(delegate, session, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean isPublicOrganizationBroker(IdentityProviderModel idp) {
|
||||||
public List<IdentityProvider> getProviders() {
|
|
||||||
return providers;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
if (idp.getOrganizationId() == null) {
|
||||||
public boolean isDisplayInfo() {
|
|
||||||
return !providers.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPublicOrganizationBroker(IdentityProvider idp) {
|
|
||||||
IdentityProviderModel model = session.identityProviders().getByAlias(idp.getAlias());
|
|
||||||
|
|
||||||
if (model.getOrganizationId() == null) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (organization != null && !Objects.equals(organization.getId(),idp.getOrganizationId())) {
|
||||||
OrganizationModel organization = Organizations.resolveOrganization(session);
|
|
||||||
|
|
||||||
if (organization != null && !organization.getId().equals(model.getOrganizationId())) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return Boolean.parseBoolean(idp.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString()));
|
||||||
return Boolean.parseBoolean(model.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isRealmBroker(IdentityProvider idp) {
|
|
||||||
IdentityProviderModel model = session.identityProviders().getByAlias(idp.getAlias());
|
|
||||||
return model.getOrganizationId() == null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,6 +197,8 @@ public class IdentityProviderResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
session.identityProviders().update(updated);
|
session.identityProviders().update(updated);
|
||||||
|
// update in case of legacy hide on login attr was used.
|
||||||
|
providerRep.setHideOnLogin(updated.isHideOnLogin());
|
||||||
|
|
||||||
if (oldProviderAlias != null && !oldProviderAlias.equals(newProviderAlias)) {
|
if (oldProviderAlias != null && !oldProviderAlias.equals(newProviderAlias)) {
|
||||||
|
|
||||||
|
|
|
@ -215,6 +215,7 @@ public class IdentityProvidersResource {
|
||||||
session.identityProviders().create(identityProvider);
|
session.identityProviders().create(identityProvider);
|
||||||
|
|
||||||
representation.setInternalId(identityProvider.getInternalId());
|
representation.setInternalId(identityProvider.getInternalId());
|
||||||
|
representation.setHideOnLogin(identityProvider.isHideOnLogin()); // update in case of legacy hide on login attr was used.
|
||||||
adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri(), identityProvider.getAlias())
|
adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri(), identityProvider.getAlias())
|
||||||
.representation(StripSecretsUtils.stripSecrets(session, representation)).success();
|
.representation(StripSecretsUtils.stripSecrets(session, representation)).success();
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ import java.nio.file.Paths;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -587,6 +588,8 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
||||||
|
|
||||||
String secret = idpRep.getConfig() != null ? idpRep.getConfig().get("clientSecret") : null;
|
String secret = idpRep.getConfig() != null ? idpRep.getConfig().get("clientSecret") : null;
|
||||||
idpRep = StripSecretsUtils.stripSecrets(null, idpRep);
|
idpRep = StripSecretsUtils.stripSecrets(null, idpRep);
|
||||||
|
// if legacy hide on login page attribute was used, the attr will be removed when converted to model
|
||||||
|
idpRep.setHideOnLogin(Boolean.parseBoolean(idpRep.getConfig().remove(IdentityProviderModel.LEGACY_HIDE_ON_LOGIN_ATTR)));
|
||||||
|
|
||||||
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.identityProviderPath(idpRep.getAlias()), idpRep, ResourceType.IDENTITY_PROVIDER);
|
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.identityProviderPath(idpRep.getAlias()), idpRep, ResourceType.IDENTITY_PROVIDER);
|
||||||
|
|
||||||
|
@ -1044,6 +1047,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
||||||
Assert.assertEquals("alias", expected.getAlias(), actual.getAlias());
|
Assert.assertEquals("alias", expected.getAlias(), actual.getAlias());
|
||||||
Assert.assertEquals("providerId", expected.getProviderId(), actual.getProviderId());
|
Assert.assertEquals("providerId", expected.getProviderId(), actual.getProviderId());
|
||||||
Assert.assertEquals("enabled", expected.isEnabled(), actual.isEnabled());
|
Assert.assertEquals("enabled", expected.isEnabled(), actual.isEnabled());
|
||||||
|
Assert.assertEquals("hideOnLogin", expected.isHideOnLogin(), actual.isHideOnLogin());
|
||||||
Assert.assertEquals("firstBrokerLoginFlowAlias", expected.getFirstBrokerLoginFlowAlias(), actual.getFirstBrokerLoginFlowAlias());
|
Assert.assertEquals("firstBrokerLoginFlowAlias", expected.getFirstBrokerLoginFlowAlias(), actual.getFirstBrokerLoginFlowAlias());
|
||||||
Assert.assertEquals("config", expected.getConfig(), actual.getConfig());
|
Assert.assertEquals("config", expected.getConfig(), actual.getConfig());
|
||||||
}
|
}
|
||||||
|
@ -1055,15 +1059,16 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
||||||
Assert.assertEquals("alias", "saml", idp.getAlias());
|
Assert.assertEquals("alias", "saml", idp.getAlias());
|
||||||
Assert.assertEquals("providerId", "saml", idp.getProviderId());
|
Assert.assertEquals("providerId", "saml", idp.getProviderId());
|
||||||
Assert.assertEquals("enabled",enabled, idp.isEnabled());
|
Assert.assertEquals("enabled",enabled, idp.isEnabled());
|
||||||
|
Assert.assertTrue("hideOnLogin", idp.isHideOnLogin());
|
||||||
Assert.assertNull("firstBrokerLoginFlowAlias", idp.getFirstBrokerLoginFlowAlias());
|
Assert.assertNull("firstBrokerLoginFlowAlias", idp.getFirstBrokerLoginFlowAlias());
|
||||||
assertSamlConfig(idp.getConfig(), postBindingResponse);
|
assertSamlConfig(idp.getConfig(), postBindingResponse, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSamlConfig(Map<String, String> config, boolean postBindingResponse) {
|
private void assertSamlConfig(Map<String, String> config, boolean postBindingResponse, boolean hasHideOnLoginPage) {
|
||||||
// import endpoint simply converts IDPSSODescriptor into key value pairs.
|
// import endpoint simply converts IDPSSODescriptor into key value pairs.
|
||||||
// check that saml-idp-metadata.xml was properly converted into key value pairs
|
// check that saml-idp-metadata.xml was properly converted into key value pairs
|
||||||
//System.out.println(config);
|
//System.out.println(config);
|
||||||
assertThat(config.keySet(), containsInAnyOrder(
|
List<String> configKeys = new ArrayList<>(List.of(
|
||||||
"syncMode",
|
"syncMode",
|
||||||
"validateSignature",
|
"validateSignature",
|
||||||
"singleLogoutServiceUrl",
|
"singleLogoutServiceUrl",
|
||||||
|
@ -1078,9 +1083,12 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
||||||
"signingCertificate",
|
"signingCertificate",
|
||||||
"addExtensionsElementWithKeyInfo",
|
"addExtensionsElementWithKeyInfo",
|
||||||
"loginHint",
|
"loginHint",
|
||||||
"hideOnLoginPage",
|
|
||||||
"idpEntityId"
|
"idpEntityId"
|
||||||
));
|
));
|
||||||
|
if (hasHideOnLoginPage) {
|
||||||
|
configKeys.add("hideOnLoginPage");
|
||||||
|
}
|
||||||
|
assertThat(config.keySet(), containsInAnyOrder(configKeys.toArray()));
|
||||||
assertThat(config, hasEntry("validateSignature", "true"));
|
assertThat(config, hasEntry("validateSignature", "true"));
|
||||||
assertThat(config, hasEntry("singleLogoutServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml"));
|
assertThat(config, hasEntry("singleLogoutServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml"));
|
||||||
assertThat(config, hasEntry("artifactResolutionServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml/resolve"));
|
assertThat(config, hasEntry("artifactResolutionServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml/resolve"));
|
||||||
|
@ -1091,9 +1099,11 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
||||||
assertThat(config, hasEntry("wantAuthnRequestsSigned", "true"));
|
assertThat(config, hasEntry("wantAuthnRequestsSigned", "true"));
|
||||||
assertThat(config, hasEntry("addExtensionsElementWithKeyInfo", "false"));
|
assertThat(config, hasEntry("addExtensionsElementWithKeyInfo", "false"));
|
||||||
assertThat(config, hasEntry("nameIDPolicyFormat", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"));
|
assertThat(config, hasEntry("nameIDPolicyFormat", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"));
|
||||||
assertThat(config, hasEntry("hideOnLoginPage", "true"));
|
|
||||||
assertThat(config, hasEntry("idpEntityId", "http://localhost:8080/auth/realms/master"));
|
assertThat(config, hasEntry("idpEntityId", "http://localhost:8080/auth/realms/master"));
|
||||||
assertThat(config, hasEntry(is("signingCertificate"), notNullValue()));
|
assertThat(config, hasEntry(is("signingCertificate"), notNullValue()));
|
||||||
|
if (hasHideOnLoginPage) {
|
||||||
|
assertThat(config, hasEntry("hideOnLoginPage", "true"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSamlImport(Map<String, String> config, String expectedSigningCertificates, boolean enabled, boolean postBindingResponse) {
|
private void assertSamlImport(Map<String, String> config, String expectedSigningCertificates, boolean enabled, boolean postBindingResponse) {
|
||||||
|
@ -1101,7 +1111,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
||||||
boolean enabledFromMetadata = Boolean.valueOf(config.get(SAMLIdentityProviderConfig.ENABLED_FROM_METADATA));
|
boolean enabledFromMetadata = Boolean.valueOf(config.get(SAMLIdentityProviderConfig.ENABLED_FROM_METADATA));
|
||||||
config.remove(SAMLIdentityProviderConfig.ENABLED_FROM_METADATA);
|
config.remove(SAMLIdentityProviderConfig.ENABLED_FROM_METADATA);
|
||||||
Assert.assertEquals(enabledFromMetadata,enabled);
|
Assert.assertEquals(enabledFromMetadata,enabled);
|
||||||
assertSamlConfig(config, postBindingResponse);
|
assertSamlConfig(config, postBindingResponse, true);
|
||||||
assertThat(config, hasEntry("signingCertificate", expectedSigningCertificates));
|
assertThat(config, hasEntry("signingCertificate", expectedSigningCertificates));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ public class KcOidcBrokerHiddenIdpHintTest extends AbstractInitializedBaseBroker
|
||||||
|
|
||||||
Map<String, String> config = idp.getConfig();
|
Map<String, String> config = idp.getConfig();
|
||||||
applyDefaultConfiguration(config, syncMode);
|
applyDefaultConfiguration(config, syncMode);
|
||||||
config.put("hideOnLoginPage", "true");
|
idp.setHideOnLogin(true);
|
||||||
return idp;
|
return idp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,12 +56,12 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
IdentityProviderRepresentation expected = orgIdPResource.toRepresentation();
|
IdentityProviderRepresentation expected = orgIdPResource.toRepresentation();
|
||||||
|
|
||||||
// organization link set
|
// organization link set
|
||||||
Assert.assertEquals(expected.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE), organization.getId());
|
Assert.assertEquals(expected.getOrganizationId(), organization.getId());
|
||||||
|
|
||||||
IdentityProviderResource idpResource = testRealm().identityProviders().get(expected.getAlias());
|
IdentityProviderResource idpResource = testRealm().identityProviders().get(expected.getAlias());
|
||||||
IdentityProviderRepresentation actual = idpResource.toRepresentation();
|
IdentityProviderRepresentation actual = idpResource.toRepresentation();
|
||||||
Assert.assertEquals(actual.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE), organization.getId());
|
Assert.assertEquals(actual.getOrganizationId(), organization.getId());
|
||||||
actual.getConfig().put(OrganizationModel.ORGANIZATION_ATTRIBUTE, "somethingelse");
|
actual.setOrganizationId("somethingelse");
|
||||||
try {
|
try {
|
||||||
idpResource.update(actual);
|
idpResource.update(actual);
|
||||||
Assert.fail("Should fail because it maps to an invalid org");
|
Assert.fail("Should fail because it maps to an invalid org");
|
||||||
|
@ -69,19 +69,19 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
OrganizationRepresentation secondOrg = createOrganization("secondorg");
|
OrganizationRepresentation secondOrg = createOrganization("secondorg");
|
||||||
actual.getConfig().put(OrganizationModel.ORGANIZATION_ATTRIBUTE, secondOrg.getId());
|
actual.setOrganizationId(secondOrg.getId());
|
||||||
idpResource.update(actual);
|
idpResource.update(actual);
|
||||||
actual = idpResource.toRepresentation();
|
actual = idpResource.toRepresentation();
|
||||||
Assert.assertEquals(actual.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE), organization.getId());
|
Assert.assertEquals(actual.getOrganizationId(), organization.getId());
|
||||||
|
|
||||||
actual = idpResource.toRepresentation();
|
actual = idpResource.toRepresentation();
|
||||||
// the link to the organization should not change
|
// the link to the organization should not change
|
||||||
Assert.assertEquals(actual.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE), organization.getId());
|
Assert.assertEquals(actual.getOrganizationId(), organization.getId());
|
||||||
actual.getConfig().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
actual.setOrganizationId(null);
|
||||||
idpResource.update(actual);
|
idpResource.update(actual);
|
||||||
actual = idpResource.toRepresentation();
|
actual = idpResource.toRepresentation();
|
||||||
// the link to the organization should not change
|
// the link to the organization should not change
|
||||||
Assert.assertEquals(actual.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE), organization.getId());
|
Assert.assertEquals(actual.getOrganizationId(), organization.getId());
|
||||||
|
|
||||||
String domain = actual.getConfig().get(ORGANIZATION_DOMAIN_ATTRIBUTE);
|
String domain = actual.getConfig().get(ORGANIZATION_DOMAIN_ATTRIBUTE);
|
||||||
|
|
||||||
|
@ -112,18 +112,19 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
.identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
.identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||||
|
|
||||||
//remove Org related stuff from the template
|
//remove Org related stuff from the template
|
||||||
idpTemplate.getConfig().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
idpTemplate.setOrganizationId(null);
|
||||||
idpTemplate.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
|
idpTemplate.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
|
||||||
|
idpTemplate.getConfig().remove(OrganizationModel.BROKER_PUBLIC);
|
||||||
idpTemplate.getConfig().remove(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
|
idpTemplate.getConfig().remove(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
idpTemplate.setAlias("idp-" + i);
|
idpTemplate.setAlias("idp-" + i);
|
||||||
idpTemplate.setInternalId(null);
|
idpTemplate.setInternalId(null);
|
||||||
try (Response response = testRealm().identityProviders().create(idpTemplate)) {
|
try (Response response = testRealm().identityProviders().create(idpTemplate)) {
|
||||||
assertThat("Falied to create idp-" + i, response.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
assertThat("Failed to create idp-" + i, response.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
||||||
}
|
}
|
||||||
try (Response response = organization.identityProviders().addIdentityProvider(idpTemplate.getAlias())) {
|
try (Response response = organization.identityProviders().addIdentityProvider(idpTemplate.getAlias())) {
|
||||||
assertThat("Falied to add idp-" + i, response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
assertThat("Failed to add idp-" + i, response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +192,7 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
// broker not removed from realm
|
// broker not removed from realm
|
||||||
IdentityProviderRepresentation idpRep = testRealm().identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
IdentityProviderRepresentation idpRep = testRealm().identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||||
// broker no longer linked to the org
|
// broker no longer linked to the org
|
||||||
Assert.assertNull(idpRep.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE));
|
Assert.assertNull(idpRep.getOrganizationId());
|
||||||
Assert.assertNull(idpRep.getConfig().get(ORGANIZATION_DOMAIN_ATTRIBUTE));
|
Assert.assertNull(idpRep.getConfig().get(ORGANIZATION_DOMAIN_ATTRIBUTE));
|
||||||
Assert.assertNull(idpRep.getConfig().get(BROKER_PUBLIC));
|
Assert.assertNull(idpRep.getConfig().get(BROKER_PUBLIC));
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
@ -194,7 +194,7 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
|
||||||
RealmRepresentation export = testRealm().partialExport(exportGroupsAndRoles, exportClients);
|
RealmRepresentation export = testRealm().partialExport(exportGroupsAndRoles, exportClients);
|
||||||
assertTrue(Optional.ofNullable(export.getGroups()).orElse(List.of()).stream().noneMatch(g -> g.getAttributes().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE)));
|
assertTrue(Optional.ofNullable(export.getGroups()).orElse(List.of()).stream().noneMatch(g -> g.getAttributes().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE)));
|
||||||
assertTrue(Optional.ofNullable(export.getOrganizations()).orElse(List.of()).isEmpty());
|
assertTrue(Optional.ofNullable(export.getOrganizations()).orElse(List.of()).isEmpty());
|
||||||
assertTrue(Optional.ofNullable(export.getIdentityProviders()).orElse(List.of()).stream().noneMatch(g -> g.getConfig().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE)));
|
assertTrue(Optional.ofNullable(export.getIdentityProviders()).orElse(List.of()).stream().noneMatch(idp -> Objects.nonNull(idp.getOrganizationId())));
|
||||||
PartialImportRepresentation rep = new PartialImportRepresentation();
|
PartialImportRepresentation rep = new PartialImportRepresentation();
|
||||||
rep.setUsers(export.getUsers());
|
rep.setUsers(export.getUsers());
|
||||||
rep.setClients(export.getClients());
|
rep.setClients(export.getClients());
|
||||||
|
|
|
@ -52,7 +52,7 @@ public class IdentityProviderBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public IdentityProviderBuilder hideOnLoginPage() {
|
public IdentityProviderBuilder hideOnLoginPage() {
|
||||||
setAttribute("hideOnLoginPage", "true");
|
rep.setHideOnLogin(true);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue