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 authenticateByDefault;
|
||||
protected boolean linkOnly;
|
||||
protected boolean hideOnLogin;
|
||||
protected String firstBrokerLoginFlowAlias;
|
||||
protected String postBrokerLoginFlowAlias;
|
||||
protected String organizationId;
|
||||
protected Map<String, String> config = new HashMap<>();
|
||||
|
||||
public String getInternalId() {
|
||||
|
@ -106,6 +108,14 @@ public class IdentityProviderRepresentation {
|
|||
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.
|
||||
|
@ -194,4 +204,12 @@ public class IdentityProviderRepresentation {
|
|||
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.
|
||||
|
||||
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 { realm } = useRealm();
|
||||
|
||||
if (!identityProvider.config?.["kc.org"]) {
|
||||
if (!identityProvider?.organizationId) {
|
||||
return "—";
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ const OrganizationLink = (identityProvider: IdentityProviderRepresentation) => {
|
|||
key={identityProvider.providerId}
|
||||
to={toEditOrganization({
|
||||
realm,
|
||||
id: identityProvider.config["kc.org"],
|
||||
id: identityProvider.organizationId,
|
||||
tab: "identityProviders",
|
||||
})}
|
||||
>
|
||||
|
@ -299,7 +299,7 @@ export default function IdentityProvidersSection() {
|
|||
cellFormatters: [upperCaseFormatter()],
|
||||
},
|
||||
{
|
||||
name: "config['kc.org']",
|
||||
name: "organizationId",
|
||||
displayKey: "linkedOrganization",
|
||||
cellRenderer: OrganizationLink,
|
||||
},
|
||||
|
|
|
@ -150,7 +150,11 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
|
|||
label="accountLinkingOnly"
|
||||
fieldType="boolean"
|
||||
/>
|
||||
<SwitchField field="config.hideOnLoginPage" label="hideOnLoginPage" />
|
||||
<SwitchField
|
||||
field="hideOnLogin"
|
||||
label="hideOnLoginPage"
|
||||
fieldType="boolean"
|
||||
/>
|
||||
|
||||
{(!isSAML || isOIDC) && (
|
||||
<FormGroupField label="filteredByClaim">
|
||||
|
|
|
@ -11,8 +11,10 @@ export default interface IdentityProviderRepresentation {
|
|||
firstBrokerLoginFlowAlias?: string;
|
||||
internalId?: string;
|
||||
linkOnly?: boolean;
|
||||
hideOnLogin?: boolean;
|
||||
postBrokerLoginFlowAlias?: string;
|
||||
providerId?: string;
|
||||
storeToken?: boolean;
|
||||
trustEmail?: boolean;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
|
|
@ -143,7 +143,7 @@ public class InfinispanIDPProvider implements IDPProvider {
|
|||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
|
|
|
@ -58,10 +58,10 @@ public class InfinispanOrganizationProviderFactory implements OrganizationProvid
|
|||
}
|
||||
|
||||
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());
|
||||
if (orgProvider != null) {
|
||||
OrganizationModel organization = orgProvider.getById(idp.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE));
|
||||
OrganizationModel organization = orgProvider.getById(idp.getOrganizationId());
|
||||
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.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.Session;
|
||||
import org.jboss.logging.Logger;
|
||||
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.ENABLED;
|
||||
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.jpa.PaginationUtils.paginateQuery;
|
||||
import static org.keycloak.utils.StreamsUtil.closing;
|
||||
|
@ -92,8 +94,10 @@ public class JpaIDPProvider implements IDPProvider {
|
|||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||
entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId());
|
||||
entity.setOrganizationId(identityProvider.getOrganizationId());
|
||||
entity.setConfig(identityProvider.getConfig());
|
||||
entity.setLinkOnly(identityProvider.isLinkOnly());
|
||||
entity.setHideOnLogin(identityProvider.isHideOnLogin());
|
||||
em.persist(entity);
|
||||
// flush so that constraint violations are flagged and converted into model exception now rather than at the end of the tx.
|
||||
em.flush();
|
||||
|
@ -113,10 +117,12 @@ public class JpaIDPProvider implements IDPProvider {
|
|||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||
entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId());
|
||||
entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId());
|
||||
entity.setOrganizationId(identityProvider.getOrganizationId());
|
||||
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
|
||||
entity.setStoreToken(identityProvider.isStoreToken());
|
||||
entity.setConfig(identityProvider.getConfig());
|
||||
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.
|
||||
em.flush();
|
||||
|
@ -218,7 +224,7 @@ public class JpaIDPProvider implements IDPProvider {
|
|||
}
|
||||
|
||||
@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();
|
||||
CriteriaQuery<IdentityProviderEntity> query = builder.createQuery(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()));
|
||||
|
||||
if (attrs != null) {
|
||||
for (Map.Entry<String, String> entry : attrs.entrySet()) {
|
||||
for (Map.Entry<String, Object> entry : attrs.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
Object value = entry.getValue();
|
||||
if (StringUtil.isBlank(key)) {
|
||||
continue;
|
||||
}
|
||||
switch(key) {
|
||||
case AUTHENTICATE_BY_DEFAULT:
|
||||
case ENABLED:
|
||||
case AUTHENTICATE_BY_DEFAULT: {
|
||||
predicates.add(builder.equal(idp.get(key), Boolean.valueOf(value)));
|
||||
case HIDE_ON_LOGIN:
|
||||
case LINK_ONLY: {
|
||||
if (Boolean.parseBoolean(value.toString())) {
|
||||
predicates.add(builder.isTrue(idp.get(key)));
|
||||
} else {
|
||||
predicates.add(builder.isFalse(idp.get(key)));
|
||||
}
|
||||
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)));
|
||||
} else {
|
||||
predicates.add(builder.equal(idp.get(key), value));
|
||||
|
@ -377,10 +391,12 @@ public class JpaIDPProvider implements IDPProvider {
|
|||
identityProviderModel.setConfig(config);
|
||||
identityProviderModel.setEnabled(entity.isEnabled());
|
||||
identityProviderModel.setLinkOnly(entity.isLinkOnly());
|
||||
identityProviderModel.setHideOnLogin(entity.isHideOnLogin());
|
||||
identityProviderModel.setTrustEmail(entity.isTrustEmail());
|
||||
identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault());
|
||||
identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId());
|
||||
identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId());
|
||||
identityProviderModel.setOrganizationId(entity.getOrganizationId());
|
||||
identityProviderModel.setStoreToken(entity.isStoreToken());
|
||||
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
||||
|
||||
|
|
|
@ -65,6 +65,9 @@ public class IdentityProviderEntity {
|
|||
@Column(name="LINK_ONLY")
|
||||
private boolean linkOnly;
|
||||
|
||||
@Column(name="HIDE_ON_LOGIN")
|
||||
private boolean hideOnLogin;
|
||||
|
||||
@Column(name="ADD_TOKEN_ROLE")
|
||||
protected boolean addReadTokenRoleOnCreate;
|
||||
|
||||
|
@ -77,6 +80,9 @@ public class IdentityProviderEntity {
|
|||
@Column(name="POST_BROKER_LOGIN_FLOW_ID")
|
||||
private String postBrokerLoginFlowId;
|
||||
|
||||
@Column(name="ORGANIZATION_ID")
|
||||
private String organizationId;
|
||||
|
||||
@ElementCollection
|
||||
@MapKeyColumn(name="NAME")
|
||||
@Column(name="VALUE", columnDefinition = "TEXT")
|
||||
|
@ -163,6 +169,22 @@ public class IdentityProviderEntity {
|
|||
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() {
|
||||
return this.config;
|
||||
}
|
||||
|
|
|
@ -73,4 +73,23 @@
|
|||
</createIndex>
|
||||
</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>
|
||||
|
|
|
@ -819,6 +819,7 @@ public class ModelToRepresentation {
|
|||
IdentityProviderRepresentation providerRep = toBriefRepresentation(realm, identityProviderModel);
|
||||
|
||||
providerRep.setLinkOnly(identityProviderModel.isLinkOnly());
|
||||
providerRep.setHideOnLogin(identityProviderModel.isHideOnLogin());
|
||||
providerRep.setStoreToken(identityProviderModel.isStoreToken());
|
||||
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
|
||||
providerRep.setAuthenticateByDefault(identityProviderModel.isAuthenticateByDefault());
|
||||
|
@ -849,8 +850,8 @@ public class ModelToRepresentation {
|
|||
providerRep.setPostBrokerLoginFlowAlias(flow.getAlias());
|
||||
}
|
||||
|
||||
if (export) {
|
||||
providerRep.getConfig().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
if (!export) {
|
||||
providerRep.setOrganizationId(identityProviderModel.getOrganizationId());
|
||||
}
|
||||
|
||||
return providerRep;
|
||||
|
|
|
@ -78,7 +78,6 @@ import org.keycloak.models.FederatedIdentityModel;
|
|||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.OrganizationDomainModel;
|
||||
|
@ -867,15 +866,20 @@ public class RepresentationToModel {
|
|||
identityProviderModel.setProviderId(representation.getProviderId());
|
||||
identityProviderModel.setEnabled(representation.isEnabled());
|
||||
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.setAuthenticateByDefault(representation.isAuthenticateByDefault());
|
||||
identityProviderModel.setStoreToken(representation.isStoreToken());
|
||||
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
|
||||
updateOrganizationBroker(realm, representation, session);
|
||||
updateOrganizationBroker(representation, session);
|
||||
identityProviderModel.setOrganizationId(representation.getOrganizationId());
|
||||
identityProviderModel.setConfig(removeEmptyString(representation.getConfig()));
|
||||
|
||||
String flowAlias = representation.getFirstBrokerLoginFlowAlias();
|
||||
if (flowAlias == null || flowAlias.trim().length() == 0) {
|
||||
if (flowAlias == null || flowAlias.trim().isEmpty()) {
|
||||
identityProviderModel.setFirstBrokerLoginFlowId(null);
|
||||
} else {
|
||||
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
|
||||
|
@ -886,7 +890,7 @@ public class RepresentationToModel {
|
|||
}
|
||||
|
||||
flowAlias = representation.getPostBrokerLoginFlowAlias();
|
||||
if (flowAlias == null || flowAlias.trim().length() == 0) {
|
||||
if (flowAlias == null || flowAlias.trim().isEmpty()) {
|
||||
identityProviderModel.setPostBrokerLoginFlowId(null);
|
||||
} else {
|
||||
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
|
||||
|
@ -1639,21 +1643,22 @@ public class RepresentationToModel {
|
|||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
IdentityProviderModel existing = Optional.ofNullable(session.identityProviders().getByAlias(representation.getAlias()))
|
||||
.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) {
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
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");
|
||||
}
|
||||
|
||||
|
@ -1666,7 +1671,7 @@ public class RepresentationToModel {
|
|||
}
|
||||
|
||||
// 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;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
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}.
|
||||
* @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.
|
||||
|
@ -129,7 +130,7 @@ public interface IDPProvider extends Provider {
|
|||
* @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria.
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -163,4 +209,6 @@ public interface IDPProvider extends Provider {
|
|||
default boolean isIdentityFederationEnabled() {
|
||||
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 FILTERED_BY_CLAIMS = "filteredByClaim";
|
||||
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 METADATA_DESCRIPTOR_URL = "metadataDescriptorUrl";
|
||||
public static final String ORGANIZATION_ID = "organizationId";
|
||||
public static final String PASS_MAX_AGE = "passMaxAge";
|
||||
public static final String POST_BROKER_LOGIN_FLOW_ID = "postBrokerLoginFlowId";
|
||||
public static final String SYNC_MODE = "syncMode";
|
||||
|
@ -80,11 +83,13 @@ public class IdentityProviderModel implements Serializable {
|
|||
|
||||
private String postBrokerLoginFlowId;
|
||||
|
||||
private String organizationId;
|
||||
|
||||
private String displayName;
|
||||
|
||||
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
|
||||
|
@ -110,7 +115,9 @@ public class IdentityProviderModel implements Serializable {
|
|||
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
|
||||
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
|
||||
this.postBrokerLoginFlowId = model.getPostBrokerLoginFlowId();
|
||||
this.organizationId = model.getOrganizationId();
|
||||
this.displayIconClasses = model.getDisplayIconClasses();
|
||||
this.hideOnLogin = model.isHideOnLogin();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,11 +232,11 @@ public class IdentityProviderModel implements Serializable {
|
|||
}
|
||||
|
||||
public String getOrganizationId() {
|
||||
return getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
return this.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() {
|
||||
return Boolean.valueOf(getConfig().get(HIDE_ON_LOGIN));
|
||||
return this.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;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Objects;
|
||||
|
||||
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.freemarker.LoginFormsUtil;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
public final class UsernameForm extends UsernamePasswordForm {
|
||||
|
||||
|
@ -34,9 +38,7 @@ public final class UsernameForm extends UsernamePasswordForm {
|
|||
public void authenticate(AuthenticationFlowContext context) {
|
||||
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
|
||||
Stream<IdentityProviderModel> identityProviders = LoginFormsUtil
|
||||
.filterIdentityProviders(context.getRealm().getIdentityProvidersStream(), context.getSession(), context);
|
||||
if (identityProviders.findAny().isEmpty()) {
|
||||
if (!this.contextUserHasFederatedIDPs(context)) {
|
||||
context.success();
|
||||
return;
|
||||
}
|
||||
|
@ -69,4 +71,28 @@ public final class UsernameForm extends UsernamePasswordForm {
|
|||
return Messages.INVALID_USERNAME_OR_EMAIL;
|
||||
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()) {
|
||||
if (MACEDIR_ENTITY_CATEGORY.equals(attribute.getName())
|
||||
&& 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.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -101,7 +100,6 @@ import java.util.Objects;
|
|||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
|
||||
import static org.keycloak.organization.utils.Organizations.resolveOrganization;
|
||||
|
@ -480,12 +478,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
if (realm != null) {
|
||||
attributes.put("realm", new RealmBean(realm));
|
||||
|
||||
Stream<IdentityProviderModel> identityProviders = LoginFormsUtil
|
||||
.filterIdentityProvidersForTheme(realm.getIdentityProvidersStream(), session, context);
|
||||
IdentityProviderBean idpBean = new IdentityProviderBean(realm, session, identityProviders, baseUriWithCodeAndClientId);
|
||||
IdentityProviderBean idpBean = new IdentityProviderBean(session, realm, baseUriWithCodeAndClientId, context);
|
||||
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
idpBean = new OrganizationAwareIdentityProviderBean(idpBean, session);
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION) && realm.isOrganizationsEnabled()) {
|
||||
idpBean = new OrganizationAwareIdentityProviderBean(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;
|
||||
|
||||
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.KeycloakSession;
|
||||
import org.keycloak.models.OrderedModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
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>
|
||||
|
@ -42,44 +54,63 @@ public class IdentityProviderBean {
|
|||
public static OrderedModel.OrderedModelComparator<IdentityProvider> IDP_COMPARATOR_INSTANCE = new OrderedModel.OrderedModelComparator<>();
|
||||
private static final String ICON_THEME_PREFIX = "kcLogoIdP-";
|
||||
|
||||
private boolean displaySocial;
|
||||
private List<IdentityProvider> providers;
|
||||
private RealmModel realm;
|
||||
private final KeycloakSession session;
|
||||
protected AuthenticationFlowContext context;
|
||||
protected List<IdentityProvider> providers;
|
||||
protected KeycloakSession session;
|
||||
protected RealmModel realm;
|
||||
protected URI baseURI;
|
||||
|
||||
public IdentityProviderBean() {
|
||||
this.session = null;
|
||||
}
|
||||
|
||||
public IdentityProviderBean(RealmModel realm, KeycloakSession session, Stream<IdentityProviderModel> identityProviders, URI baseURI) {
|
||||
this.realm = realm;
|
||||
public IdentityProviderBean(KeycloakSession session, RealmModel realm, URI baseURI, AuthenticationFlowContext context) {
|
||||
this.session = session;
|
||||
|
||||
List<IdentityProvider> orderedList = new ArrayList<>();
|
||||
|
||||
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;
|
||||
}
|
||||
this.realm = realm;
|
||||
this.baseURI = baseURI;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
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 displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProvider);
|
||||
Map<String, String> config = identityProvider.getConfig();
|
||||
boolean hideOnLoginPage = config != null && Boolean.parseBoolean(config.get("hideOnLoginPage"));
|
||||
if (!hideOnLoginPage) {
|
||||
orderedSet.add(new IdentityProvider(identityProvider.getAlias(),
|
||||
displayName, identityProvider.getProviderId(), loginUrl,
|
||||
config != null ? config.get("guiOrder") : null, getLoginIconClasses(identityProvider)));
|
||||
}
|
||||
return new IdentityProvider(identityProvider.getAlias(),
|
||||
displayName, identityProvider.getProviderId(), loginUrl,
|
||||
identityProvider.getConfig().get("guiOrder"), getLoginIconClasses(identityProvider));
|
||||
}
|
||||
|
||||
// Get icon classes defined in properties of current theme with key 'kcLogoIdP-{alias}'
|
||||
|
@ -97,12 +128,121 @@ public class IdentityProviderBean {
|
|||
return "";
|
||||
}
|
||||
|
||||
public List<IdentityProvider> getProviders() {
|
||||
return providers;
|
||||
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;
|
||||
}
|
||||
|
||||
public boolean isDisplayInfo() {
|
||||
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 {
|
||||
|
@ -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 -> {
|
||||
if (hasPublicBrokers(organization)) {
|
||||
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
|
||||
attributes.computeIfPresent("realm",
|
||||
|
@ -188,7 +188,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
|||
);
|
||||
} else {
|
||||
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()
|
||||
.setAttributeMapper(attributes -> {
|
||||
attributes.computeIfPresent("social",
|
||||
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, false, true)
|
||||
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, false, true)
|
||||
);
|
||||
attributes.computeIfPresent("auth",
|
||||
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
|
||||
|
|
|
@ -18,73 +18,88 @@
|
|||
package org.keycloak.organization.forms.login.freemarker.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
||||
import org.keycloak.models.IDPProvider;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.organization.utils.Organizations;
|
||||
|
||||
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final List<IdentityProvider> providers;
|
||||
private final OrganizationModel organization;
|
||||
private final boolean onlyRealmBrokers;
|
||||
private final boolean onlyOrganizationBrokers;
|
||||
|
||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers) {
|
||||
this(delegate, session, onlyOrganizationBrokers, false);
|
||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate) {
|
||||
this(delegate, false);
|
||||
}
|
||||
|
||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers, boolean onlyRealmBrokers) {
|
||||
this.session = session;
|
||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, boolean onlyOrganizationBrokers) {
|
||||
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) {
|
||||
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
||||
.filter(Predicate.not(this::isPublicOrganizationBroker))
|
||||
.toList();
|
||||
} else if (onlyOrganizationBrokers) {
|
||||
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
||||
.filter(this::isPublicOrganizationBroker)
|
||||
.toList();
|
||||
} else {
|
||||
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
||||
.filter(p -> isRealmBroker(p) || isPublicOrganizationBroker(p))
|
||||
.toList();
|
||||
// we only want the realm-level IDPs - i.e. those not associated with any orgs.
|
||||
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();
|
||||
}
|
||||
}
|
||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session) {
|
||||
this(delegate, session, false);
|
||||
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();
|
||||
}
|
||||
// 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
|
||||
public List<IdentityProvider> getProviders() {
|
||||
return providers;
|
||||
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) {
|
||||
return isPublicOrganizationBroker(idp);
|
||||
} else {
|
||||
return idp.getOrganizationId() == null || isPublicOrganizationBroker(idp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisplayInfo() {
|
||||
return !providers.isEmpty();
|
||||
}
|
||||
private boolean isPublicOrganizationBroker(IdentityProviderModel idp) {
|
||||
|
||||
private boolean isPublicOrganizationBroker(IdentityProvider idp) {
|
||||
IdentityProviderModel model = session.identityProviders().getByAlias(idp.getAlias());
|
||||
|
||||
if (model.getOrganizationId() == null) {
|
||||
if (idp.getOrganizationId() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OrganizationModel organization = Organizations.resolveOrganization(session);
|
||||
|
||||
if (organization != null && !organization.getId().equals(model.getOrganizationId())) {
|
||||
if (organization != null && !Objects.equals(organization.getId(),idp.getOrganizationId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
return Boolean.parseBoolean(idp.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -197,6 +197,8 @@ public class IdentityProviderResource {
|
|||
}
|
||||
|
||||
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)) {
|
||||
|
||||
|
|
|
@ -215,6 +215,7 @@ public class IdentityProvidersResource {
|
|||
session.identityProviders().create(identityProvider);
|
||||
|
||||
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())
|
||||
.representation(StripSecretsUtils.stripSecrets(session, representation)).success();
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ import java.nio.file.Paths;
|
|||
import java.security.PublicKey;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
|
@ -587,6 +588,8 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
|||
|
||||
String secret = idpRep.getConfig() != null ? idpRep.getConfig().get("clientSecret") : null;
|
||||
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);
|
||||
|
||||
|
@ -1044,6 +1047,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
|||
Assert.assertEquals("alias", expected.getAlias(), actual.getAlias());
|
||||
Assert.assertEquals("providerId", expected.getProviderId(), actual.getProviderId());
|
||||
Assert.assertEquals("enabled", expected.isEnabled(), actual.isEnabled());
|
||||
Assert.assertEquals("hideOnLogin", expected.isHideOnLogin(), actual.isHideOnLogin());
|
||||
Assert.assertEquals("firstBrokerLoginFlowAlias", expected.getFirstBrokerLoginFlowAlias(), actual.getFirstBrokerLoginFlowAlias());
|
||||
Assert.assertEquals("config", expected.getConfig(), actual.getConfig());
|
||||
}
|
||||
|
@ -1055,32 +1059,36 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
|||
Assert.assertEquals("alias", "saml", idp.getAlias());
|
||||
Assert.assertEquals("providerId", "saml", idp.getProviderId());
|
||||
Assert.assertEquals("enabled",enabled, idp.isEnabled());
|
||||
Assert.assertTrue("hideOnLogin", idp.isHideOnLogin());
|
||||
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.
|
||||
// check that saml-idp-metadata.xml was properly converted into key value pairs
|
||||
//System.out.println(config);
|
||||
assertThat(config.keySet(), containsInAnyOrder(
|
||||
"syncMode",
|
||||
"validateSignature",
|
||||
"singleLogoutServiceUrl",
|
||||
"postBindingLogout",
|
||||
"postBindingResponse",
|
||||
"artifactBindingResponse",
|
||||
"postBindingAuthnRequest",
|
||||
"singleSignOnServiceUrl",
|
||||
"artifactResolutionServiceUrl",
|
||||
"wantAuthnRequestsSigned",
|
||||
"nameIDPolicyFormat",
|
||||
"signingCertificate",
|
||||
"addExtensionsElementWithKeyInfo",
|
||||
"loginHint",
|
||||
"hideOnLoginPage",
|
||||
"idpEntityId"
|
||||
List<String> configKeys = new ArrayList<>(List.of(
|
||||
"syncMode",
|
||||
"validateSignature",
|
||||
"singleLogoutServiceUrl",
|
||||
"postBindingLogout",
|
||||
"postBindingResponse",
|
||||
"artifactBindingResponse",
|
||||
"postBindingAuthnRequest",
|
||||
"singleSignOnServiceUrl",
|
||||
"artifactResolutionServiceUrl",
|
||||
"wantAuthnRequestsSigned",
|
||||
"nameIDPolicyFormat",
|
||||
"signingCertificate",
|
||||
"addExtensionsElementWithKeyInfo",
|
||||
"loginHint",
|
||||
"idpEntityId"
|
||||
));
|
||||
if (hasHideOnLoginPage) {
|
||||
configKeys.add("hideOnLoginPage");
|
||||
}
|
||||
assertThat(config.keySet(), containsInAnyOrder(configKeys.toArray()));
|
||||
assertThat(config, hasEntry("validateSignature", "true"));
|
||||
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"));
|
||||
|
@ -1091,9 +1099,11 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
|||
assertThat(config, hasEntry("wantAuthnRequestsSigned", "true"));
|
||||
assertThat(config, hasEntry("addExtensionsElementWithKeyInfo", "false"));
|
||||
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(is("signingCertificate"), notNullValue()));
|
||||
if (hasHideOnLoginPage) {
|
||||
assertThat(config, hasEntry("hideOnLoginPage", "true"));
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
config.remove(SAMLIdentityProviderConfig.ENABLED_FROM_METADATA);
|
||||
Assert.assertEquals(enabledFromMetadata,enabled);
|
||||
assertSamlConfig(config, postBindingResponse);
|
||||
assertSamlConfig(config, postBindingResponse, true);
|
||||
assertThat(config, hasEntry("signingCertificate", expectedSigningCertificates));
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ public class KcOidcBrokerHiddenIdpHintTest extends AbstractInitializedBaseBroker
|
|||
|
||||
Map<String, String> config = idp.getConfig();
|
||||
applyDefaultConfiguration(config, syncMode);
|
||||
config.put("hideOnLoginPage", "true");
|
||||
idp.setHideOnLogin(true);
|
||||
return idp;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,12 +56,12 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
|||
IdentityProviderRepresentation expected = orgIdPResource.toRepresentation();
|
||||
|
||||
// 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());
|
||||
IdentityProviderRepresentation actual = idpResource.toRepresentation();
|
||||
Assert.assertEquals(actual.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE), organization.getId());
|
||||
actual.getConfig().put(OrganizationModel.ORGANIZATION_ATTRIBUTE, "somethingelse");
|
||||
Assert.assertEquals(actual.getOrganizationId(), organization.getId());
|
||||
actual.setOrganizationId("somethingelse");
|
||||
try {
|
||||
idpResource.update(actual);
|
||||
Assert.fail("Should fail because it maps to an invalid org");
|
||||
|
@ -69,19 +69,19 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
|||
}
|
||||
|
||||
OrganizationRepresentation secondOrg = createOrganization("secondorg");
|
||||
actual.getConfig().put(OrganizationModel.ORGANIZATION_ATTRIBUTE, secondOrg.getId());
|
||||
actual.setOrganizationId(secondOrg.getId());
|
||||
idpResource.update(actual);
|
||||
actual = idpResource.toRepresentation();
|
||||
Assert.assertEquals(actual.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE), organization.getId());
|
||||
Assert.assertEquals(actual.getOrganizationId(), organization.getId());
|
||||
|
||||
actual = idpResource.toRepresentation();
|
||||
// the link to the organization should not change
|
||||
Assert.assertEquals(actual.getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE), organization.getId());
|
||||
actual.getConfig().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
Assert.assertEquals(actual.getOrganizationId(), organization.getId());
|
||||
actual.setOrganizationId(null);
|
||||
idpResource.update(actual);
|
||||
actual = idpResource.toRepresentation();
|
||||
// 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);
|
||||
|
||||
|
@ -112,18 +112,19 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
|||
.identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||
|
||||
//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.BROKER_PUBLIC);
|
||||
idpTemplate.getConfig().remove(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
idpTemplate.setAlias("idp-" + i);
|
||||
idpTemplate.setInternalId(null);
|
||||
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())) {
|
||||
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
|
||||
IdentityProviderRepresentation idpRep = testRealm().identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||
// 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(BROKER_PUBLIC));
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.hamcrest.Matchers;
|
||||
|
@ -194,7 +194,7 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
|
|||
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.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();
|
||||
rep.setUsers(export.getUsers());
|
||||
rep.setClients(export.getClients());
|
||||
|
|
|
@ -52,7 +52,7 @@ public class IdentityProviderBuilder {
|
|||
}
|
||||
|
||||
public IdentityProviderBuilder hideOnLoginPage() {
|
||||
setAttribute("hideOnLoginPage", "true");
|
||||
rep.setHideOnLogin(true);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue