From 623b33263b389c0269705449a34d4b957478662e Mon Sep 17 00:00:00 2001 From: Todd Kazakov Date: Thu, 24 Oct 2024 21:51:36 +0300 Subject: [PATCH] [BACK-3225] Add authenticator that looks up SMART IDPs given issuer query param --- .../SMARTIdentityProviderAuthenticator.java | 93 +++++++++++++++++++ ...TIdentityProviderAuthenticatorFactory.java | 79 ++++++++++++++++ ...ycloak.authentication.AuthenticatorFactory | 1 + 3 files changed, 173 insertions(+) create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticator.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticatorFactory.java diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticator.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticator.java new file mode 100644 index 0000000..a638326 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticator.java @@ -0,0 +1,93 @@ +package org.tidepool.keycloak.extensions.authenticator; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.authentication.Authenticator; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.Urls; +import org.keycloak.services.managers.ClientSessionCode; + +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.util.Optional; + +public class SMARTIdentityProviderAuthenticator implements Authenticator { + + private static final Logger LOG = Logger.getLogger(SMARTIdentityProviderAuthenticator.class); + + protected static final String ACCEPTS_PROMPT_NONE = "acceptsPromptNoneForwardFromClient"; + + private static final String ISSUER = "iss"; + + @Override + public void authenticate(AuthenticationFlowContext context) { + String issuer = context.getUriInfo().getQueryParameters().getFirst(ISSUER); + if (issuer == null || issuer.isBlank()) { + LOG.warnf("No issuer set or %s query parameter provided", ISSUER); + context.attempted(); + return; + } + + LOG.infof("Redirecting: %s set to %s", ISSUER, issuer); + redirect(context, issuer); + } + + protected void redirect(AuthenticationFlowContext context, String issuer) { + Optional idp = context.getRealm().getIdentityProvidersStream() + .filter(IdentityProviderModel::isEnabled) + .filter(identityProvider -> identityProvider.getConfig().getOrDefault("issuer", "").equals(issuer)) + .findFirst(); + if (idp.isPresent()) { + String providerId = idp.get().getProviderId(); + + String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode(); + String clientId = context.getAuthenticationSession().getClient().getClientId(); + String tabId = context.getAuthenticationSession().getTabId(); + String clientData = AuthenticationProcessor.getClientData(context.getSession(), context.getAuthenticationSession()); + URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId, clientData, null); + Response response = Response.seeOther(location) + .build(); + + // will forward the request to the IDP with prompt=none if the IDP accepts forwards with prompt=none. + if ("none".equals(context.getAuthenticationSession().getClientNote(OIDCLoginProtocol.PROMPT_PARAM)) && + Boolean.parseBoolean(idp.get().getConfig().get(ACCEPTS_PROMPT_NONE))) { + context.getAuthenticationSession().setAuthNote(AuthenticationProcessor.FORWARDED_PASSIVE_LOGIN, "true"); + } + + LOG.debugf("Redirecting to %s", providerId); + context.forceChallenge(response); + return; + } + + LOG.warnf("Smart issuer %s not found or not enabled for realm", issuer); + context.attempted(); + } + + @Override + public void action(AuthenticationFlowContext context) { + } + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + } + + @Override + public void close() { + } + +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticatorFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticatorFactory.java new file mode 100644 index 0000000..5d479d8 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticatorFactory.java @@ -0,0 +1,79 @@ +package org.tidepool.keycloak.extensions.authenticator; + +import org.keycloak.Config; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.List; + +public class SMARTIdentityProviderAuthenticatorFactory implements AuthenticatorFactory { + protected static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.DISABLED + }; + + public static final String PROVIDER_ID = "smart-identity-provider-redirector"; + + @Override + public String getDisplayType() { + return "SMART Identity Provider Redirector"; + } + + @Override + public String getReferenceCategory() { + return null; + } + + @Override + public boolean isConfigurable() { + return true; + } + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return true; + } + + @Override + public String getHelpText() { + return "Redirects to a SMART Identity Provider specified with issuer query parameter"; + } + + @Override + public List getConfigProperties() { + return List.of(); + } + + @Override + public Authenticator create(KeycloakSession session) { + return new SMARTIdentityProviderAuthenticator(); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return PROVIDER_ID; + } + +} diff --git a/admin/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/admin/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index 4488404..2f7ae0f 100755 --- a/admin/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/admin/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -2,3 +2,4 @@ org.tidepool.keycloak.extensions.authenticator.ConditionUserInContextFactory org.tidepool.keycloak.extensions.authenticator.RedirectToRegistrationPageFactory org.tidepool.keycloak.extensions.authenticator.RegistrationRoleDiscoveryAuthenticatorFactory org.tidepool.keycloak.extensions.authenticator.ResetUserInContextFactory +org.tidepool.keycloak.extensions.authenticator.SMARTIdentityProviderAuthenticatorFactory