-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Support for SAML Authentication. (#370)
- Loading branch information
1 parent
8e567e6
commit 88a1912
Showing
13 changed files
with
523 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
configuration.properties |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# SAML client | ||
|
||
## Setup | ||
1. Define properties: define the properties required on SAMLClientConfiguration in the `configuration.properties` file. | ||
Sample available at `bennu-saml-client/src/main/resources/configuration.properties.sample`. | ||
You need to define these properties in the webapp and on | ||
`bennu-saml-client/src/main/resources/configuration.properties`) if you want to generate the metadata | ||
automatically. | ||
|
||
2. Generate Metadata file: run the ManualMetadataGenerator | ||
|
||
3. Add More callbacks: if you need the IDP to callback to more than one location after authentication you | ||
need to change the file generated on the previous step. Search for `AssertionConsumerService` and add similar | ||
entries but with the other urls. | ||
|
||
4. Copy the metadata file with all the `AssertionConsumerService` to the location you specified in the variable | ||
`saml.serviceProviderFinalMetadataLocation`, this file will be made available at your webapp endpoint | ||
`/api/saml-client/metadata` | ||
|
||
5. Configure IDP to fetch metadata from the path `/api/saml-client/metadata` | ||
|
||
IMPORTANT: at this moment filters on fenix are causing problems on the requests, and when they arrive at the saml client | ||
they are broken. To solve this you should add the filter `FixRequestArgumentsForSAMLFilter` to the callback urls |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>org.fenixedu</groupId> | ||
<artifactId>bennu</artifactId> | ||
<version>6.10.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>bennu-saml-client</artifactId> | ||
<name>Bennu SAML Client</name> | ||
|
||
<properties> | ||
<pac4j.version>4.0.3</pac4j.version> | ||
<jax-rs-pac4j.version>2.3.0</jax-rs-pac4j.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.fenixedu</groupId> | ||
<artifactId>bennu-core</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.fenixedu</groupId> | ||
<artifactId>bennu-portal</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.fenixedu</groupId> | ||
<artifactId>fenixedu-commons</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.google.guava</groupId> | ||
<artifactId>guava</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>javax.ws.rs</groupId> | ||
<artifactId>javax.ws.rs-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.pac4j</groupId> | ||
<artifactId>pac4j-saml-opensamlv3</artifactId> | ||
<version>${pac4j.version}</version> | ||
</dependency> | ||
</dependencies> | ||
<repositories> | ||
<repository> | ||
<id>fenixedu-maven-repository</id> | ||
<url>https://repo.fenixedu.org/fenixedu-maven-repository</url> | ||
</repository> | ||
<repository> | ||
<id>central</id> | ||
<url>https://repo1.maven.org/maven2/</url> | ||
</repository> | ||
</repositories> | ||
</project> |
23 changes: 23 additions & 0 deletions
23
bennu-saml-client/src/main/java/org/fenixedu/bennu/saml/client/ManualMetadataGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package org.fenixedu.bennu.saml.client; | ||
|
||
import org.pac4j.saml.client.SAML2Client; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
|
||
public class ManualMetadataGenerator { | ||
public static void main(String[] args) throws IOException { | ||
// Make sure you have the configurations.properties files with the correct variables | ||
SAML2Client client = SAMLClientSDK.getClient(); | ||
client.init(); | ||
String destinationFilePath = | ||
SAMLClientConfiguration.getConfiguration().serviceProviderMetadataGenerationDestinationPath(); | ||
File file = new File(destinationFilePath); | ||
if (file.exists()) { | ||
System.out.println("SAML metadata written to: " + destinationFilePath | ||
+ " If you need more than one saml callback location you need add mode AssertionConsumerService"); | ||
} else { | ||
System.out.println("Failed to write to SAML metadata file: " + destinationFilePath); | ||
} | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
bennu-saml-client/src/main/java/org/fenixedu/bennu/saml/client/SAMLClientConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package org.fenixedu.bennu.saml.client; | ||
|
||
import org.fenixedu.commons.configuration.ConfigurationInvocationHandler; | ||
import org.fenixedu.commons.configuration.ConfigurationManager; | ||
import org.fenixedu.commons.configuration.ConfigurationProperty; | ||
|
||
public class SAMLClientConfiguration { | ||
|
||
@ConfigurationManager(description = "Bennu SAML Client Configuration") | ||
public static interface ConfigurationProperties { | ||
|
||
@ConfigurationProperty(key = "saml.enabled", defaultValue = "false", description = "Whether the SAML client is enabled") | ||
public Boolean samlEnabled(); | ||
|
||
@ConfigurationProperty(key = "saml.keystorePath", description = "Where the keystore for SAML is") | ||
public String keystorePath(); | ||
|
||
@ConfigurationProperty(key = "saml.keystorePassword", description = "The Password for SAML keystore") | ||
public String keystorePassword(); | ||
|
||
@ConfigurationProperty(key = "saml.privateKeyPassword", description = "The Password for SAML private key in the keystore") | ||
public String privateKeyPassword(); | ||
|
||
@ConfigurationProperty(key = "saml.identityProviderMetadataPath", description = "The path to the identity provider metadata") | ||
public String identityProviderMetadataPath(); | ||
|
||
@ConfigurationProperty(key = "saml.serviceProviderMetadataGenerationDestinationPath", description = "The path where to store the automatically generated service provider metadata with only one assertion consumer service") | ||
public String serviceProviderMetadataGenerationDestinationPath(); | ||
|
||
@ConfigurationProperty(key = "saml.serviceProviderFinalMetadataLocation", description = "The path to the final metadata(with all the possible callback/AssertionConsumerService), it probably needs to be generated by hand") | ||
public String serviceProviderFinalMetadataPath(); | ||
|
||
@ConfigurationProperty(key = "saml.serviceProviderEntityId", description = "The id we want this service to have in the SAML") | ||
public String serviceProviderEntityId(); | ||
|
||
@ConfigurationProperty(key = "saml.callbackUrl", description = "The url to where the client is redirected to after logging in the SAML identity provide") | ||
public String callbackUrl(); | ||
} | ||
|
||
public static ConfigurationProperties getConfiguration() { | ||
return ConfigurationInvocationHandler.getConfiguration(ConfigurationProperties.class); | ||
} | ||
|
||
} |
144 changes: 144 additions & 0 deletions
144
bennu-saml-client/src/main/java/org/fenixedu/bennu/saml/client/SAMLClientSDK.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package org.fenixedu.bennu.saml.client; | ||
|
||
import org.opensaml.core.xml.schema.XSAny; | ||
import org.opensaml.core.xml.schema.impl.XSAnyBuilder; | ||
import org.opensaml.saml.common.xml.SAMLConstants; | ||
import org.pac4j.saml.client.SAML2Client; | ||
import org.pac4j.saml.config.SAML2Configuration; | ||
|
||
import javax.xml.namespace.QName; | ||
import java.io.File; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.function.Supplier; | ||
|
||
public class SAMLClientSDK { | ||
|
||
private static final SAML2Configuration CONFIG = getNewDefaultConfiguration(); | ||
|
||
private static final List<String> AUT_CONTEXT_CLASSES_LDAP = | ||
new ArrayList<>(Arrays.asList("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport")); | ||
|
||
private static final List<String> AUT_CONTEXT_CLASSES_GOV = new ArrayList<>( | ||
Arrays.asList("urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI", | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract")); | ||
|
||
// These are the attributes that are necessary for the IDP to work with the GOV IDP | ||
private static final String[] GOV_REQUIRED_ATTRIBUTES = new String[] { "NIC" }; | ||
|
||
private static SAML2Configuration getNewDefaultConfiguration() { | ||
SAML2Configuration config = new SAML2Configuration(SAMLClientConfiguration.getConfiguration().keystorePath(), | ||
SAMLClientConfiguration.getConfiguration().keystorePassword(), | ||
SAMLClientConfiguration.getConfiguration().privateKeyPassword(), | ||
SAMLClientConfiguration.getConfiguration().identityProviderMetadataPath()); | ||
|
||
config.setAuthnRequestBindingType(SAMLConstants.SAML2_REDIRECT_BINDING_URI); | ||
config.setResponseBindingType(SAMLConstants.SAML2_POST_BINDING_URI); | ||
|
||
config.setAuthnRequestSigned(true); | ||
config.setWantsResponsesSigned(true); | ||
config.setMaximumAuthenticationLifetime(3600 * 8); | ||
// custom SP entity ID | ||
config.setServiceProviderEntityId(SAMLClientConfiguration.getConfiguration().serviceProviderEntityId()); | ||
config.setForceServiceProviderMetadataGeneration(true); | ||
|
||
config.setServiceProviderMetadataPath( | ||
new File(SAMLClientConfiguration.getConfiguration().serviceProviderMetadataGenerationDestinationPath()) | ||
.getAbsolutePath()); | ||
return config; | ||
} | ||
|
||
private static SAML2Configuration getNewConfiguration(String[] govAttributesToRequire) { | ||
SAML2Configuration config = getNewDefaultConfiguration(); | ||
|
||
Supplier<List<XSAny>> requestExtensions = () -> { | ||
List<XSAny> extensionList = new ArrayList<>(); | ||
|
||
/* | ||
<samlp:Extensions> | ||
<RequestedAttributes xmlns="http://autenticacao.cartaodecidadao.pt/atributos"> | ||
<RequestedAttribute Name="http://interop.gov.pt/MDC/Cidadao/NIC" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true"/> | ||
</RequestedAttributes> | ||
<FAAALevel xmlns="http://autenticacao.cartaodecidadao.pt/atributos">3</FAAALevel> | ||
</samlp:Extensions> | ||
*/ | ||
|
||
XSAny requestedAttributes = new XSAnyBuilder() | ||
.buildObject(new QName("http://autenticacao.cartaodecidadao.pt/atributos", "RequestedAttributes")); | ||
extensionList.add(requestedAttributes); | ||
|
||
HashSet<String> attributesToRequire = new HashSet<>(Arrays.asList(GOV_REQUIRED_ATTRIBUTES)); | ||
attributesToRequire.addAll(Arrays.asList(govAttributesToRequire)); | ||
|
||
for (String name : attributesToRequire) { | ||
XSAny requestedAttribute = new XSAnyBuilder() | ||
.buildObject(new QName("http://autenticacao.cartaodecidadao.pt/atributos", "RequestedAttribute")); | ||
requestedAttribute.getUnknownAttributes().put(new QName("Name"), "http://interop.gov.pt/MDC/Cidadao/" + name); | ||
requestedAttribute.getUnknownAttributes() | ||
.put(new QName("NameFormat"), "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); | ||
requestedAttribute.getUnknownAttributes().put(new QName("isRequired"), "true"); | ||
requestedAttributes.getUnknownXMLObjects().add(requestedAttribute); | ||
} | ||
|
||
XSAny FAAALevel = | ||
new XSAnyBuilder().buildObject(new QName("http://autenticacao.cartaodecidadao.pt/atributos", "FAAALevel")); | ||
FAAALevel.setTextContent("3"); | ||
extensionList.add(FAAALevel); | ||
|
||
return extensionList; | ||
}; | ||
config.setAuthnRequestExtensions(requestExtensions); | ||
|
||
return config; | ||
} | ||
|
||
private static final SAML2Client CLIENT = new SAML2Client(CONFIG); | ||
|
||
static { | ||
CLIENT.setCallbackUrl(SAMLClientConfiguration.getConfiguration().callbackUrl()); | ||
} | ||
|
||
public static SAML2Client getClient() { | ||
return CLIENT; | ||
} | ||
|
||
public static SAML2Client getClientForGov(String[] govAttributesToRequire, String callbackUrl) { | ||
SAML2Configuration config = getNewConfiguration(govAttributesToRequire); | ||
config.setAuthnContextClassRefs(AUT_CONTEXT_CLASSES_GOV); | ||
config.setComparisonType("exact"); // ignored by SP but needed for pac4f to use context classes | ||
SAML2Client client = new SAML2Client(config); | ||
client.setCallbackUrl(callbackUrl); | ||
return client; | ||
} | ||
|
||
public static SAML2Client getClient(String callbackUrl) { | ||
SAML2Client client = new SAML2Client(CONFIG); | ||
client.setCallbackUrl(callbackUrl); | ||
return client; | ||
} | ||
|
||
public static SAML2Client getClientWithSpecificAuthContextClasses(List<String> authContextClasses, | ||
String[] govAttributesToRequire, String callbackUrl) { | ||
SAML2Configuration config; | ||
if (govAttributesToRequire != null) { | ||
config = getNewConfiguration(govAttributesToRequire); | ||
} else { | ||
config = getNewDefaultConfiguration(); | ||
} | ||
|
||
if (authContextClasses != null) { | ||
config.setAuthnContextClassRefs(authContextClasses); | ||
config.setComparisonType("exact"); // ignored by SP but needed for pac4f to use context classes | ||
} | ||
|
||
SAML2Client client = new SAML2Client(config); | ||
if (callbackUrl != null) { | ||
client.setCallbackUrl(callbackUrl); | ||
} else { | ||
client.setCallbackUrl(SAMLClientConfiguration.getConfiguration().callbackUrl()); | ||
} | ||
return client; | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
bennu-saml-client/src/main/java/org/fenixedu/bennu/saml/client/SAMLLoginProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package org.fenixedu.bennu.saml.client; | ||
|
||
import com.google.common.base.Strings; | ||
import org.fenixedu.bennu.core.security.Authenticate; | ||
import org.fenixedu.bennu.portal.login.LoginProvider; | ||
import org.pac4j.core.context.JEEContext; | ||
import org.pac4j.core.exception.http.HttpAction; | ||
import org.pac4j.core.http.adapter.JEEHttpActionAdapter; | ||
import org.pac4j.saml.client.SAML2Client; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
|
||
public class SAMLLoginProvider implements LoginProvider { | ||
|
||
@Override | ||
public void showLogin(final HttpServletRequest request, final HttpServletResponse response, final String callback) { | ||
Authenticate.logout(request, response); | ||
final JEEContext context = new JEEContext(request, response); | ||
|
||
final SAML2Client client = SAMLClientSDK.getClient(); | ||
|
||
try { | ||
final HttpAction action = client.getRedirectionAction(context).get(); | ||
JEEHttpActionAdapter.INSTANCE.adapt(action, context); | ||
} catch (final HttpAction ex) { | ||
throw new Error(ex); | ||
} | ||
} | ||
|
||
@Override | ||
public String getKey() { | ||
return "saml"; | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return "SAML"; | ||
} | ||
|
||
@Override | ||
public boolean isEnabled() { | ||
return SAMLClientConfiguration.getConfiguration().samlEnabled(); | ||
} | ||
} |
Oops, something went wrong.