From a3b2998be7a4f19776b8c0d41af4801927c7de14 Mon Sep 17 00:00:00 2001 From: Todd Kazakov Date: Thu, 24 Oct 2024 15:27:45 +0300 Subject: [PATCH 1/5] [BACK-3125] Add SMART identity provider --- .../broker/SMARTIdentityProvider.java | 74 +++++++++++++++++++ .../broker/SMARTIdentityProviderConfig.java | 62 ++++++++++++++++ .../broker/SMARTIdentityProviderFactory.java | 67 +++++++++++++++++ ...ak.broker.provider.IdentityProviderFactory | 1 + 4 files changed, 204 insertions(+) create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderConfig.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java create mode 100644 admin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java new file mode 100644 index 0000000..d98ce16 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java @@ -0,0 +1,74 @@ +package org.tidepool.keycloak.extensions.broker; + +import org.keycloak.broker.oidc.OIDCIdentityProvider; +import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; +import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; +import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.models.KeycloakSession; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +public class SMARTIdentityProvider extends OIDCIdentityProvider { + + private static final String[] defaultForwardParameters = {"launch", "aud", "iss"}; + + private final String fhirVersion; + + public SMARTIdentityProvider(KeycloakSession session, SMARTIdentityProviderConfig config) { + super(session, discoverConfig(session, config.getIssuer())); + getConfig().setIssuer(config.getIssuer()); + getConfig().setClientId(config.getClientId()); + getConfig().setClientSecret(config.getClientSecret()); + getConfig().setDefaultScope(config.getScopes()); + getConfig().setAlias(config.getAlias()); + getConfig().setForwardParameters(withDefaultForwardParameters(config.getForwardParameters())); + + fhirVersion = config.getFHIRVersion(); + } + + private static String withDefaultForwardParameters(String params){ + if (params == null) { + params = ""; + } + + HashSet set = new HashSet<>(Arrays.asList(params.split(","))); + set.addAll(Arrays.asList(defaultForwardParameters)); + return String.join(",", set.stream().map(String::trim).toArray(String[]::new)); + } + + private static OIDCIdentityProviderConfig discoverConfig(KeycloakSession session, String issuer) { + OIDCIdentityProviderFactory factory = new OIDCIdentityProviderFactory(); + OIDCIdentityProviderConfig identityProviderConfig = factory.createConfig(); + + if (issuer == null || issuer.isEmpty()) { + return identityProviderConfig; + } + + if (!issuer.endsWith("/")) { + issuer = issuer + "/"; + } + + String smartConfigurationUrl = issuer + ".well-known/smart-configuration"; + SimpleHttp request = SimpleHttp.doGet(smartConfigurationUrl, session).header("Accept", "application/fhir+json"); + + try { + SimpleHttp.Response response = request.asResponse(); + if (response.getStatus() != 200) { + String msg = "failed to invoke url [" + smartConfigurationUrl + "]"; + String tmp = response.asString(); + if (tmp != null) msg = tmp; + + throw new IdentityBrokerException("Failed to invoke url [" + smartConfigurationUrl + "]: " + msg); + } + + identityProviderConfig.setConfig(factory.parseConfig(session, response.asString())); + } catch (IOException e) { + throw new IdentityBrokerException("Unable to retrieve smart configuration"); + } + + return identityProviderConfig; + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderConfig.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderConfig.java new file mode 100644 index 0000000..608366c --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderConfig.java @@ -0,0 +1,62 @@ +package org.tidepool.keycloak.extensions.broker; + +import org.keycloak.models.IdentityProviderModel; + +public class SMARTIdentityProviderConfig extends IdentityProviderModel { + + public SMARTIdentityProviderConfig(IdentityProviderModel identityProviderModel) { + super(identityProviderModel); + } + + public SMARTIdentityProviderConfig() { + super(); + } + + public String getIssuer() { + return getConfig().get("issuer"); + } + + public void setIssuer(String issuer) { + getConfig().put("issuer", issuer); + } + + public String getClientId() { + return getConfig().get("clientId"); + } + + public void setClientId(String clientId) { + getConfig().put("clientId", clientId); + } + + public String getClientSecret() { + return getConfig().get("clientSecret"); + } + + public void setClientSecret(String clientSecret) { + getConfig().put("clientSecret", clientSecret); + } + + public String getScopes() { + return getConfig().get("scopes"); + } + + public void setScopes(String scopes) { + getConfig().put("scopes", scopes); + } + + public String getForwardParameters() { + return getConfig().get("forwardParameters"); + } + + public void setForwardParameters(String params) { + getConfig().put("forwardParameters", params); + } + + public String getFHIRVersion() { + return getConfig().get("fhirVersion"); + } + + public void setFHIRVersion(String version) { + getConfig().put("fhirVersion", version); + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java new file mode 100644 index 0000000..5c83164 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java @@ -0,0 +1,67 @@ +package org.tidepool.keycloak.extensions.broker; + +import org.keycloak.broker.provider.AbstractIdentityProviderFactory; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class SMARTIdentityProviderFactory extends AbstractIdentityProviderFactory { + + public static final String PROVIDER_ID = "smart"; + + public static final String[] SUPPORTED_FHIR_VERSIONS = {"R4"}; + + @Override + public String getName() { + return "SMART"; + } + + @Override + public SMARTIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new SMARTIdentityProvider(session, new SMARTIdentityProviderConfig(model)); + } + + @Override + public IdentityProviderModel createConfig() { + return new SMARTIdentityProviderConfig(); + } + + @Override + public Map parseConfig(KeycloakSession session, String config) { + return parseSMARTConfig(session, config); + } + + protected static Map parseSMARTConfig(KeycloakSession session, String configString) { + OIDCConfigurationRepresentation rep; + try { + rep = JsonSerialization.readValue(configString, OIDCConfigurationRepresentation.class); + } catch (IOException e) { + throw new RuntimeException("failed to load openid connect metadata", e); + } + SMARTIdentityProviderConfig config = new SMARTIdentityProviderConfig(); + config.setIssuer(rep.getIssuer()); + return config.getConfig(); + } + + @Override + public List getConfigProperties() { + return ProviderConfigurationBuilder.create() + .property().name("issuer").label("Issuer").type(ProviderConfigProperty.STRING_TYPE).required(true).add() + .property().name("scopes").label("Scopes").type(ProviderConfigProperty.STRING_TYPE).add() + .property().name("forwardParameters").label("Forward Parameters").type(ProviderConfigProperty.STRING_TYPE).add() + .property().name("fhirVersion").label("FHIR Version").type(ProviderConfigProperty.LIST_TYPE).options(SUPPORTED_FHIR_VERSIONS).defaultValue(SUPPORTED_FHIR_VERSIONS[0]).required(true).add() + .build(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/admin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory b/admin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory new file mode 100644 index 0000000..8f1c4f2 --- /dev/null +++ b/admin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory @@ -0,0 +1 @@ +org.tidepool.keycloak.extensions.broker.SMARTIdentityProviderFactory From 623b33263b389c0269705449a34d4b957478662e Mon Sep 17 00:00:00 2001 From: Todd Kazakov Date: Thu, 24 Oct 2024 21:51:36 +0300 Subject: [PATCH 2/5] [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 From d4724991921945fdd00486e6df2e2105218c2b15 Mon Sep 17 00:00:00 2001 From: Todd Kazakov Date: Thu, 24 Oct 2024 21:53:28 +0300 Subject: [PATCH 3/5] [BACK-3227] Retrieve clinician identity using Practitioner FHIR Resource --- .gitignore | 3 + admin/pom.xml | 32 +++++++++ .../extensions/broker/FHIRContext.java | 14 ++++ .../broker/SMARTIdentityProvider.java | 70 ++++++++++++++++++- .../broker/SMARTIdentityProviderFactory.java | 2 +- docker-compose.yml | 8 +-- 6 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/broker/FHIRContext.java diff --git a/.gitignore b/.gitignore index d91c50a..549ec3b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ themes/base # OSX files .DS_Store + +# Maven shade plugin +admin/dependency-reduced-pom.xml diff --git a/admin/pom.xml b/admin/pom.xml index 2caa011..cde60c0 100644 --- a/admin/pom.xml +++ b/admin/pom.xml @@ -30,6 +30,16 @@ + + ca.uhn.hapi.fhir + hapi-fhir-client + 7.4.5 + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + 7.4.5 + org.keycloak keycloak-server-spi @@ -62,6 +72,28 @@ maven-compiler-plugin ${maven-compiler-plugin.version} + + org.apache.maven.plugins + maven-shade-plugin + 3.4.0 + + + package + + shade + + + + + ca.uhn.hapi.fhir:* + io.opentelemetry:* + org.apache.commons:* + + + + + + diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/FHIRContext.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/FHIRContext.java new file mode 100644 index 0000000..03b7677 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/FHIRContext.java @@ -0,0 +1,14 @@ +package org.tidepool.keycloak.extensions.broker; + +import ca.uhn.fhir.context.FhirContext; + +public class FHIRContext { + // Singleton instance - the creation of this object is expensive + private static final FhirContext R4 = FhirContext.forR4(); + + private FHIRContext() {} + + public static FhirContext getR4() { + return R4; + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java index d98ce16..c11e17b 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java @@ -1,32 +1,96 @@ package org.tidepool.keycloak.extensions.broker; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.client.api.IClientInterceptor; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor; +import org.hl7.fhir.r4.model.ContactPoint; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; +import org.jboss.logging.Logger; import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; +import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.JsonWebToken; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; public class SMARTIdentityProvider extends OIDCIdentityProvider { + private static final Logger LOG = Logger.getLogger(SMARTIdentityProvider.class); private static final String[] defaultForwardParameters = {"launch", "aud", "iss"}; - private final String fhirVersion; + public static final String FHIR_R4 = "R4"; + + private final SMARTIdentityProviderConfig config; public SMARTIdentityProvider(KeycloakSession session, SMARTIdentityProviderConfig config) { super(session, discoverConfig(session, config.getIssuer())); - getConfig().setIssuer(config.getIssuer()); getConfig().setClientId(config.getClientId()); getConfig().setClientSecret(config.getClientSecret()); getConfig().setDefaultScope(config.getScopes()); getConfig().setAlias(config.getAlias()); getConfig().setForwardParameters(withDefaultForwardParameters(config.getForwardParameters())); + getConfig().setDisableUserInfoService(true); + + this.config = config; + } + + @Override + protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException { + BrokeredIdentityContext identity = super.extractIdentity(tokenResponse, accessToken, idToken); + + Practitioner practitioner = getPractitioner(idToken.getSubject(), accessToken); + for (ContactPoint c : practitioner.getTelecom()) { + if (c.getSystem() == ContactPoint.ContactPointSystem.EMAIL && c.getValue() != null && !c.getValue().isEmpty()) { + identity.setEmail(c.getValue()); + break; + } + } + + identity.setFirstName(practitioner.getNameFirstRep().getGivenAsSingleString()); + identity.setLastName(practitioner.getNameFirstRep().getFamily()); + + return identity; + } + + private Practitioner getPractitioner(String id, String accessToken) { + if (!FHIR_R4.equals(config.getFHIRVersion())) { + throw new IdentityBrokerException("Unsupported FHIR Version: " + config.getFHIRVersion()); + } + + FhirContext ctx = FHIRContext.getR4(); + IClientInterceptor authInterceptor = new BearerTokenAuthInterceptor(accessToken); + IGenericClient client = ctx.newRestfulGenericClient(config.getIssuer()); + client.registerInterceptor(authInterceptor); + Practitioner practitioner = client.read().resource(Practitioner.class).withId(id).execute(); + + if (LOG.isTraceEnabled()) { + IParser parser = ctx.newJsonParser(); + String serialized = parser.encodeResourceToString(practitioner); + LOG.tracef("Retrieved practitioner resource: " + serialized); + } + + return practitioner; + } + + private Patient getPatient(String id, String accessToken) { + if (!FHIR_R4.equals(config.getFHIRVersion())) { + throw new IdentityBrokerException("Unsupported FHIR Version: " + config.getFHIRVersion()); + } - fhirVersion = config.getFHIRVersion(); + FhirContext ctx = FHIRContext.getR4(); + IClientInterceptor authInterceptor = new BearerTokenAuthInterceptor(accessToken); + IGenericClient client = ctx.newRestfulGenericClient(config.getIssuer()); + return client.read().resource(Patient.class).withId(id).execute(); } private static String withDefaultForwardParameters(String params){ diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java index 5c83164..e9d6b87 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java @@ -16,7 +16,7 @@ public class SMARTIdentityProviderFactory extends AbstractIdentityProviderFactor public static final String PROVIDER_ID = "smart"; - public static final String[] SUPPORTED_FHIR_VERSIONS = {"R4"}; + public static final String[] SUPPORTED_FHIR_VERSIONS = {SMARTIdentityProvider.FHIR_R4}; @Override public String getName() { diff --git a/docker-compose.yml b/docker-compose.yml index 2e8724f..5fbc9fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3" - volumes: providers: driver: local @@ -48,6 +46,8 @@ services: - ./tidepool-theme:/opt/keycloak/themes/tidepool - providers:/opt/keycloak/providers depends_on: - - providers - - postgres + providers: + condition: service_completed_successfully + postgres: + condition: service_started command: start-dev From a4cc8568ba5cd1b84f36becb21466c0bdf04e7a7 Mon Sep 17 00:00:00 2001 From: Todd Kazakov Date: Tue, 29 Oct 2024 15:32:25 +0200 Subject: [PATCH 4/5] [BACK-3228] Add Patient FHIR resource to session note mapper --- .../SMARTIdentityProviderAuthenticator.java | 27 +++- .../extensions/broker/FHIRContext.java | 17 ++ .../broker/SMARTIdentityProviderFactory.java | 10 +- .../broker/mappers/PatientRepresentation.java | 66 ++++++++ .../mappers/PatientsUserSessionNote.java | 30 ++++ .../PatientsUserSessionNoteMapper.java | 150 ++++++++++++++++++ ...oak.broker.provider.IdentityProviderMapper | 1 + 7 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientRepresentation.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNote.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNoteMapper.java create mode 100644 admin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper 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 index a638326..a378e45 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticator.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/SMARTIdentityProviderAuthenticator.java @@ -2,8 +2,10 @@ import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.Authenticator; +import org.keycloak.events.Errors; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -22,21 +24,36 @@ public class SMARTIdentityProviderAuthenticator implements Authenticator { protected static final String ACCEPTS_PROMPT_NONE = "acceptsPromptNoneForwardFromClient"; - private static final String ISSUER = "iss"; + public static final String CORRELATION_ID = "correlation_id"; @Override public void authenticate(AuthenticationFlowContext context) { - String issuer = context.getUriInfo().getQueryParameters().getFirst(ISSUER); + String issuer = context.getUriInfo().getQueryParameters().getFirst(OIDCLoginProtocol.ISSUER); if (issuer == null || issuer.isBlank()) { - LOG.warnf("No issuer set or %s query parameter provided", ISSUER); - context.attempted(); + LOG.warnf("No issuer %s query parameter provided", OIDCLoginProtocol.ISSUER); + respondWithInvalidRequest(context, OIDCLoginProtocol.ISSUER + "query parameter is required"); return; } - LOG.infof("Redirecting: %s set to %s", ISSUER, issuer); + String correlationId = context.getUriInfo().getQueryParameters().getFirst(CORRELATION_ID); + if (correlationId == null || issuer.isBlank()) { + LOG.warnf("No correlationId %s query parameter provided", CORRELATION_ID); + respondWithInvalidRequest(context, CORRELATION_ID + "query parameter is required"); + return; + } + + context.getAuthenticationSession().setClientNote(CORRELATION_ID, correlationId); + + LOG.infof("Redirecting with correlationId %s: %s set to %s", correlationId, OIDCLoginProtocol.ISSUER, issuer); redirect(context, issuer); } + protected void respondWithInvalidRequest(AuthenticationFlowContext context, String errorMessage) { + context.getEvent().error(Errors.IDENTITY_PROVIDER_ERROR); + Response challenge = context.form().setError("Invalid request: " + errorMessage).createErrorPage(Response.Status.BAD_REQUEST); + context.failure(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challenge, "Invalid request", errorMessage); + } + protected void redirect(AuthenticationFlowContext context, String issuer) { Optional idp = context.getRealm().getIdentityProvidersStream() .filter(IdentityProviderModel::isEnabled) diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/FHIRContext.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/FHIRContext.java index 03b7677..1fabf98 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/FHIRContext.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/FHIRContext.java @@ -1,6 +1,10 @@ package org.tidepool.keycloak.extensions.broker; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IClientInterceptor; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor; +import org.keycloak.broker.provider.IdentityBrokerException; public class FHIRContext { // Singleton instance - the creation of this object is expensive @@ -11,4 +15,17 @@ private FHIRContext() {} public static FhirContext getR4() { return R4; } + + public static IGenericClient getFHIRClient(String version, String baseUrl, String accessToken) { + if (!SMARTIdentityProviderFactory.FHIR_R4.equals(version)) { + throw new IdentityBrokerException("Unsupported FHIR Version: " + version); + } + + FhirContext ctx = FHIRContext.getR4(); + IClientInterceptor authInterceptor = new BearerTokenAuthInterceptor(accessToken); + IGenericClient client = ctx.newRestfulGenericClient(baseUrl); + client.registerInterceptor(authInterceptor); + + return client; + } } diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java index e9d6b87..c5ea749 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProviderFactory.java @@ -1,5 +1,6 @@ package org.tidepool.keycloak.extensions.broker; +import org.keycloak.Config; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -16,7 +17,8 @@ public class SMARTIdentityProviderFactory extends AbstractIdentityProviderFactor public static final String PROVIDER_ID = "smart"; - public static final String[] SUPPORTED_FHIR_VERSIONS = {SMARTIdentityProvider.FHIR_R4}; + public static final String FHIR_R4 = "R4"; + public static final String[] SUPPORTED_FHIR_VERSIONS = {FHIR_R4}; @Override public String getName() { @@ -64,4 +66,10 @@ public List getConfigProperties() { public String getId() { return PROVIDER_ID; } + + @Override + public void init(Config.Scope config) { + // Load the FHIR Context on startup + FHIRContext.getR4(); + } } diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientRepresentation.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientRepresentation.java new file mode 100644 index 0000000..94d6de8 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientRepresentation.java @@ -0,0 +1,66 @@ +package org.tidepool.keycloak.extensions.broker.mappers; + +import org.hl7.fhir.r4.model.Patient; + +import java.time.Instant; + +public class PatientRepresentation { + public String id; + public String firstName; + public String lastName; + public String mrn; + public Long timestamp; + + public PatientRepresentation(Patient patient) { + id = patient.getId(); + firstName = patient.getNameFirstRep().getGivenAsSingleString(); + lastName = patient.getNameFirstRep().getFamily(); + mrn = patient.getIdentifierFirstRep().getValue(); + + // Capture the time when the patient representation was instantiated + // to allow for LRU pruning if needed + timestamp = Instant.now().getEpochSecond(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getMrn() { + return mrn; + } + + public void setMrn(String mrn) { + this.mrn = mrn; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + +} + diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNote.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNote.java new file mode 100644 index 0000000..361245d --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNote.java @@ -0,0 +1,30 @@ +package org.tidepool.keycloak.extensions.broker.mappers; + +import org.hl7.fhir.r4.model.Patient; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; +import java.util.HashMap; + +public class PatientsUserSessionNote extends HashMap { + + public PatientsUserSessionNote() { + super(); + } + + public void addPatient(String correlationId, Patient patient) { + this.put(correlationId, new PatientRepresentation(patient)); + } + + public String serializeAsString() throws IOException { + return JsonSerialization.writeValueAsString(this); + } + + public static PatientsUserSessionNote deserializeFromString(String value) throws IOException { + if (value == null || value.isBlank()) { + return new PatientsUserSessionNote(); + } + + return JsonSerialization.readValue(value, PatientsUserSessionNote.class); + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNoteMapper.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNoteMapper.java new file mode 100644 index 0000000..c1617b1 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNoteMapper.java @@ -0,0 +1,150 @@ +package org.tidepool.keycloak.extensions.broker.mappers; + +import ca.uhn.fhir.rest.client.api.IGenericClient; +import org.hl7.fhir.r4.model.Patient; +import org.jboss.logging.Logger; +import org.keycloak.broker.oidc.OIDCIdentityProvider; +import org.keycloak.broker.provider.AbstractIdentityProviderMapper; +import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderSyncMode; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.services.managers.AuthenticationManager; +import org.tidepool.keycloak.extensions.broker.FHIRContext; +import org.tidepool.keycloak.extensions.broker.SMARTIdentityProvider; +import org.tidepool.keycloak.extensions.broker.SMARTIdentityProviderFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.tidepool.keycloak.extensions.authenticator.SMARTIdentityProviderAuthenticator.CORRELATION_ID; + + +public class PatientsUserSessionNoteMapper extends AbstractIdentityProviderMapper { + + private static final Logger LOG = Logger.getLogger(PatientsUserSessionNoteMapper.class); + + private static final String PATIENTS_NOTE_NAME = "smart/patients"; + + private static final String[] COMPATIBLE_PROVIDERS = { ANY_PROVIDER }; + + private static final List CONFIG_PROPERTIES = new ArrayList<>(); + + private static final Set IDENTITY_PROVIDER_SYNC_MODES = + new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); + + public static final String PROVIDER_ID = "smart-patients-session-note-idp-mapper"; + + @Override + public String[] getCompatibleProviders() { + return COMPATIBLE_PROVIDERS; + } + + @Override + public String getDisplayCategory() { + return "User Session"; + } + + @Override + public String getDisplayType() { + return "Patient User Session Note Mapper"; + } + + @Override + public String getHelpText() { + return "Adds the patient in context to the user session note."; + } + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } + + @Override + public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, + IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + addPatientToSessionNote(session, realm, context); + } + + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, + IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + addPatientToSessionNote(session, realm, context); + } + + private void addPatientToSessionNote(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context) { + String correlationId = context.getAuthenticationSession().getClientNote(CORRELATION_ID); + if (correlationId.isBlank()) { + LOG.warnf("Client correlationId is not defined for brokered user %s", context.getBrokerUserId()); + return; + } + + AccessTokenResponse accessTokenResponse = (AccessTokenResponse) context.getContextData().get(OIDCIdentityProvider.FEDERATED_ACCESS_TOKEN_RESPONSE); + Object patientId = accessTokenResponse.getOtherClaims().getOrDefault("patient", ""); + if (!(patientId instanceof String) || ((String)patientId).isBlank()) { + LOG.warnf("Patient id not found in access token response for brokered user %s", context.getBrokerUserId()); + return; + } + + String fhirVersion = (String) context.getContextData().get(SMARTIdentityProvider.FHIR_VERSION); + String fhirBaseURL = (String) context.getContextData().get(SMARTIdentityProvider.FHIR_BASE_URL); + + IGenericClient client = FHIRContext.getFHIRClient(fhirVersion, fhirBaseURL, accessTokenResponse.getToken()); + Patient patient = client.read().resource(Patient.class).withId((String)patientId).execute(); + + PatientsUserSessionNote patients; + String noteValue; + + // There may be multiple authentication sessions for a single SSO session. + // Retrieve the session notes from the user session, to make sure we are appending + // to the list of patients associated to the SSO session, not to the current auth session. + UserSessionModel userSession = this.getUserSession(session, realm); + if (userSession != null) { + noteValue = userSession.getNote(PATIENTS_NOTE_NAME); + try { + patients = PatientsUserSessionNote.deserializeFromString(noteValue); + } catch (IOException e) { + LOG.warnf("Unable to deserialize patient notes: %s", noteValue); + return; + } + } else { + patients = new PatientsUserSessionNote(); + } + + patients.addPatient(correlationId, patient); + try { + context.setSessionNote(PATIENTS_NOTE_NAME, patients.serializeAsString()); + } catch (IOException e) { + LOG.warnf("Unable to serialize patient notes: %s", e.getMessage()); + } + } + + private UserSessionModel getUserSession(KeycloakSession session, RealmModel realm) { + AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, true); + if (authResult == null) { + return null; + } + return authResult.getSession(); + } +} + diff --git a/admin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/admin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper new file mode 100644 index 0000000..7ac29d6 --- /dev/null +++ b/admin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper @@ -0,0 +1 @@ +org.tidepool.keycloak.extensions.broker.mappers.PatientsUserSessionNoteMapper From bbc911adb3184a2dda0f4a28873aeda3f80c37b4 Mon Sep 17 00:00:00 2001 From: Todd Kazakov Date: Tue, 29 Oct 2024 15:35:46 +0200 Subject: [PATCH 5/5] [BACK-3125] Address SMART IDP code review feedback and add small improvements --- .../broker/SMARTIdentityProvider.java | 53 +++++++------------ .../PatientsUserSessionNoteMapper.java | 2 - 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java index c11e17b..4fa3bf9 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/SMARTIdentityProvider.java @@ -1,12 +1,8 @@ package org.tidepool.keycloak.extensions.broker; -import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.client.api.IClientInterceptor; import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor; import org.hl7.fhir.r4.model.ContactPoint; -import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.jboss.logging.Logger; import org.keycloak.broker.oidc.OIDCIdentityProvider; @@ -26,22 +22,23 @@ public class SMARTIdentityProvider extends OIDCIdentityProvider { private static final Logger LOG = Logger.getLogger(SMARTIdentityProvider.class); - private static final String[] defaultForwardParameters = {"launch", "aud", "iss"}; + public static final String FHIR_VERSION = "smart/fhir_version"; + public static final String FHIR_BASE_URL = "smart/fhir_base_url"; - public static final String FHIR_R4 = "R4"; + private static final String[] DEFAULT_FORWARD_PARAMETERS = {"launch", "aud", "iss"}; private final SMARTIdentityProviderConfig config; public SMARTIdentityProvider(KeycloakSession session, SMARTIdentityProviderConfig config) { super(session, discoverConfig(session, config.getIssuer())); + + this.config = config; getConfig().setClientId(config.getClientId()); getConfig().setClientSecret(config.getClientSecret()); getConfig().setDefaultScope(config.getScopes()); getConfig().setAlias(config.getAlias()); getConfig().setForwardParameters(withDefaultForwardParameters(config.getForwardParameters())); getConfig().setDisableUserInfoService(true); - - this.config = config; } @Override @@ -50,7 +47,7 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo Practitioner practitioner = getPractitioner(idToken.getSubject(), accessToken); for (ContactPoint c : practitioner.getTelecom()) { - if (c.getSystem() == ContactPoint.ContactPointSystem.EMAIL && c.getValue() != null && !c.getValue().isEmpty()) { + if (c.getSystem() == ContactPoint.ContactPointSystem.EMAIL && c.getValue() != null && !c.getValue().isBlank()) { identity.setEmail(c.getValue()); break; } @@ -59,47 +56,31 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo identity.setFirstName(practitioner.getNameFirstRep().getGivenAsSingleString()); identity.setLastName(practitioner.getNameFirstRep().getFamily()); + identity.getContextData().put(FHIR_VERSION, config.getFHIRVersion()); + identity.getContextData().put(FHIR_BASE_URL, config.getIssuer()); + return identity; } private Practitioner getPractitioner(String id, String accessToken) { - if (!FHIR_R4.equals(config.getFHIRVersion())) { - throw new IdentityBrokerException("Unsupported FHIR Version: " + config.getFHIRVersion()); - } - - FhirContext ctx = FHIRContext.getR4(); - IClientInterceptor authInterceptor = new BearerTokenAuthInterceptor(accessToken); - IGenericClient client = ctx.newRestfulGenericClient(config.getIssuer()); - client.registerInterceptor(authInterceptor); + IGenericClient client = FHIRContext.getFHIRClient(config.getFHIRVersion(), config.getIssuer(), accessToken); Practitioner practitioner = client.read().resource(Practitioner.class).withId(id).execute(); if (LOG.isTraceEnabled()) { - IParser parser = ctx.newJsonParser(); - String serialized = parser.encodeResourceToString(practitioner); - LOG.tracef("Retrieved practitioner resource: " + serialized); + IParser parser = FHIRContext.getR4().newJsonParser(); + LOG.tracef("Retrieved practitioner resource: %s", parser.encodeResourceToString(practitioner)); } return practitioner; } - private Patient getPatient(String id, String accessToken) { - if (!FHIR_R4.equals(config.getFHIRVersion())) { - throw new IdentityBrokerException("Unsupported FHIR Version: " + config.getFHIRVersion()); - } - - FhirContext ctx = FHIRContext.getR4(); - IClientInterceptor authInterceptor = new BearerTokenAuthInterceptor(accessToken); - IGenericClient client = ctx.newRestfulGenericClient(config.getIssuer()); - return client.read().resource(Patient.class).withId(id).execute(); - } - private static String withDefaultForwardParameters(String params){ if (params == null) { params = ""; } HashSet set = new HashSet<>(Arrays.asList(params.split(","))); - set.addAll(Arrays.asList(defaultForwardParameters)); + set.addAll(Arrays.asList(DEFAULT_FORWARD_PARAMETERS)); return String.join(",", set.stream().map(String::trim).toArray(String[]::new)); } @@ -121,11 +102,13 @@ private static OIDCIdentityProviderConfig discoverConfig(KeycloakSession session try { SimpleHttp.Response response = request.asResponse(); if (response.getStatus() != 200) { - String msg = "failed to invoke url [" + smartConfigurationUrl + "]"; + String detail = String.format("Unexpected response %d", response.getStatus()); String tmp = response.asString(); - if (tmp != null) msg = tmp; + if (tmp != null) detail = tmp; + + String msg = String.format("Failed to invoke url [%s]: %s", smartConfigurationUrl, detail) ; - throw new IdentityBrokerException("Failed to invoke url [" + smartConfigurationUrl + "]: " + msg); + throw new IdentityBrokerException(msg); } identityProviderConfig.setConfig(factory.parseConfig(session, response.asString())); diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNoteMapper.java b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNoteMapper.java index c1617b1..ec50386 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNoteMapper.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/broker/mappers/PatientsUserSessionNoteMapper.java @@ -12,13 +12,11 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.managers.AuthenticationManager; import org.tidepool.keycloak.extensions.broker.FHIRContext; import org.tidepool.keycloak.extensions.broker.SMARTIdentityProvider; -import org.tidepool.keycloak.extensions.broker.SMARTIdentityProviderFactory; import java.io.IOException; import java.util.ArrayList;