Skip to content

Commit

Permalink
Ability to declare a default "First broker login flow" per Realm
Browse files Browse the repository at this point in the history
Closes keycloak#25823

Signed-off-by: Réda Housni Alaoui <reda-alaoui@hey.com>
Co-authored-by: Jon Koops <jonkoops@gmail.com>
  • Loading branch information
reda-alaoui and jonkoops authored Feb 28, 2024
1 parent 9cced05 commit a3b3ee4
Show file tree
Hide file tree
Showing 21 changed files with 127 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ public class RealmRepresentation {
protected String resetCredentialsFlow;
protected String clientAuthenticationFlow;
protected String dockerAuthenticationFlow;
protected String firstBrokerLoginFlow;

protected Map<String, String> attributes;

Expand Down Expand Up @@ -1328,6 +1329,15 @@ public RealmRepresentation setDockerAuthenticationFlow(final String dockerAuthen
return this;
}

public String getFirstBrokerLoginFlow() {
return firstBrokerLoginFlow;
}

public RealmRepresentation setFirstBrokerLoginFlow(String firstBrokerLoginFlow) {
this.firstBrokerLoginFlow = firstBrokerLoginFlow;
return this;
}

public String getKeycloakVersion() {
return keycloakVersion;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Masthead from "../../Masthead";
const masthead = new Masthead();

export enum LoginFlowOption {
empty = "",
none = "None",
browser = "browser",
directGrant = "direct grant",
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
12 changes: 7 additions & 5 deletions js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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}
Expand All @@ -68,7 +69,7 @@ const LoginFlow = ({
...(defaultValue === ""
? [
<SelectOption key="empty" value="">
{t("none")}
{t(labelForEmpty)}
</SelectOption>,
]
: []),
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,18 @@ public void setDockerAuthenticationFlow(final AuthenticationFlowModel flow) {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -302,6 +303,7 @@ public CachedRealm(Long revision, RealmModel model) {
resetCredentialsFlow = model.getResetCredentialsFlow();
clientAuthenticationFlow = model.getClientAuthenticationFlow();
dockerAuthenticationFlow = model.getDockerAuthenticationFlow();
firstBrokerLoginFlow = model.getFirstBrokerLoginFlow();

model.getComponentsStream().forEach(component ->
componentsByParentAndType.add(component.getParentId() + component.getProviderType(), component)
Expand Down Expand Up @@ -687,6 +689,10 @@ public AuthenticationFlowModel getDockerAuthenticationFlow() {
return dockerAuthenticationFlow;
}

public AuthenticationFlowModel getFirstBrokerLoginFlow() {
return firstBrokerLoginFlow;
}

public List<String> getDefaultGroups() {
return defaultGroups;
}
Expand Down
12 changes: 12 additions & 0 deletions model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -1568,6 +1568,18 @@ public void setDockerAuthenticationFlow(AuthenticationFlowModel flow) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ public interface RealmAttributes {

String ADMIN_EVENTS_EXPIRATION = "adminEventsExpiration";

String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId";

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@

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;
Expand Down Expand Up @@ -60,6 +62,7 @@ private void migrateRealm(KeycloakSession session, RealmModel realm) {
updateUserProfileSettings(session);
updateLdapProviderConfig(session);
createHS512ComponentModelKey(session);
bindFirstBrokerLoginFlow(session);
} finally {
context.setRealm(null);
}
Expand Down Expand Up @@ -103,4 +106,16 @@ private void createHS512ComponentModelKey(KeycloakSession session) {
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,9 @@ public void updateRealm(RealmRepresentation rep, RealmModel realm) {
if (rep.getDockerAuthenticationFlow() != null) {
realm.setDockerAuthenticationFlow(realm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
}
if (rep.getFirstBrokerLoginFlow() != null) {
realm.setFirstBrokerLoginFlow(realm.getFlowByAlias(rep.getFirstBrokerLoginFlow()));
}
}

@Override
Expand Down Expand Up @@ -1363,10 +1366,15 @@ public static Map<String, String> importAuthenticationFlows(KeycloakSession sess
} 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public static Authentication convertToModel(AuthenticationFlowModel flow, RealmM
}

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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ public static void firstBrokerLoginFlow(RealmModel realm, boolean migrate) {
firstBrokerLogin.setTopLevel(true);
firstBrokerLogin.setBuiltIn(true);
firstBrokerLogin = realm.addAuthenticationFlow(firstBrokerLogin);
realm.setFirstBrokerLoginFlow(firstBrokerLogin);

AuthenticatorConfigModel reviewProfileConfig = new AuthenticatorConfigModel();
reviewProfileConfig.setAlias(IDP_REVIEW_PROFILE_CONFIG_ALIAS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ public static boolean isFlowUsed(RealmModel realm, AuthenticationFlowModel model
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()) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -471,6 +473,7 @@ public static RealmRepresentation toRepresentation(KeycloakSession session, Real
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()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -838,21 +838,21 @@ public static IdentityProviderModel toModel(RealmModel realm, IdentityProviderRe
identityProviderModel.setConfig(removeEmptyString(representation.getConfig()));

String flowAlias = representation.getFirstBrokerLoginFlowAlias();
if (flowAlias == null) {
flowAlias = DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW;
}

AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias);
if (flowModel == null) {
throw new ModelException("No available authentication flow with alias: " + flowAlias);
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());
}
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,16 @@ public void setDockerAuthenticationFlow(AuthenticationFlowModel flow) {

}

@Override
public AuthenticationFlowModel getFirstBrokerLoginFlow() {
return null;
}

@Override
public void setFirstBrokerLoginFlow(AuthenticationFlowModel flow) {

}

@Override
public Stream<AuthenticationFlowModel> getAuthenticationFlowsStream() {
return null;
Expand Down
3 changes: 3 additions & 0 deletions server-spi/src/main/java/org/keycloak/models/RealmModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ default Boolean getAttribute(String name, Boolean defaultValue) {
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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,15 @@ protected Response brokerLoginFlow(String authSessionId, String code, String exe
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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ public void testCreate() {
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"));

Expand Down Expand Up @@ -1022,7 +1023,7 @@ private void assertCreatedSamlIdp(IdentityProviderRepresentation idp,boolean ena
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());
}

Expand Down
Loading

0 comments on commit a3b3ee4

Please sign in to comment.