diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..42e4a4a --- /dev/null +++ b/README.adoc @@ -0,0 +1,84 @@ += Grouper External Authentication Plugin + +Grouper plugin that provides configurable authentication. Features include + +* Authentication for UI +* Multiple methods, including SAML2 and OIDC + +== Usage + +=== Version 4.x + +For a fully integrated sample configuration, see `src/test/docker` in the git repo at https://github.internet2.edu/internet2/grouper-ext-auth[] + +. Add plugin to Grouper image ++ +[source, dockerfile] +---- +COPY grouper-authentication-plugin.jar /opt/grouper/plugins +---- + +. Enable Plugins ++ +In `grouper.properties`, add properties ++ +[source, properties] +---- +grouper.osgi.enable = true +grouper.osgi.jar.dir = /opt/grouper/plugins +grouper.osgi.framework.boot.delegation=org.osgi.*,javax.*,org.apache.commons.logging,edu.internet2.middleware.grouperClient.*,edu.internet2.middleware.grouper.*,org.w3c.*,org.xml.*,sun.* + +grouperOsgiPlugin.0.jarName = grouper-authentication-plugin.jar +---- ++ +`grouper.osgi.jar.dir` should point to the directory you copied the file to in your image build file ++ +`grouperOsgiPlugin.0.jarName` should be the name of the file you copied in + +. Configure UI ++ +In `grouper-ui.properties, add properties appropriate for desired authentication. Note that only one can be used. ++ +Most of the configuration for the underlying authentication library is exposed to the Grouper configuration. Any field in the Java classes can be directly set using the field name or a setter used by using a related property (setting `attribute=value` will call `setAttribute(value)` ) + +.. SAML2 ++ +For SAML2, for example: ++ +[source,properties] +---- +external.authentication.provider = saml +external.authentication.saml.identityProviderEntityId = https://idp.unicon.local/idp/shibboleth +external.authentication.saml.serviceProviderEntityId = http://localhost:8080/grouper +external.authentication.saml.serviceProviderMetadataPath = file:/opt/grouper/sp-metadata.xml +external.authentication.saml.identityProviderMetadataPath = file:/opt/grouper/idp-metadata.xml +external.authentication.saml.keystorePath = file:/opt/grouper/here.key +external.authentication.saml.keystorePassword = testme +external.authentication.saml.privateKeyPassword = testme +external.authentication.saml.attributeAsId = urn:oid:0.9.2342.19200300.100.1.1 +---- ++ +For more information and more options, see https://www.pac4j.org/5.7.x/docs/clients/saml.html[] and https://github.com/pac4j/pac4j/blob/5.7.x/pac4j-saml/src/main/java/org/pac4j/saml/config/SAML2Configuration.java[] + +.. OIDC ++ +For OIDC, for example: ++ +[source,properties] +---- +external.authentication.provider = oidc +external.authentication.oidc.clientId = ***** +external.authentication.oidc.discoveryURI = https://unicon.okta.com/.well-known/openid-configuration +external.authentication.oidc.secret = ***** +external.authentication.oidc.claimAsUsername = preferred_username +---- ++ +For more information and more options, see https://www.pac4j.org/5.7.x/docs/clients/openid-connect.html[] and https://github.com/pac4j/pac4j/blob/5.7.x/pac4j-oidc/src/main/java/org/pac4j/oidc/config/OidcConfiguration.java[] + +=== Version 5.x+ + +TODO + +== More Information + +If assistance is needed (e.g., bugs, errors, configuration samples), feel free to open a ticket in the github repository or ask on the Slack channel \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 56ae56c..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# grouper-ext-auth \ No newline at end of file diff --git a/pom.xml b/pom.xml index 634fcef..6bd8a8f 100644 --- a/pom.xml +++ b/pom.xml @@ -32,10 +32,10 @@ bundle - 1.8 - 1.8 - 4.3.1 - 5.0.0 + 17 + 17 + 5.7.1 + 7.1.0 4.7.0 @@ -74,12 +74,12 @@ org.pac4j - jee-pac4j - ${jee-pac4j.version} + javaee-pac4j + ${javaee-pac4j.version} org.pac4j - pac4j-saml-opensamlv3 + pac4j-saml ${pac4j.version} diff --git a/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/ConfigUtils.java b/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/ConfigUtils.java index fb0c971..9573a10 100644 --- a/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/ConfigUtils.java +++ b/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/ConfigUtils.java @@ -1,6 +1,7 @@ package edu.internet2.middleware.grouper.authentication.plugin; import edu.internet2.middleware.grouperClient.config.ConfigPropertiesCascadeBase; +import org.apache.commons.lang3.StringUtils; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; @@ -10,6 +11,8 @@ import org.springframework.core.io.ResourceLoader; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.time.Period; import java.util.Arrays; @@ -17,6 +20,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; public class ConfigUtils { final static ResourceLoader resourceLoader = new DefaultResourceLoader(); @@ -50,23 +55,27 @@ public static void setProperties(BaseClientConfiguration configuration, String a Class clazz = configuration.getClass(); for (String name : grouperConfig.propertyNames()) { if (name.startsWith("external.authentication." + authMechanism)) { + String fieldName = name.substring(name.lastIndexOf('.') + 1); try { - String fieldName = name.substring(name.lastIndexOf('.') + 1); - Field field = getField(clazz, fieldName); - - //TODO: prefer setters - - field.setAccessible(true); - field.set(configuration, getProperty(grouperConfig, field.getType(), name)); - } catch (NoSuchFieldException e) { - throw new IllegalStateException("Unexpected property name: " + name); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Unable to access property name: " + name); + Method method = getSetter(clazz, getMethodNameFromFieldName(fieldName)); + method.invoke(configuration, getProperty(grouperConfig, method.getParameterTypes()[0], name)); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + try { + Field field = getField(clazz, fieldName); + field.setAccessible(true); + field.set(configuration, getProperty(grouperConfig, field.getType(), name)); + } catch (NoSuchFieldException | IllegalAccessException ex) { + throw new RuntimeException("could not set " + fieldName, ex); + } } } } } + private static String getMethodNameFromFieldName(String fieldName) { + return "set" + StringUtils.capitalize(fieldName); + } + private static Field getField(Class clazz, String name) throws NoSuchFieldException { try { return clazz.getDeclaredField(name); @@ -78,6 +87,11 @@ private static Field getField(Class clazz, String name) throws NoSuchFieldExcept } } + private static Method getSetter(Class clazz, String name) throws NoSuchMethodException { + //TODO: this is dangerous. currently there are no overloaded methods, but there could be in the future. need to decide best way to handle this (parameter type precedence?) + return Arrays.stream(clazz.getMethods()).filter(m -> m.getName().equals(name)).findFirst().orElseThrow(NoSuchMethodException::new); + } + private static Object getProperty(ConfigPropertiesCascadeBase configPropertiesCascadeBase, Type type, String propName) { switch (type.getTypeName()) { case "java.lang.String" : { diff --git a/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/client/ClaimAsUsernameOidcClient.java b/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/client/ClaimAsUsernameOidcClient.java index 7cb2c3e..9f20d9d 100644 --- a/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/client/ClaimAsUsernameOidcClient.java +++ b/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/client/ClaimAsUsernameOidcClient.java @@ -4,15 +4,15 @@ import edu.internet2.middleware.grouper.authentication.plugin.oidc.profile.ClaimAsUsernameProfileCreator; import org.pac4j.oidc.client.OidcClient; -public class ClaimAsUsernameOidcClient extends OidcClient { +public class ClaimAsUsernameOidcClient extends OidcClient { public ClaimAsUsernameOidcClient(final ClaimAsUsernameOidcConfiguration claimAsUsernameOidcConfiguration) { super(claimAsUsernameOidcConfiguration); } @Override - protected void clientInit() { + public void init() { this.defaultProfileCreator(new ClaimAsUsernameProfileCreator(this.getConfiguration(), this)); - super.clientInit(); + super.init(); } } \ No newline at end of file diff --git a/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/profile/ClaimAsUsernameProfileCreator.java b/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/profile/ClaimAsUsernameProfileCreator.java index 163c983..2ad5cf7 100644 --- a/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/profile/ClaimAsUsernameProfileCreator.java +++ b/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/profile/ClaimAsUsernameProfileCreator.java @@ -6,17 +6,17 @@ import org.pac4j.oidc.config.OidcConfiguration; import org.pac4j.oidc.profile.creator.OidcProfileCreator; -public class ClaimAsUsernameProfileCreator extends OidcProfileCreator { +public class ClaimAsUsernameProfileCreator extends OidcProfileCreator { public ClaimAsUsernameProfileCreator(OidcConfiguration configuration, OidcClient client) { super(configuration, client); } @Override - protected void internalInit() { + protected void internalInit(boolean forceReinit) { CommonHelper.assertNotNull("claimAsUsername", ((ClaimAsUsernameOidcConfiguration)this.configuration).getClaimAsUsername()); defaultProfileDefinition(new ClaimAsUsernameProfileDefinition(((ClaimAsUsernameOidcConfiguration)this.configuration).getClaimAsUsername())); - super.internalInit(); + super.internalInit(forceReinit); } } \ No newline at end of file diff --git a/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/profile/ClaimAsUsernameProfileDefinition.java b/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/profile/ClaimAsUsernameProfileDefinition.java index 8981c5a..0bb5260 100644 --- a/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/profile/ClaimAsUsernameProfileDefinition.java +++ b/src/main/java/edu/internet2/middleware/grouper/authentication/plugin/oidc/profile/ClaimAsUsernameProfileDefinition.java @@ -2,7 +2,7 @@ import org.pac4j.oidc.profile.OidcProfileDefinition; -public class ClaimAsUsernameProfileDefinition extends OidcProfileDefinition { +public class ClaimAsUsernameProfileDefinition extends OidcProfileDefinition { public ClaimAsUsernameProfileDefinition(final String claimAsUsername) { super(); setProfileFactory(x -> new ClaimAsUsernameProfile(claimAsUsername)); diff --git a/src/test/docker/docker-compose.yml b/src/test/docker/docker-compose.yml index a2ac27f..93fa879 100644 --- a/src/test/docker/docker-compose.yml +++ b/src/test/docker/docker-compose.yml @@ -91,11 +91,11 @@ services: GROUPER_DATABASE_USERNAME: "grouper" GROUPER_DATABASE_PASSWORD: "grouper" GROUPER_MORPHSTRING_ENCRYPT_KEY: "THISISSUPERSECRET!" - GROUPER_AUTO_DDL_UPTOVERSION: "2.6.*" + GROUPER_AUTO_DDL_UPTOVERSION: "4.*.*" GROUPER_RUN_TOMCAT_NOT_SUPERVISOR: "true" GROUPER_UI_CONFIGURATION_EDITOR_SOURCEIPADDRESSES: "0.0.0.0/0" RUN_SHIB_SP: "false" - GROUPER_EXTRA_CATALINA_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:15005" + GROUPER_EXTRA_CATALINA_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:15005" # GROUPER_UI_GROUPER_AUTH: "true" # GROUPERSYSTEM_QUICKSTART_PASS: "letmein7" networks: diff --git a/src/test/docker/grouper/config/grouper-ui.properties b/src/test/docker/grouper/config/grouper-ui.properties index dbb5467..3febaef 100644 --- a/src/test/docker/grouper/config/grouper-ui.properties +++ b/src/test/docker/grouper/config/grouper-ui.properties @@ -7,12 +7,16 @@ external.authentication.grouperContextUrl = https://grouper-ui.unicon.local/grou #external.authentication.oidc.secret = ***** #external.authentication.oidc.claimAsUsername = preferred_username -external.authentication.provider = saml -external.authentication.saml.identityProviderEntityId = https://idp.unicon.local/idp/shibboleth -external.authentication.saml.serviceProviderEntityId = http://localhost:8080/grouper -external.authentication.saml.serviceProviderMetadataResource = file:/opt/grouper/sp-metadata.xml -external.authentication.saml.identityProviderMetadataResource = file:/opt/grouper/idp-metadata.xml -external.authentication.saml.keystoreResource = file:/opt/grouper/here.key -external.authentication.saml.keystorePassword = testme -external.authentication.saml.privateKeyPassword = testme -external.authentication.saml.attributeAsId = urn:oid:0.9.2342.19200300.100.1.1 \ No newline at end of file +#external.authentication.provider = saml +#external.authentication.saml.identityProviderEntityId = https://idp.unicon.local/idp/shibboleth +#external.authentication.saml.serviceProviderEntityId = http://localhost:8080/grouper +#external.authentication.saml.serviceProviderMetadataPath = file:/opt/grouper/sp-metadata.xml +#external.authentication.saml.identityProviderMetadataPath = file:/opt/grouper/idp-metadata.xml +#external.authentication.saml.keystorePath = file:/opt/grouper/here.key +#external.authentication.saml.keystorePassword = testme +#external.authentication.saml.privateKeyPassword = testme +#external.authentication.saml.attributeAsId = urn:oid:0.9.2342.19200300.100.1.1 + +# Note for CAS: you'll need to make sure that the CAS server SSL certificate is available in the trust store +#external.authentication.provider = cas +#external.authentication.cas.loginUrl = https://idp.unicon.local/idp/profile/cas/login \ No newline at end of file diff --git a/src/test/java/edu/internet2/middleware/grouper/authentication/Pac4JConfigFactoryTest.java b/src/test/java/edu/internet2/middleware/grouper/authentication/Pac4JConfigFactoryTest.java index c094936..0a4e739 100644 --- a/src/test/java/edu/internet2/middleware/grouper/authentication/Pac4JConfigFactoryTest.java +++ b/src/test/java/edu/internet2/middleware/grouper/authentication/Pac4JConfigFactoryTest.java @@ -27,7 +27,9 @@ import org.pac4j.oidc.config.OidcConfiguration; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.config.SAML2Configuration; +import org.springframework.core.io.FileSystemResource; +import java.io.IOException; import java.time.Period; import java.util.Arrays; import java.util.Collections; @@ -112,7 +114,7 @@ public void testPac4JConfigFactorCAS() { properties.put("external.authentication.cas.postLogoutUrlParameter","logout"); properties.put("external.authentication.cas.customParams","param1=value1,param2=value2,param3=value3"); properties.put("external.authentication.cas.method","post"); - properties.put("external.authentication.cas.privateKeyPath","http://localhost/key"); + properties.put("external.authentication.cas.privateKeyPath","file:/key"); properties.put("external.authentication.cas.privateKeyAlgorithm","AES"); Pac4jConfigFactory pac4jConfigFactory = new Pac4jConfigFactory(); @@ -191,7 +193,6 @@ public void testPac4JConfigFactorSAML() { properties.put("external.authentication.saml.issuerFormat","urn:oasis:names:tc:SAML:2.0:nameid-format:entity"); properties.put("external.authentication.saml.nameIdPolicyAllowCreate","true"); properties.put("external.authentication.saml.supportedProtocols","urn:oasis:names:tc:SAML:2.0:protocol, urn:oasis:names:tc:SAML:1.0:protocol, urn:oasis:names:tc:SAML:1.1:protocol"); - properties.put("external.authentication.saml.normalizedCertificateName","ringo"); Pac4jConfigFactory pac4jConfigFactory = new Pac4jConfigFactory(); Config config = pac4jConfigFactory.build(); @@ -311,4 +312,24 @@ public void testPac4jForManualProvider() { Assert.assertTrue(true); } + + @Test + public void testPac4jConfigMethodFind() throws IOException { + // external.authentication.saml.identityProviderMetadataPath = file:/opt/grouper/idp-metadata.xml + ConfigPropertiesCascadeBase grouperConfig = ConfigUtils.getConfigPropertiesCascadeBase("ui"); + + grouperConfig.propertiesOverrideMap().clear(); + Map overrides = grouperConfig.propertiesOverrideMap(); + overrides.put("external.authentication.provider","saml"); + overrides.put("external.authentication.grouperContextUrl","localhost"); + overrides.put("external.authentication.callbackUrl","callback"); + overrides.put("external.authentication.saml.identityProviderMetadataPath", "file:/opt/grouper/idp-metadata.xml"); + + Pac4jConfigFactory pac4jConfigFactory = new Pac4jConfigFactory(); + Config config = pac4jConfigFactory.build(); + + SAML2Configuration configuration = ((SAML2Client) config.getClients().getClients().get(0)).getConfiguration(); + + Assert.assertTrue(configuration.getIdentityProviderMetadataResource().isFile() && ((FileSystemResource)configuration.getIdentityProviderMetadataResource()).getPath().equals("/opt/grouper/idp-metadata.xml")); + } } \ No newline at end of file