Ability to declare a default "First broker login flow" per Realm

Closes #25823

Signed-off-by: Réda Housni Alaoui <reda-alaoui@hey.com>
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Réda Housni Alaoui 2024-02-28 16:17:51 +01:00 committed by GitHub
parent 9cced05049
commit a3b3ee4b87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 127 additions and 28 deletions

View file

@ -206,6 +206,7 @@ public class RealmRepresentation {
protected String resetCredentialsFlow;
protected String clientAuthenticationFlow;
protected String dockerAuthenticationFlow;
protected String firstBrokerLoginFlow;
protected Map<String, String> attributes;
@ -1328,6 +1329,15 @@ public class RealmRepresentation {
return this;
}
public String getFirstBrokerLoginFlow() {
return firstBrokerLoginFlow;
}
public RealmRepresentation setFirstBrokerLoginFlow(String firstBrokerLoginFlow) {
this.firstBrokerLoginFlow = firstBrokerLoginFlow;
return this;
}
public String getKeycloakVersion() {
return keycloakVersion;
}

View file

@ -4,6 +4,7 @@ import Masthead from "../../Masthead";
const masthead = new Masthead();
export enum LoginFlowOption {
empty = "",
none = "None",
browser = "browser",
directGrant = "direct grant",
@ -68,7 +69,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
#doNotStoreUsers = "#doNotStoreUsers";
#accountLinkingOnlySwitch = "#accountLinkingOnly";
#hideOnLoginPageSwitch = "#hideOnLoginPage";
#firstLoginFlowSelect = "#firstBrokerLoginFlowAlias";
#firstLoginFlowSelect = "#firstBrokerLoginFlowAliasOverride";
#postLoginFlowSelect = "#postBrokerLoginFlowAlias";
#syncModeSelect = "#syncMode";
#essentialClaimSwitch = "#filteredByClaim";
@ -496,9 +497,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
this.assertAccountLinkingOnlySwitchTurnedOn(false);
this.assertHideOnLoginPageSwitchTurnedOn(false);
this.assertFirstLoginFlowSelectOptionEqual(
LoginFlowOption.firstBrokerLogin,
);
this.assertFirstLoginFlowSelectOptionEqual(LoginFlowOption.empty);
this.assertPostLoginFlowSelectOptionEqual(LoginFlowOption.none);
this.assertSyncModeSelectOptionEqual(SyncModeOption.import);
this.assertClientAssertSigAlgSelectOptionEqual(

View file

@ -357,6 +357,7 @@ algorithmNotSpecified=Algorithm not specified
jwtX509HeadersEnabled=Add X.509 Headers to the JWT
rememberMe=Remember me
flow.registration=Registration flow
flow.firstBrokerLogin=First broker login flow
showLess=Show less
registeredClusterNodes=Registered cluster nodes
connectionAndAuthenticationSettings=Connection and authentication settings
@ -950,6 +951,7 @@ homeURL=Home URL
eventTypes.REVOKE_GRANT_ERROR.name=Revoke grant error
contentSecurityPolicyReportOnly=Content-Security-Policy-Report-Only
firstBrokerLoginFlowAlias=First login flow
firstBrokerLoginFlowAliasOverride=First login flow override
missingAttributes=No {{label}} have been defined yet. Click the below button to add {{label}}, key and value are required for a key pair.
testConnectionError=Error\! {{error}}
authenticatedAccessPoliciesHelp=Those Policies are used when Client Registration Service is invoked by authenticated request. This means that the request contains Initial Access Token or Bearer Token.

View file

@ -57,6 +57,7 @@ export const REALM_FLOWS = new Map<string, string>([
["resetCredentialsFlow", "reset credentials"],
["clientAuthenticationFlow", "clients"],
["dockerAuthenticationFlow", "docker auth"],
["firstBrokerLoginFlow", "firstBrokerLogin"],
]);
const AliasRenderer = ({ id, alias, usedBy, builtIn }: AuthenticationType) => {

View file

@ -26,7 +26,8 @@ const LoginFlow = ({
field,
label,
defaultValue,
}: FieldProps & { defaultValue: string }) => {
labelForEmpty = "none",
}: FieldProps & { defaultValue: string; labelForEmpty?: string }) => {
const { t } = useTranslation();
const { control } = useFormContext();
@ -59,7 +60,7 @@ const LoginFlow = ({
field.onChange(value as string);
setOpen(false);
}}
selections={field.value || t("none")}
selections={field.value || t(labelForEmpty)}
variant={SelectVariant.single}
aria-label={t(label)}
isOpen={open}
@ -68,7 +69,7 @@ const LoginFlow = ({
...(defaultValue === ""
? [
<SelectOption key="empty" value="">
{t("none")}
{t(labelForEmpty)}
</SelectOption>,
]
: []),
@ -232,8 +233,9 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
)}
<LoginFlow
field="firstBrokerLoginFlowAlias"
label="firstBrokerLoginFlowAlias"
defaultValue="fist broker login"
label="firstBrokerLoginFlowAliasOverride"
defaultValue=""
labelForEmpty=""
/>
<LoginFlow
field="postBrokerLoginFlowAlias"

View file

@ -1266,6 +1266,18 @@ public class RealmAdapter implements CachedRealmModel {
updated.setDockerAuthenticationFlow(flow);
}
@Override
public AuthenticationFlowModel getFirstBrokerLoginFlow() {
if (isUpdated()) return updated.getFirstBrokerLoginFlow();
return cached.getFirstBrokerLoginFlow();
}
@Override
public void setFirstBrokerLoginFlow(AuthenticationFlowModel flow) {
getDelegateForUpdate();
updated.setFirstBrokerLoginFlow(flow);
}
@Override
public Stream<AuthenticationFlowModel> getAuthenticationFlowsStream() {
if (isUpdated()) return updated.getAuthenticationFlowsStream();

View file

@ -144,6 +144,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected AuthenticationFlowModel resetCredentialsFlow;
protected AuthenticationFlowModel clientAuthenticationFlow;
protected AuthenticationFlowModel dockerAuthenticationFlow;
protected AuthenticationFlowModel firstBrokerLoginFlow;
protected boolean eventsEnabled;
protected long eventsExpiration;
@ -302,6 +303,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
resetCredentialsFlow = model.getResetCredentialsFlow();
clientAuthenticationFlow = model.getClientAuthenticationFlow();
dockerAuthenticationFlow = model.getDockerAuthenticationFlow();
firstBrokerLoginFlow = model.getFirstBrokerLoginFlow();
model.getComponentsStream().forEach(component ->
componentsByParentAndType.add(component.getParentId() + component.getProviderType(), component)
@ -687,6 +689,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return dockerAuthenticationFlow;
}
public AuthenticationFlowModel getFirstBrokerLoginFlow() {
return firstBrokerLoginFlow;
}
public List<String> getDefaultGroups() {
return defaultGroups;
}

View file

@ -1568,6 +1568,18 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel<RealmEn
realm.setDockerAuthenticationFlow(flow.getId());
}
@Override
public AuthenticationFlowModel getFirstBrokerLoginFlow() {
String flowId = getAttribute(RealmAttributes.FIRST_BROKER_LOGIN_FLOW_ID);
if (flowId == null) return null;
return getAuthenticationFlowById(flowId);
}
@Override
public void setFirstBrokerLoginFlow(AuthenticationFlowModel flow) {
setAttribute(RealmAttributes.FIRST_BROKER_LOGIN_FLOW_ID, flow.getId());
}
@Override
public Stream<AuthenticationFlowModel> getAuthenticationFlowsStream() {
return realm.getAuthenticationFlows().stream().map(this::entityToModel);

View file

@ -54,4 +54,6 @@ public interface RealmAttributes {
String ADMIN_EVENTS_EXPIRATION = "adminEventsExpiration";
String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId";
}

View file

@ -21,10 +21,12 @@ package org.keycloak.migration.migrators;
import org.jboss.logging.Logger;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.DefaultKeyProviders;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.userprofile.config.UPConfig;
@ -60,6 +62,7 @@ public class MigrateTo24_0_0 implements Migration {
updateUserProfileSettings(session);
updateLdapProviderConfig(session);
createHS512ComponentModelKey(session);
bindFirstBrokerLoginFlow(session);
} finally {
context.setRealm(null);
}
@ -103,4 +106,16 @@ public class MigrateTo24_0_0 implements Migration {
RealmModel realm = session.getContext().getRealm();
DefaultKeyProviders.createSecretProvider(realm);
}
private void bindFirstBrokerLoginFlow(KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();
String flowAlias = DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW;
AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias);
if (flow == null) {
LOG.debugf("No flow found for alias '%s'. Skipping.", flowAlias);
return;
}
realm.setFirstBrokerLoginFlow(flow);
LOG.debugf("Flow '%s' has been bound to realm %s as 'First broker login' flow", realm.getName());
}
}

View file

@ -871,6 +871,9 @@ public class DefaultExportImportManager implements ExportImportManager {
if (rep.getDockerAuthenticationFlow() != null) {
realm.setDockerAuthenticationFlow(realm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
}
if (rep.getFirstBrokerLoginFlow() != null) {
realm.setFirstBrokerLoginFlow(realm.getFlowByAlias(rep.getFirstBrokerLoginFlow()));
}
}
@Override
@ -1363,10 +1366,15 @@ public class DefaultExportImportManager implements ExportImportManager {
} else {
newRealm.setClientAuthenticationFlow(newRealm.getFlowByAlias(rep.getClientAuthenticationFlow()));
}
// Added in 1.7
if (newRealm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW) == null) {
DefaultAuthenticationFlows.firstBrokerLoginFlow(newRealm, true);
if (rep.getFirstBrokerLoginFlow() == null) {
AuthenticationFlowModel firstBrokerLoginFlow = newRealm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW);
if (firstBrokerLoginFlow == null) {
DefaultAuthenticationFlows.firstBrokerLoginFlow(newRealm, true);
} else {
newRealm.setFirstBrokerLoginFlow(firstBrokerLoginFlow);
}
} else {
newRealm.setFirstBrokerLoginFlow(newRealm.getFlowByAlias(rep.getFirstBrokerLoginFlow()));
}
// Added in 2.2

View file

@ -42,7 +42,7 @@ public class AuthenticationMapper {
}
final List<String> useAsDefault = Stream.of(realm.getBrowserFlow(), realm.getRegistrationFlow(), realm.getDirectGrantFlow(),
realm.getResetCredentialsFlow(), realm.getClientAuthenticationFlow(), realm.getDockerAuthenticationFlow())
realm.getResetCredentialsFlow(), realm.getClientAuthenticationFlow(), realm.getDockerAuthenticationFlow(), realm.getFirstBrokerLoginFlow())
.filter(f -> flow.getAlias().equals(f.getAlias())).map(AuthenticationFlowModel::getAlias).collect(Collectors.toList());
if (!useAsDefault.isEmpty()) {

View file

@ -469,6 +469,7 @@ public class DefaultAuthenticationFlows {
firstBrokerLogin.setTopLevel(true);
firstBrokerLogin.setBuiltIn(true);
firstBrokerLogin = realm.addAuthenticationFlow(firstBrokerLogin);
realm.setFirstBrokerLoginFlow(firstBrokerLogin);
AuthenticatorConfigModel reviewProfileConfig = new AuthenticatorConfigModel();
reviewProfileConfig.setAlias(IDP_REVIEW_PROFILE_CONFIG_ALIAS);

View file

@ -854,6 +854,7 @@ public final class KeycloakModelUtils {
if ((realmFlow = realm.getDirectGrantFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getResetCredentialsFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getDockerAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getFirstBrokerLoginFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
return realm.getIdentityProvidersStream().anyMatch(idp ->
Objects.equals(idp.getFirstBrokerLoginFlowId(), model.getId()) ||

View file

@ -115,6 +115,8 @@ public class ModelToRepresentation {
REALM_EXCLUDED_ATTRIBUTES.add(Constants.CLIENT_POLICIES);
REALM_EXCLUDED_ATTRIBUTES.add(Constants.CLIENT_PROFILES);
REALM_EXCLUDED_ATTRIBUTES.add("firstBrokerLoginFlowId");
}
private static final Logger LOG = Logger.getLogger(ModelToRepresentation.class);
@ -471,6 +473,7 @@ public class ModelToRepresentation {
if (realm.getResetCredentialsFlow() != null) rep.setResetCredentialsFlow(realm.getResetCredentialsFlow().getAlias());
if (realm.getClientAuthenticationFlow() != null) rep.setClientAuthenticationFlow(realm.getClientAuthenticationFlow().getAlias());
if (realm.getDockerAuthenticationFlow() != null) rep.setDockerAuthenticationFlow(realm.getDockerAuthenticationFlow().getAlias());
if (realm.getFirstBrokerLoginFlow() != null) rep.setFirstBrokerLoginFlow(realm.getFirstBrokerLoginFlow().getAlias());
rep.setDefaultRole(toBriefRepresentation(realm.getDefaultRole()));

View file

@ -838,21 +838,21 @@ public class RepresentationToModel {
identityProviderModel.setConfig(removeEmptyString(representation.getConfig()));
String flowAlias = representation.getFirstBrokerLoginFlowAlias();
if (flowAlias == null) {
flowAlias = DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW;
if (flowAlias == null || flowAlias.trim().length() == 0) {
identityProviderModel.setFirstBrokerLoginFlowId(null);
} else {
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
if (flowModel == null) {
throw new ModelException("No available authentication flow with alias: " + flowAlias);
}
identityProviderModel.setFirstBrokerLoginFlowId(flowModel.getId());
}
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
if (flowModel == null) {
throw new ModelException("No available authentication flow with alias: " + flowAlias);
}
identityProviderModel.setFirstBrokerLoginFlowId(flowModel.getId());
flowAlias = representation.getPostBrokerLoginFlowAlias();
if (flowAlias == null || flowAlias.trim().length() == 0) {
identityProviderModel.setPostBrokerLoginFlowId(null);
} else {
flowModel = realm.getFlowByAlias(flowAlias);
AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
if (flowModel == null) {
throw new ModelException("No available authentication flow with alias: " + flowAlias);
}

View file

@ -1244,6 +1244,16 @@ public class IdentityBrokerStateTestHelpers {
}
@Override
public AuthenticationFlowModel getFirstBrokerLoginFlow() {
return null;
}
@Override
public void setFirstBrokerLoginFlow(AuthenticationFlowModel flow) {
}
@Override
public Stream<AuthenticationFlowModel> getAuthenticationFlowsStream() {
return null;

View file

@ -375,6 +375,9 @@ public interface RealmModel extends RoleContainerModel {
AuthenticationFlowModel getDockerAuthenticationFlow();
void setDockerAuthenticationFlow(AuthenticationFlowModel flow);
AuthenticationFlowModel getFirstBrokerLoginFlow();
void setFirstBrokerLoginFlow(AuthenticationFlowModel flow);
/**
* Returns authentications flows as a stream.
* @return Stream of {@link AuthenticationFlowModel}. Never returns {@code null}.

View file

@ -849,7 +849,15 @@ public class LoginActionsService {
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, authSession);
final String identityProviderAlias = brokerContext.getIdpConfig().getAlias();
String flowId = firstBrokerLogin ? brokerContext.getIdpConfig().getFirstBrokerLoginFlowId() : brokerContext.getIdpConfig().getPostBrokerLoginFlowId();
String flowId;
if (firstBrokerLogin) {
flowId = brokerContext.getIdpConfig().getFirstBrokerLoginFlowId();
if (flowId == null) {
flowId = realm.getFirstBrokerLoginFlow().getId();
}
} else {
flowId = brokerContext.getIdpConfig().getPostBrokerLoginFlowId();
}
if (flowId == null) {
ServicesLogger.LOGGER.flowNotConfigForIDP(identityProviderAlias);
String message = "Flow not configured for identity provider";

View file

@ -204,6 +204,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
assertTrue(representation.isEnabled());
assertFalse(representation.isStoreToken());
assertFalse(representation.isTrustEmail());
assertNull(representation.getFirstBrokerLoginFlowAlias());
assertEquals("some secret value", testingClient.testing("admin-client-test").getIdentityProviderConfig("new-identity-provider").get("clientSecret"));
@ -1022,7 +1023,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
Assert.assertEquals("alias", "saml", idp.getAlias());
Assert.assertEquals("providerId", "saml", idp.getProviderId());
Assert.assertEquals("enabled",enabled, idp.isEnabled());
Assert.assertEquals("firstBrokerLoginFlowAlias", "first broker login",idp.getFirstBrokerLoginFlowAlias());
Assert.assertNull("firstBrokerLoginFlowAlias", idp.getFirstBrokerLoginFlowAlias());
assertSamlConfig(idp.getConfig());
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.saml;
import java.util.Map;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
@ -135,15 +136,17 @@ public class BrokerTest extends AbstractSamlTest {
final ClientsResource clients = realm.clients();
AuthenticationExecutionInfoRepresentation reviewProfileAuthenticator = null;
String firstBrokerLoginFlowAlias = null;
final String firstBrokerLoginFlowAlias = UUID.randomUUID().toString();
realm.flows().copy(realm.toRepresentation().getFirstBrokerLoginFlow(), Map.of("newName", firstBrokerLoginFlowAlias)).close();
final IdentityProviderRepresentation rep = addIdentityProvider("https://saml.idp/saml");
rep.setFirstBrokerLoginFlowAlias(firstBrokerLoginFlowAlias);
rep.getConfig().put(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, "undefined");
rep.getConfig().put(SAMLIdentityProviderConfig.PRINCIPAL_TYPE, SamlPrincipalType.ATTRIBUTE.toString());
rep.getConfig().put(SAMLIdentityProviderConfig.PRINCIPAL_ATTRIBUTE, "mail");
try (IdentityProviderCreator idp = new IdentityProviderCreator(realm, rep)) {
IdentityProviderRepresentation idpRepresentation = idp.identityProvider().toRepresentation();
firstBrokerLoginFlowAlias = idpRepresentation.getFirstBrokerLoginFlowAlias();
List<AuthenticationExecutionInfoRepresentation> executions = realm.flows().getExecutions(firstBrokerLoginFlowAlias);
reviewProfileAuthenticator = executions.stream()
.filter(ex -> Objects.equals(ex.getProviderId(), IdpReviewProfileAuthenticatorFactory.PROVIDER_ID))