Skip to content

Commit

Permalink
Added SP functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Nov 13, 2023
1 parent 67d5736 commit 79c4624
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 35 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
### [Usage](#usage)

The main interface of the SAML library is `SAMLService`.
It provides the following functionality:
It provides the following functionality for service / identity providers:
- create an (optionally signed) `org.opensaml.saml.saml2.core.AuthnRequest`
- construct the SP metadata
- parsing SAML to an `org.opensaml.saml.saml2.core.AuthnRequest`
- sending SAML response back to the Service Provider
- construct the IdP metadata
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.openconext</groupId>
<artifactId>saml-idp</artifactId>
<version>0.0.6-SNAPSHOT</version>
<version>0.0.7-SNAPSHOT</version>
<name>saml-idp</name>

<properties>
Expand Down
43 changes: 42 additions & 1 deletion src/main/java/saml/DefaultSAMLService.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.SAMLVersion;
import org.opensaml.saml.common.SignableSAMLObject;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.ext.saml2mdui.Description;
import org.opensaml.saml.ext.saml2mdui.DisplayName;
import org.opensaml.saml.ext.saml2mdui.Logo;
Expand Down Expand Up @@ -72,6 +73,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.opensaml.saml.common.xml.SAMLConstants.SAML20P_NS;
import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_BINDING_URI;
import static saml.parser.EncodingUtils.deflatedBase64encoded;


public class DefaultSAMLService implements SAMLService {
Expand Down Expand Up @@ -206,6 +208,44 @@ public AuthnRequest parseAuthnRequest(String xml, boolean encoded, boolean defla
return authnRequest;
}

@SneakyThrows
@Override
public String createAuthnRequest(SAMLServiceProvider serviceProvider,
String destination,
boolean signRequest,
boolean forceAuthn,
String authnContextClassRef) {
AuthnRequest authnRequest = buildSAMLObject(AuthnRequest.class);
authnRequest.setAssertionConsumerServiceURL(serviceProvider.getAcsLocation());
authnRequest.setDestination(destination);
authnRequest.setForceAuthn(forceAuthn);
authnRequest.setID("A" + UUID.randomUUID());
authnRequest.setIsPassive(false);
authnRequest.setIssueInstant(Instant.now());
authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
authnRequest.setVersion(SAMLVersion.VERSION_20);

Issuer issuer = buildSAMLObject(Issuer.class);
issuer.setValue(serviceProvider.getEntityId());
authnRequest.setIssuer(issuer);

if (StringUtils.isNotEmpty(authnContextClassRef)) {
RequestedAuthnContext requestedAuthnContext = buildSAMLObject(RequestedAuthnContext.class);
AuthnContextClassRef newAuthnContextClassRef = buildSAMLObject(AuthnContextClassRef.class);
newAuthnContextClassRef.setURI(authnContextClassRef);
requestedAuthnContext.getAuthnContextClassRefs().add(newAuthnContextClassRef);
authnRequest.setRequestedAuthnContext(requestedAuthnContext);
}

if (signRequest) {
this.signObject(authnRequest, serviceProvider.getCredential());
}

Element element = XMLObjectSupport.marshall(authnRequest);
String samlAuthnRequest = SerializeSupport.nodeToString(element);
return deflatedBase64encoded(samlAuthnRequest);
}

@Override
@SneakyThrows
public Response parseResponse(String xml) {
Expand Down Expand Up @@ -515,7 +555,8 @@ public SAMLServiceProvider resolveSigningCredential(SAMLServiceProvider serviceP
}

@SneakyThrows
protected String serviceProviderMetaData(SAMLServiceProvider serviceProvider) {
@Override
public String serviceProviderMetaData(SAMLServiceProvider serviceProvider) {
EntityDescriptor entityDescriptor = buildSAMLObject(EntityDescriptor.class);
entityDescriptor.setEntityID(serviceProvider.getEntityId());
entityDescriptor.setID("M" + UUID.randomUUID());
Expand Down
24 changes: 22 additions & 2 deletions src/main/java/saml/SAMLService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package saml;

import lombok.SneakyThrows;
import org.opensaml.saml.saml2.core.*;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.security.credential.Credential;
import saml.model.SAMLAttribute;
import saml.model.SAMLServiceProvider;
import saml.model.SAMLStatus;
Expand All @@ -11,8 +13,17 @@

public interface SAMLService {


AuthnRequest createAuthnRequest(String authnContextClassRef);
/**
* Create an {@link AuthnRequest} and return the XML representation
*
* @param serviceProvider the (e.g. {@link SAMLServiceProvider}) containing the entityID
* @param destination the destination (e.g. singleSignService URL of the IdP)
* @param signRequest will the request be signed. If so, then the {@link Credential} must be present in the SP
* @param forceAuthn do we force a new authentication
* @param authnContextClassRef an optional value for the authnContextClassRef element
* @return deflated and Base64 encoded SAML AuthnRequest
*/
String createAuthnRequest(SAMLServiceProvider serviceProvider, String destination, boolean signRequest, boolean forceAuthn, String authnContextClassRef);

/**
* Parse XML String to {@link Response}
Expand Down Expand Up @@ -73,4 +84,13 @@ void sendResponse(String spEntityID,
* @return the SAMLServiceProvider that may be null
*/
SAMLServiceProvider resolveSigningCredential(SAMLServiceProvider serviceProvider);

/**
* Create SP metaData
*
* @param serviceProvider the (e.g. {@link SAMLServiceProvider}) containing the entityID and certificate
* @return SAML metadata
*/
@SneakyThrows
String serviceProviderMetaData(SAMLServiceProvider serviceProvider);
}
12 changes: 11 additions & 1 deletion src/main/java/saml/parser/EncodingUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
import org.apache.commons.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterOutputStream;

import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.zip.Deflater.DEFLATED;

public class EncodingUtils {

Expand All @@ -32,6 +33,15 @@ private static String inflate(byte[] b) {
return out.toString(UTF_8);
}

public static String deflatedBase64encoded(String input) throws IOException {
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
Deflater deflater = new Deflater(Deflater.DEFLATED, true);
DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, deflater);
deflaterStream.write(input.getBytes(Charset.defaultCharset()));
deflaterStream.finish();
return new String(Base64.encodeBase64(bytesOut.toByteArray()));
}

public static String samlEncode(String s) {
return UN_CHUNKED_ENCODER.encodeToString(s.getBytes(UTF_8));
}
Expand Down
60 changes: 31 additions & 29 deletions src/test/java/saml/DefaultSAMLServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
Expand All @@ -28,8 +27,6 @@
import saml.crypto.KeyStoreLocator;
import saml.model.*;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.KeyStore;
Expand All @@ -40,11 +37,10 @@
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.junit.jupiter.api.Assertions.*;
import static saml.parser.EncodingUtils.deflatedBase64encoded;

class DefaultSAMLServiceTest {

Expand All @@ -54,7 +50,7 @@ class DefaultSAMLServiceTest {

@RegisterExtension
WireMockExtension mockServer = new WireMockExtension(8999);
private DefaultSAMLService samlIdPService;
private DefaultSAMLService defaultSAMLService;

static {
java.security.Security.addProvider(
Expand All @@ -77,7 +73,7 @@ class DefaultSAMLServiceTest {
@BeforeEach
void beforeEach() {
SAMLConfiguration samlConfiguration = getSamlConfiguration(false);
samlIdPService = new DefaultSAMLService(samlConfiguration);
defaultSAMLService = new DefaultSAMLService(samlConfiguration);
}

private String getSPMetaData() {
Expand Down Expand Up @@ -127,8 +123,8 @@ private String samlAuthnRequest() {
private String signedSamlAuthnRequest() {
String samlRequest = samlAuthnRequest();

AuthnRequest authnRequest = samlIdPService.parseAuthnRequest(samlRequest, true, true);
samlIdPService.signObject(authnRequest, signingCredential);
AuthnRequest authnRequest = defaultSAMLService.parseAuthnRequest(samlRequest, true, true);
defaultSAMLService.signObject(authnRequest, signingCredential);

Element element = XMLObjectSupport.marshall(authnRequest);
String xml = SerializeSupport.nodeToString(element);
Expand All @@ -143,20 +139,11 @@ private static String readFile(String path) {
return IOUtils.toString(inputStream, Charset.defaultCharset());
}

private String deflatedBase64encoded(String input) throws IOException {
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
Deflater deflater = new Deflater(Deflater.DEFLATED, true);
DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, deflater);
deflaterStream.write(input.getBytes(Charset.defaultCharset()));
deflaterStream.finish();
return new String(Base64.encodeBase64(bytesOut.toByteArray()));
}

@SneakyThrows
@Test
void parseAuthnRequest() {
String samlRequest = this.samlAuthnRequest();
AuthnRequest authnRequest = samlIdPService.parseAuthnRequest(samlRequest, true, true);
AuthnRequest authnRequest = defaultSAMLService.parseAuthnRequest(samlRequest, true, true);
String uri = authnRequest.getScoping().getRequesterIDs().get(0).getURI();
assertEquals("https://test.surfconext.nl", uri);
}
Expand All @@ -177,7 +164,7 @@ void unknownServiceProvider() {
String samlRequestTemplate = readFile("authn_request.xml");
String samlRequest = String.format(samlRequestTemplate, UUID.randomUUID(), issueFormat.format(new Date()), "https://nope.nl");
String encodedSamlRequest = deflatedBase64encoded(samlRequest);
assertThrows(IllegalArgumentException.class, () -> samlIdPService.parseAuthnRequest(encodedSamlRequest, true, true));
assertThrows(IllegalArgumentException.class, () -> defaultSAMLService.parseAuthnRequest(encodedSamlRequest, true, true));
}

@SneakyThrows
Expand All @@ -187,14 +174,14 @@ void acsLocationInvalid() {
String samlRequest = String.format(samlRequestTemplate, UUID.randomUUID(), issueFormat.format(new Date()), spEntityId);
samlRequest = samlRequest.replace("https://engine.test.surfconext.nl/authentication/sp/consume-assertion", "https://nope");
String encodedSamlRequest = deflatedBase64encoded(samlRequest);
assertThrows(IllegalArgumentException.class, () -> samlIdPService.parseAuthnRequest(encodedSamlRequest, true, true));
assertThrows(IllegalArgumentException.class, () -> defaultSAMLService.parseAuthnRequest(encodedSamlRequest, true, true));
}

@SneakyThrows
@Test
void parseSignedAuthnRequest() {
String authnRequestXML = this.signedSamlAuthnRequest();
AuthnRequest authnRequest = samlIdPService.parseAuthnRequest(authnRequestXML, true, true);
AuthnRequest authnRequest = defaultSAMLService.parseAuthnRequest(authnRequestXML, true, true);

String uri = authnRequest.getScoping().getRequesterIDs().get(0).getURI();
assertEquals("https://test.surfconext.nl", uri);
Expand All @@ -205,7 +192,7 @@ void parseSignedAuthnRequest() {
void sendResponse() {
String inResponseTo = UUID.randomUUID().toString();
MockHttpServletResponse httpServletResponse = new MockHttpServletResponse();
samlIdPService.sendResponse(
defaultSAMLService.sendResponse(
spEntityId,
inResponseTo,
"urn:specified",
Expand All @@ -227,7 +214,7 @@ void sendResponse() {

String samlResponse = document.select("input[name=\"SAMLResponse\"]").first().attr("value");
//Convenient way to make simple assertions
Response response = samlIdPService.parseResponse(samlResponse);
Response response = defaultSAMLService.parseResponse(samlResponse);

String statusCode = response.getStatus().getStatusCode().getValue();
assertEquals(statusCode, "urn:oasis:names:tc:SAML:2.0:status:Success");
Expand Down Expand Up @@ -258,7 +245,7 @@ void sendResponse() {
void sendResponseNoAuthnContext() {
String inResponseTo = UUID.randomUUID().toString();
MockHttpServletResponse httpServletResponse = new MockHttpServletResponse();
samlIdPService.sendResponse(
defaultSAMLService.sendResponse(
spEntityId,
inResponseTo,
"urn:specified",
Expand All @@ -273,7 +260,7 @@ void sendResponseNoAuthnContext() {
Document document = Jsoup.parse(html);
String samlResponse = document.select("input[name=\"SAMLResponse\"]").first().attr("value");
//Convenient way to make simple assertions
Response response = samlIdPService.parseResponse(samlResponse);
Response response = defaultSAMLService.parseResponse(samlResponse);

StatusCode statusCode = response.getStatus().getStatusCode();
StatusCode innerStatusCode = statusCode.getStatusCode();
Expand All @@ -290,7 +277,7 @@ void sendResponseNoAuthnContext() {
@Test
void metadata() {
String singleSignOnServiceURI = "https://single.sign.on";
String metaData = samlIdPService.metaData(
String metaData = defaultSAMLService.metaData(
singleSignOnServiceURI,
"Test",
"Test description",
Expand All @@ -300,7 +287,7 @@ void metadata() {

@Test
void resolveSigningCredential() {
SAMLServiceProvider serviceProvider = samlIdPService.resolveSigningCredential(
SAMLServiceProvider serviceProvider = defaultSAMLService.resolveSigningCredential(
new SAMLServiceProvider(spEntityId, "https://metadata.test.surfconext.nl/sp-metadata.xml")
);
assertEquals("https://engine.test.surfconext.nl/authentication/sp/metadata", serviceProvider.getEntityId());
Expand All @@ -309,9 +296,24 @@ void resolveSigningCredential() {

@Test
void resolveSigningCredentialResilience() {
SAMLServiceProvider serviceProvider = samlIdPService.resolveSigningCredential(
SAMLServiceProvider serviceProvider = defaultSAMLService.resolveSigningCredential(
new SAMLServiceProvider(spEntityId, "https://nope")
);
assertNull(serviceProvider);
}

@Test
void createAuthnRequest() {
SAMLServiceProvider serviceProvider = new SAMLServiceProvider(spEntityId, spEntityId);
serviceProvider.setCredential(signingCredential);
serviceProvider.setAcsLocation("https://engine.test.surfconext.nl/authentication/sp/consume-assertion");

String authnRequestXML = this.defaultSAMLService.createAuthnRequest(serviceProvider,
"https://mujina-idp.test.surfconext.nl/SingleSignOnService",
true, true, "https://refeds.org/profile/mfa");

AuthnRequest authnRequest = this.defaultSAMLService.parseAuthnRequest(authnRequestXML, true, true);
assertEquals(serviceProvider.getEntityId(), authnRequest.getIssuer().getValue());
assertEquals(serviceProvider.getAcsLocation(), authnRequest.getAssertionConsumerServiceURL());
}
}

0 comments on commit 79c4624

Please sign in to comment.