diff --git a/.gitignore b/.gitignore
index 32858aa..c9afe86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,5 +8,7 @@
*.war
*.ear
+# Maven target #
+target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..6b3a20c
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,52 @@
+
+
+ 4.0.0
+
+ no.steras.opensamlbook
+ webprofile-ref-project
+ 1.0-SNAPSHOT
+ war
+
+
+
+ org.opensaml
+ opensaml
+ 2.6.0
+
+
+ org.apache.xerces
+ xercesImpl
+
+
+
+
+ xerces
+ xercesImpl
+ 2.10.0
+
+
+ ch.qos.logback
+ logback-core
+ 1.0.13
+
+
+ ch.qos.logback
+ logback-classic
+ 1.0.13
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+
+
+ Shibboleth repo
+ https://build.shibboleth.net/nexus/content/repositories/releases
+
+
+
\ No newline at end of file
diff --git a/src/main/java/no/steras/opensamlbook/OpenSAMLUtils.java b/src/main/java/no/steras/opensamlbook/OpenSAMLUtils.java
new file mode 100644
index 0000000..6d18b1a
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/OpenSAMLUtils.java
@@ -0,0 +1,101 @@
+package no.steras.opensamlbook;
+
+import org.opensaml.common.impl.SecureRandomIdentifierGenerator;
+import org.opensaml.ws.soap.soap11.Body;
+import org.opensaml.ws.soap.soap11.Envelope;
+import org.opensaml.xml.Configuration;
+import org.opensaml.xml.XMLObject;
+import org.opensaml.xml.XMLObjectBuilderFactory;
+import org.opensaml.xml.io.Marshaller;
+import org.opensaml.xml.io.MarshallingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.StringWriter;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Created by Privat on 4/6/14.
+ */
+public class OpenSAMLUtils {
+ private static Logger logger = LoggerFactory.getLogger(OpenSAMLUtils.class);
+ private static SecureRandomIdentifierGenerator secureRandomIdGenerator;
+
+ static {
+ try {
+ secureRandomIdGenerator = new SecureRandomIdentifierGenerator();
+ } catch (NoSuchAlgorithmException e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ public static T buildSAMLObject(final Class clazz) {
+ T object = null;
+ try {
+ XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory();
+ QName defaultElementName = (QName)clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null);
+ object = (T)builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName);
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException("Could not create SAML object");
+ } catch (NoSuchFieldException e) {
+ throw new IllegalArgumentException("Could not create SAML object");
+ }
+
+ return object;
+ }
+
+ public static String generateSecureRandomId() {
+ return secureRandomIdGenerator.generateIdentifier();
+ }
+
+ public static void logSAMLObject(final XMLObject object) {
+ try {
+ DocumentBuilder builder;
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+
+ builder = factory.newDocumentBuilder();
+
+ Document document = builder.newDocument();
+ Marshaller out = Configuration.getMarshallerFactory().getMarshaller(object);
+ out.marshall(object, document);
+
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ StreamResult result = new StreamResult(new StringWriter());
+ DOMSource source = new DOMSource(document);
+ transformer.transform(source, result);
+ String xmlString = result.getWriter().toString();
+
+ logger.info(xmlString);
+ } catch (ParserConfigurationException e) {
+ logger.error(e.getMessage(), e);
+ } catch (MarshallingException e) {
+ logger.error(e.getMessage(), e);
+ } catch (TransformerException e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ public static Envelope wrapInSOAPEnvelope(final XMLObject xmlObject) throws IllegalAccessException {
+ Envelope envelope = OpenSAMLUtils.buildSAMLObject(Envelope.class);
+ Body body = OpenSAMLUtils.buildSAMLObject(Body.class);
+
+ body.getUnknownXMLObjects().add(xmlObject);
+
+ envelope.setBody(body);
+
+ return envelope;
+ }
+}
diff --git a/src/main/java/no/steras/opensamlbook/app/ApplicationServlet.java b/src/main/java/no/steras/opensamlbook/app/ApplicationServlet.java
new file mode 100644
index 0000000..977ae34
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/app/ApplicationServlet.java
@@ -0,0 +1,19 @@
+package no.steras.opensamlbook.app;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * This servlet acts as the resource that the access filter is protecting
+ */
+public class ApplicationServlet extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setContentType("text/html");
+ resp.getWriter().append("You are now at the requested resource
");
+ resp.getWriter().append("This is the protected resource. You are authenticated");
+ }
+}
diff --git a/src/main/java/no/steras/opensamlbook/idp/ArtifactResolutionServlet.java b/src/main/java/no/steras/opensamlbook/idp/ArtifactResolutionServlet.java
new file mode 100644
index 0000000..53bd8a8
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/idp/ArtifactResolutionServlet.java
@@ -0,0 +1,331 @@
+package no.steras.opensamlbook.idp;
+
+import no.steras.opensamlbook.OpenSAMLUtils;
+import no.steras.opensamlbook.sp.SPConstants;
+import no.steras.opensamlbook.sp.SPCredentials;
+import org.apache.xml.security.utils.EncryptionConstants;
+import org.joda.time.DateTime;
+import org.opensaml.common.SAMLObject;
+import org.opensaml.common.impl.SecureRandomIdentifierGenerator;
+import org.opensaml.saml2.core.*;
+import org.opensaml.saml2.encryption.Encrypter;
+import org.opensaml.saml2.metadata.provider.MetadataProviderException;
+import org.opensaml.security.SAMLSignatureProfileValidator;
+import org.opensaml.ws.soap.soap11.Body;
+import org.opensaml.ws.soap.soap11.Envelope;
+import org.opensaml.xml.Configuration;
+import org.opensaml.xml.XMLObject;
+import org.opensaml.xml.encryption.EncryptionException;
+import org.opensaml.xml.encryption.EncryptionParameters;
+import org.opensaml.xml.encryption.KeyEncryptionParameters;
+import org.opensaml.xml.io.*;
+import org.opensaml.xml.parse.BasicParserPool;
+import org.opensaml.xml.parse.XMLParserException;
+import org.opensaml.xml.schema.XSString;
+import org.opensaml.xml.schema.impl.XSStringBuilder;
+import org.opensaml.xml.security.keyinfo.KeyInfoGeneratorFactory;
+import org.opensaml.xml.signature.*;
+import org.opensaml.xml.validation.ValidationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.ECField;
+
+/**
+ * Created by Privat on 4/6/14.
+ */
+public class ArtifactResolutionServlet extends HttpServlet {
+ private static Logger logger = LoggerFactory.getLogger(ArtifactResolutionServlet.class);
+
+ @Override
+ protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ ArtifactResponse artifactResponse = buildArtifactResponse();
+ artifactResponse.setInResponseTo("Made up ID");
+
+ printSAMLObject(wrapInSOAPEnvelope(artifactResponse), resp.getWriter());
+ }
+
+ public static ArtifactResolve unmarshallArtifactResolve(final InputStream input) {
+ try {
+ BasicParserPool ppMgr = new BasicParserPool();
+ ppMgr.setNamespaceAware(true);
+
+ Document soap = ppMgr.parse(input);
+
+ Element soapRoot = soap.getDocumentElement();
+
+ UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
+ Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(soapRoot);
+
+ Envelope soapEnvelope = (Envelope)unmarshaller.unmarshall(soapRoot);
+
+ return (ArtifactResolve)soapEnvelope.getBody().getUnknownXMLObjects().get(0);
+ } catch (XMLParserException e) {
+ throw new RuntimeException(e);
+ } catch (UnmarshallingException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ public static org.w3c.dom.Element marshallSAMLObject(final SAMLObject object) {
+ org.w3c.dom.Element element = null;
+ try {
+ MarshallerFactory unMarshallerFactory = Configuration.getMarshallerFactory();
+
+ Marshaller marshaller = unMarshallerFactory.getMarshaller(object);
+
+ element = marshaller.marshall(object);
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException("The class does not implement the interface XMLObject", e);
+ } catch (MarshallingException e) {
+ throw new RuntimeException(e);
+ }
+
+ return element;
+ }
+
+ private ArtifactResponse buildArtifactResponse() {
+
+ ArtifactResponse artifactResponse = OpenSAMLUtils.buildSAMLObject(ArtifactResponse.class);
+
+ Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
+ issuer.setValue(IDPConstants.IDP_ENTITY_ID);
+ artifactResponse.setIssuer(issuer);
+ artifactResponse.setIssueInstant(new DateTime());
+ artifactResponse.setDestination(SPConstants.ASSERTION_CONSUMER_SERVICE);
+
+ artifactResponse.setID(OpenSAMLUtils.generateSecureRandomId());
+
+ Status status = OpenSAMLUtils.buildSAMLObject(Status.class);
+ StatusCode statusCode = OpenSAMLUtils.buildSAMLObject(StatusCode.class);
+ statusCode.setValue(StatusCode.SUCCESS_URI);
+ status.setStatusCode(statusCode);
+ artifactResponse.setStatus(status);
+
+ Response response = OpenSAMLUtils.buildSAMLObject(Response.class);
+ response.setDestination(SPConstants.ASSERTION_CONSUMER_SERVICE);
+ response.setIssueInstant(new DateTime());
+ response.setID(OpenSAMLUtils.generateSecureRandomId());
+ Issuer issuer2 = OpenSAMLUtils.buildSAMLObject(Issuer.class);
+ issuer2.setValue(IDPConstants.IDP_ENTITY_ID);
+
+ response.setIssuer(issuer2);
+
+ Status status2 = OpenSAMLUtils.buildSAMLObject(Status.class);
+ StatusCode statusCode2 = OpenSAMLUtils.buildSAMLObject(StatusCode.class);
+ statusCode2.setValue(StatusCode.SUCCESS_URI);
+ status2.setStatusCode(statusCode2);
+
+ response.setStatus(status2);
+
+ artifactResponse.setMessage(response);
+
+ Assertion assertion = buildAssertion();
+
+ signAssertion(assertion);
+ EncryptedAssertion encryptedAssertion = encryptAssertion(assertion);
+
+ response.getEncryptedAssertions().add(encryptedAssertion);
+ return artifactResponse;
+ }
+
+ private EncryptedAssertion encryptAssertion(Assertion assertion) {
+ EncryptionParameters encryptionParameters = new EncryptionParameters();
+ encryptionParameters.setAlgorithm(EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128);
+
+ KeyEncryptionParameters keyEncryptionParameters = new KeyEncryptionParameters();
+ keyEncryptionParameters.setEncryptionCredential(SPCredentials.getCredential());
+ keyEncryptionParameters.setAlgorithm(EncryptionConstants.ALGO_ID_KEYTRANSPORT_RSAOAEP);
+
+ Encrypter encrypter = new Encrypter(encryptionParameters, keyEncryptionParameters);
+ encrypter.setKeyPlacement(Encrypter.KeyPlacement.INLINE);
+
+ try {
+ EncryptedAssertion encryptedAssertion = encrypter.encrypt(assertion);
+ return encryptedAssertion;
+ } catch (EncryptionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void signAssertion(Assertion assertion) {
+ Signature signature = OpenSAMLUtils.buildSAMLObject(Signature.class);
+ signature.setSigningCredential(IDPCredentials.getCredential());
+ signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
+ signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
+
+ assertion.setSignature(signature);
+
+ try {
+ Configuration.getMarshallerFactory().getMarshaller(assertion).marshall(assertion);
+ } catch (MarshallingException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ Signer.signObject(signature);
+ } catch (SignatureException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Assertion buildAssertion() {
+
+ Assertion assertion = OpenSAMLUtils.buildSAMLObject(Assertion.class);
+
+ Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
+ issuer.setValue(IDPConstants.IDP_ENTITY_ID);
+ assertion.setIssuer(issuer);
+ assertion.setIssueInstant(new DateTime());
+
+ assertion.setID(OpenSAMLUtils.generateSecureRandomId());
+
+ Subject subject = OpenSAMLUtils.buildSAMLObject(Subject.class);
+ assertion.setSubject(subject);
+
+ NameID nameID = OpenSAMLUtils.buildSAMLObject(NameID.class);
+ nameID.setFormat(NameIDType.TRANSIENT);
+ nameID.setValue("Some NameID value");
+ nameID.setSPNameQualifier("SP name qualifier");
+ nameID.setNameQualifier("Name qualifier");
+
+ subject.setNameID(nameID);
+
+ subject.getSubjectConfirmations().add(buildSubjectConfirmation());
+
+ assertion.setConditions(buildConditions());
+
+ assertion.getAttributeStatements().add(buildAttributeStatement());
+
+ assertion.getAuthnStatements().add(buildAuthnStatement());
+
+ return assertion;
+ }
+
+ private SubjectConfirmation buildSubjectConfirmation() {
+ SubjectConfirmation subjectConfirmation = OpenSAMLUtils.buildSAMLObject(SubjectConfirmation.class);
+ subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER);
+
+ SubjectConfirmationData subjectConfirmationData = OpenSAMLUtils.buildSAMLObject(SubjectConfirmationData.class);
+ subjectConfirmationData.setInResponseTo("Made up ID");
+ subjectConfirmationData.setNotBefore(new DateTime().minusDays(2));
+ subjectConfirmationData.setNotOnOrAfter(new DateTime().plusDays(2));
+ subjectConfirmationData.setRecipient(SPConstants.ASSERTION_CONSUMER_SERVICE);
+
+ subjectConfirmation.setSubjectConfirmationData(subjectConfirmationData);
+
+ return subjectConfirmation;
+ }
+
+ private AuthnStatement buildAuthnStatement() {
+ AuthnStatement authnStatement = OpenSAMLUtils.buildSAMLObject(AuthnStatement.class);
+ AuthnContext authnContext = OpenSAMLUtils.buildSAMLObject(AuthnContext.class);
+ AuthnContextClassRef authnContextClassRef = OpenSAMLUtils.buildSAMLObject(AuthnContextClassRef.class);
+ authnContextClassRef.setAuthnContextClassRef(AuthnContext.SMARTCARD_AUTHN_CTX);
+ authnContext.setAuthnContextClassRef(authnContextClassRef);
+ authnStatement.setAuthnContext(authnContext);
+
+ authnStatement.setAuthnInstant(new DateTime());
+
+ return authnStatement;
+ }
+
+ private Conditions buildConditions() {
+ Conditions conditions = OpenSAMLUtils.buildSAMLObject(Conditions.class);
+ conditions.setNotBefore(new DateTime().minusDays(2));
+ conditions.setNotOnOrAfter(new DateTime().plusDays(2));
+ AudienceRestriction audienceRestriction = OpenSAMLUtils.buildSAMLObject(AudienceRestriction.class);
+ Audience audience = OpenSAMLUtils.buildSAMLObject(Audience.class);
+ audience.setAudienceURI(SPConstants.ASSERTION_CONSUMER_SERVICE);
+ audienceRestriction.getAudiences().add(audience);
+ conditions.getAudienceRestrictions().add(audienceRestriction);
+ return conditions;
+ }
+
+ private AttributeStatement buildAttributeStatement() {
+ AttributeStatement attributeStatement = OpenSAMLUtils.buildSAMLObject(AttributeStatement.class);
+
+ Attribute attributeUserName = OpenSAMLUtils.buildSAMLObject(Attribute.class);
+
+ XSStringBuilder stringBuilder = (XSStringBuilder)Configuration.getBuilderFactory().getBuilder(XSString.TYPE_NAME);
+ XSString userNameValue = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
+ userNameValue.setValue("bob");
+
+ attributeUserName.getAttributeValues().add(userNameValue);
+ attributeUserName.setName("username");
+ attributeStatement.getAttributes().add(attributeUserName);
+
+ Attribute attributeLevel = OpenSAMLUtils.buildSAMLObject(Attribute.class);
+ XSString levelValue = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
+ levelValue.setValue("999999999");
+
+ attributeLevel.getAttributeValues().add(levelValue);
+ attributeLevel.setName("telephone");
+ attributeStatement.getAttributes().add(attributeLevel);
+
+ return attributeStatement;
+
+ }
+
+ public static Envelope wrapInSOAPEnvelope(final XMLObject xmlObject) {
+ Envelope envelope = OpenSAMLUtils.buildSAMLObject(Envelope.class);
+ Body body = OpenSAMLUtils.buildSAMLObject(Body.class);
+
+ body.getUnknownXMLObjects().add(xmlObject);
+
+ envelope.setBody(body);
+
+ return envelope;
+ }
+
+
+ public static void printSAMLObject(final XMLObject object, final PrintWriter writer) {
+ try {
+ DocumentBuilder builder;
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+
+ builder = factory.newDocumentBuilder();
+
+ org.w3c.dom.Document document = builder.newDocument();
+ Marshaller out = Configuration.getMarshallerFactory().getMarshaller(object);
+ out.marshall(object, document);
+
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ StreamResult result = new StreamResult(writer);
+ DOMSource source = new DOMSource(document);
+ transformer.transform(source, result);
+ } catch (ParserConfigurationException e) {
+ e.printStackTrace();
+ } catch (MarshallingException e) {
+ e.printStackTrace();
+ } catch (TransformerException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/src/main/java/no/steras/opensamlbook/idp/IDPConstants.java b/src/main/java/no/steras/opensamlbook/idp/IDPConstants.java
new file mode 100644
index 0000000..92572ee
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/idp/IDPConstants.java
@@ -0,0 +1,10 @@
+package no.steras.opensamlbook.idp;
+
+/**
+ * Created by Privat on 4/7/14.
+ */
+public class IDPConstants {
+ public static final String IDP_ENTITY_ID = "TestIDP";
+ public static final String SSO_SERVICE = "http://localhost:8080/webprofile-ref-project/idp/singleSignOnService";
+ public static final String ARTIFACT_RESOLUTION_SERVICE = "http://localhost:8080/webprofile-ref-project/idp/artifactResolutionService";
+}
diff --git a/src/main/java/no/steras/opensamlbook/idp/IDPCredentials.java b/src/main/java/no/steras/opensamlbook/idp/IDPCredentials.java
new file mode 100644
index 0000000..3307f3f
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/idp/IDPCredentials.java
@@ -0,0 +1,45 @@
+package no.steras.opensamlbook.idp;
+
+import org.opensaml.xml.security.*;
+import org.opensaml.xml.security.credential.BasicCredential;
+import org.opensaml.xml.security.credential.Credential;
+import org.opensaml.xml.security.credential.KeyStoreCredentialResolver;
+import org.opensaml.xml.security.credential.UsageType;
+import org.opensaml.xml.security.criteria.EntityIDCriteria;
+import org.opensaml.xml.security.x509.X509Credential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.*;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by Privat on 13/05/14.
+ */
+public class IDPCredentials {
+ private static final Credential credential;
+
+ static {
+ credential = generateCredential();
+ }
+
+ private static Credential generateCredential() {
+ try {
+ KeyPair keyPair = SecurityHelper.generateKeyPair("RSA", 1024, null);
+ return SecurityHelper.getSimpleCredential(keyPair.getPublic(), keyPair.getPrivate());
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchProviderException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Credential getCredential() {
+ return credential;
+ }
+}
diff --git a/src/main/java/no/steras/opensamlbook/idp/SingleSignOnServlet.java b/src/main/java/no/steras/opensamlbook/idp/SingleSignOnServlet.java
new file mode 100644
index 0000000..c0668ba
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/idp/SingleSignOnServlet.java
@@ -0,0 +1,37 @@
+package no.steras.opensamlbook.idp;
+
+import no.steras.opensamlbook.OpenSAMLUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Created by Privat on 4/6/14.
+ */
+public class SingleSignOnServlet extends HttpServlet {
+ private static Logger logger = LoggerFactory.getLogger(SingleSignOnServlet.class);
+ private static final String ASSERTION_CONSUMER_SERVICE = "http://localhost:8080/webprofile-ref-project/sp/consumer";
+
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ logger.info("AuthnRequest recieved");
+ Writer w = resp.getWriter();
+ resp.setContentType("text/html");
+ w.append("" + "" + "You are now at IDP, click the button to authenticate
" + "" + "");
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ resp.sendRedirect(ASSERTION_CONSUMER_SERVICE + "?SAMLart=AAQAAMFbLinlXaCM%2BFIxiDwGOLAy2T71gbpO7ZhNzAgEANlB90ECfpNEVLg%3D");
+ }
+
+
+}
diff --git a/src/main/java/no/steras/opensamlbook/sp/AccessFilter.java b/src/main/java/no/steras/opensamlbook/sp/AccessFilter.java
new file mode 100644
index 0000000..1575e40
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/sp/AccessFilter.java
@@ -0,0 +1,165 @@
+package no.steras.opensamlbook.sp;
+
+import no.steras.opensamlbook.OpenSAMLUtils;
+import no.steras.opensamlbook.idp.IDPConstants;
+import org.joda.time.DateTime;
+import org.opensaml.Configuration;
+import org.opensaml.DefaultBootstrap;
+import org.opensaml.common.SAMLObject;
+import org.opensaml.common.binding.BasicSAMLMessageContext;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.binding.encoding.HTTPRedirectDeflateEncoder;
+import org.opensaml.saml2.core.*;
+import org.opensaml.saml2.metadata.Endpoint;
+import org.opensaml.saml2.metadata.SingleSignOnService;
+import org.opensaml.ws.message.encoder.MessageEncodingException;
+import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
+import org.opensaml.xml.ConfigurationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.security.Provider;
+import java.security.Security;
+
+/**
+ * The filter intercepts the user and start the SAML authentication if it is not authenticated
+ */
+public class AccessFilter implements Filter {
+ private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ Configuration.validateJCEProviders();
+ Configuration.validateNonSunJAXP();
+
+ for (Provider jceProvider : Security.getProviders()) {
+ logger.info(jceProvider.getInfo());
+ }
+
+ try {
+ logger.info("Bootstrapping");
+ DefaultBootstrap.bootstrap();
+ } catch (ConfigurationException e) {
+ throw new RuntimeException("Bootstrapping failed");
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpServletRequest = (HttpServletRequest)request;
+ HttpServletResponse httpServletResponse = (HttpServletResponse)response;
+
+ if (httpServletRequest.getSession().getAttribute(SPConstants.AUTHENTICATED_SESSION_ATTRIBUTE) != null) {
+ chain.doFilter(request, response);
+ } else {
+ setGotoURLOnSession(httpServletRequest);
+ redirectUserForAuthentication(httpServletResponse);
+ }
+ }
+
+ private void setGotoURLOnSession(HttpServletRequest request) {
+ request.getSession().setAttribute(SPConstants.GOTO_URL_SESSION_ATTRIBUTE, request.getRequestURL().toString());
+ }
+
+ private void redirectUserForAuthentication(HttpServletResponse httpServletResponse) {
+ AuthnRequest authnRequest = buildAuthnRequest();
+ redirectUserWithRequest(httpServletResponse, authnRequest);
+
+ }
+
+ private void redirectUserWithRequest(HttpServletResponse httpServletResponse, AuthnRequest authnRequest) {
+ HttpServletResponseAdapter responseAdapter = new HttpServletResponseAdapter(httpServletResponse, true);
+ BasicSAMLMessageContext context = new BasicSAMLMessageContext();
+ context.setPeerEntityEndpoint(getIPDEndpoint());
+ context.setOutboundSAMLMessage(authnRequest);
+ context.setOutboundMessageTransport(responseAdapter);
+ context.setOutboundSAMLMessageSigningCredential(SPCredentials.getCredential());
+
+ HTTPRedirectDeflateEncoder encoder = new HTTPRedirectDeflateEncoder();
+ logger.info("AuthnRequest: ");
+ OpenSAMLUtils.logSAMLObject(authnRequest);
+
+ logger.info("Redirecting to IDP");
+ try {
+ encoder.encode(context);
+ } catch (MessageEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private AuthnRequest buildAuthnRequest() {
+ AuthnRequest authnRequest = OpenSAMLUtils.buildSAMLObject(AuthnRequest.class);
+ authnRequest.setIssueInstant(new DateTime());
+ authnRequest.setDestination(getIPDSSODestination());
+ authnRequest.setProtocolBinding(SAMLConstants.SAML2_ARTIFACT_BINDING_URI);
+ authnRequest.setAssertionConsumerServiceURL(getAssertionConsumerEndpoint());
+ authnRequest.setID(OpenSAMLUtils.generateSecureRandomId());
+ authnRequest.setIssuer(buildIssuer());
+ authnRequest.setNameIDPolicy(buildNameIdPolicy());
+ authnRequest.setRequestedAuthnContext(buildRequestedAuthnContext());
+
+ return authnRequest;
+ }
+ private RequestedAuthnContext buildRequestedAuthnContext() {
+ RequestedAuthnContext requestedAuthnContext = OpenSAMLUtils.buildSAMLObject(RequestedAuthnContext.class);
+ requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM);
+
+ AuthnContextClassRef passwordAuthnContextClassRef = OpenSAMLUtils.buildSAMLObject(AuthnContextClassRef.class);
+ passwordAuthnContextClassRef.setAuthnContextClassRef(AuthnContext.PASSWORD_AUTHN_CTX);
+
+ requestedAuthnContext.getAuthnContextClassRefs().add(passwordAuthnContextClassRef);
+
+ return requestedAuthnContext;
+
+ }
+
+ private NameIDPolicy buildNameIdPolicy() {
+ NameIDPolicy nameIDPolicy = OpenSAMLUtils.buildSAMLObject(NameIDPolicy.class);
+ nameIDPolicy.setAllowCreate(true);
+
+ nameIDPolicy.setFormat(NameIDType.TRANSIENT);
+
+ return nameIDPolicy;
+ }
+
+ private Issuer buildIssuer() {
+ Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
+ issuer.setValue(getSPIssuerValue());
+
+ return issuer;
+ }
+
+ private String getSPIssuerValue() {
+ return SPConstants.SP_ENTITY_ID;
+ }
+
+ private String getSPNameQualifier() {
+ return SPConstants.SP_ENTITY_ID;
+ }
+
+ private String getAssertionConsumerEndpoint() {
+ return SPConstants.ASSERTION_CONSUMER_SERVICE;
+ }
+
+ private String getIPDSSODestination() {
+ return IDPConstants.SSO_SERVICE;
+ }
+
+ private Endpoint getIPDEndpoint() {
+ SingleSignOnService endpoint = OpenSAMLUtils.buildSAMLObject(SingleSignOnService.class);
+ endpoint.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
+ endpoint.setLocation(getIPDSSODestination());
+
+ return endpoint;
+ }
+
+
+ @Override
+ public void destroy() {
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/no/steras/opensamlbook/sp/ConsumerServlet.java b/src/main/java/no/steras/opensamlbook/sp/ConsumerServlet.java
new file mode 100644
index 0000000..d1e5ff8
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/sp/ConsumerServlet.java
@@ -0,0 +1,210 @@
+package no.steras.opensamlbook.sp;
+
+import no.steras.opensamlbook.OpenSAMLUtils;
+import no.steras.opensamlbook.idp.IDPConstants;
+import no.steras.opensamlbook.idp.IDPCredentials;
+import org.joda.time.DateTime;
+import org.opensaml.saml2.core.*;
+import org.opensaml.saml2.encryption.Decrypter;
+import org.opensaml.security.SAMLSignatureProfileValidator;
+import org.opensaml.ws.soap.client.BasicSOAPMessageContext;
+import org.opensaml.ws.soap.client.http.HttpClientBuilder;
+import org.opensaml.ws.soap.client.http.HttpSOAPClient;
+import org.opensaml.ws.soap.common.SOAPException;
+import org.opensaml.ws.soap.soap11.Envelope;
+import org.opensaml.xml.Configuration;
+import org.opensaml.xml.XMLObject;
+import org.opensaml.xml.encryption.DecryptionException;
+import org.opensaml.xml.encryption.InlineEncryptedKeyResolver;
+import org.opensaml.xml.io.MarshallingException;
+import org.opensaml.xml.parse.BasicParserPool;
+import org.opensaml.xml.schema.XSString;
+import org.opensaml.xml.security.SecurityException;
+import org.opensaml.xml.security.keyinfo.StaticKeyInfoCredentialResolver;
+import org.opensaml.xml.signature.*;
+import org.opensaml.xml.validation.ValidationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Created by Privat on 4/6/14.
+ */
+public class ConsumerServlet extends HttpServlet {
+ private static Logger logger = LoggerFactory.getLogger(ConsumerServlet.class);
+
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ logger.info("Artifact received");
+ Artifact artifact = buildArtifactFromRequest(req);
+ logger.info("Artifact: " + artifact.getArtifact());
+
+ ArtifactResolve artifactResolve = buildArtifactResolve(artifact);
+ signArtifactResolve(artifactResolve);
+ logger.info("Sending ArtifactResolve");
+ logger.info("ArtifactResolve: ");
+ OpenSAMLUtils.logSAMLObject(artifactResolve);
+
+ ArtifactResponse artifactResponse = sendAndReceiveArtifactResolve(artifactResolve);
+ logger.info("ArtifactResponse received");
+ logger.info("ArtifactResponse: ");
+ OpenSAMLUtils.logSAMLObject(artifactResponse);
+
+ EncryptedAssertion encryptedAssertion = getEncryptedAssertion(artifactResponse);
+ Assertion assertion = decryptAssertion(encryptedAssertion);
+ verifyAssertionSignature(assertion);
+ logger.info("Decrypted Assertion: ");
+ OpenSAMLUtils.logSAMLObject(assertion);
+
+ logAssertionAttributes(assertion);
+ logAuthenticationInstant(assertion);
+ logAuthenticationMethod(assertion);
+
+ setAuthenticatedSession(req);
+ redirectToGotoURL(req, resp);
+ }
+
+ private Assertion decryptAssertion(EncryptedAssertion encryptedAssertion) {
+ StaticKeyInfoCredentialResolver keyInfoCredentialResolver = new StaticKeyInfoCredentialResolver(SPCredentials.getCredential());
+
+ Decrypter decrypter = new Decrypter(null, keyInfoCredentialResolver, new InlineEncryptedKeyResolver());
+ decrypter.setRootInNewDocument(true);
+
+ try {
+ return decrypter.decrypt(encryptedAssertion);
+ } catch (DecryptionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void verifyAssertionSignature(Assertion assertion) {
+ if (!assertion.isSigned()) {
+ throw new RuntimeException("The SAML Assertion was not signed");
+ }
+
+ try {
+ SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
+ profileValidator.validate(assertion.getSignature());
+
+ SignatureValidator sigValidator = new SignatureValidator(IDPCredentials.getCredential());
+
+ sigValidator.validate(assertion.getSignature());
+
+ logger.info("SAML Assertion signature verified");
+ } catch (ValidationException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ private void signArtifactResolve(ArtifactResolve artifactResolve) {
+ Signature signature = OpenSAMLUtils.buildSAMLObject(Signature.class);
+ signature.setSigningCredential(SPCredentials.getCredential());
+ signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
+ signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
+
+ artifactResolve.setSignature(signature);
+
+ try {
+ Configuration.getMarshallerFactory().getMarshaller(artifactResolve).marshall(artifactResolve);
+ } catch (MarshallingException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ Signer.signObject(signature);
+ } catch (SignatureException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void setAuthenticatedSession(HttpServletRequest req) {
+ req.getSession().setAttribute(SPConstants.AUTHENTICATED_SESSION_ATTRIBUTE, true);
+ }
+
+ private void redirectToGotoURL(HttpServletRequest req, HttpServletResponse resp) {
+ String gotoURL = (String)req.getSession().getAttribute(SPConstants.GOTO_URL_SESSION_ATTRIBUTE);
+ logger.info("Redirecting to requested URL: " + gotoURL);
+ try {
+ resp.sendRedirect(gotoURL);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void logAuthenticationMethod(Assertion assertion) {
+ logger.info("Authentication method: " + assertion.getAuthnStatements().get(0)
+ .getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef());
+ }
+
+ private void logAuthenticationInstant(Assertion assertion) {
+ logger.info("Authentication instant: " + assertion.getAuthnStatements().get(0).getAuthnInstant());
+ }
+
+ private void logAssertionAttributes(Assertion assertion) {
+ for (Attribute attribute : assertion.getAttributeStatements().get(0).getAttributes()) {
+ logger.info("Attribute name: " + attribute.getName());
+ for (XMLObject attributeValue : attribute.getAttributeValues()) {
+ logger.info("Attribute value: " + ((XSString) attributeValue).getValue());
+ }
+ }
+ }
+
+ private EncryptedAssertion getEncryptedAssertion(ArtifactResponse artifactResponse) {
+ Response response = (Response)artifactResponse.getMessage();
+ return response.getEncryptedAssertions().get(0);
+ }
+
+ private ArtifactResponse sendAndReceiveArtifactResolve(final ArtifactResolve artifactResolve) {
+ try {
+ Envelope envelope = OpenSAMLUtils.wrapInSOAPEnvelope(artifactResolve);
+
+ HttpClientBuilder clientBuilder = new HttpClientBuilder();
+ HttpSOAPClient soapClient = new HttpSOAPClient(clientBuilder.buildClient(), new BasicParserPool());
+
+ BasicSOAPMessageContext soapContext = new BasicSOAPMessageContext();
+ soapContext.setOutboundMessage(envelope);
+
+ soapClient.send(IDPConstants.ARTIFACT_RESOLUTION_SERVICE, soapContext);
+
+ Envelope soapResponse = (Envelope)soapContext.getInboundMessage();
+ return (ArtifactResponse)soapResponse.getBody().getUnknownXMLObjects().get(0);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (SecurityException e) {
+ throw new RuntimeException(e);
+ } catch (SOAPException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Artifact buildArtifactFromRequest(final HttpServletRequest req) {
+ Artifact artifact = OpenSAMLUtils.buildSAMLObject(Artifact.class);
+ artifact.setArtifact(req.getParameter("SAMLart"));
+ return artifact;
+ }
+
+ private ArtifactResolve buildArtifactResolve(final Artifact artifact) {
+ ArtifactResolve artifactResolve = OpenSAMLUtils.buildSAMLObject(ArtifactResolve.class);
+
+ Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
+ issuer.setValue(SPConstants.SP_ENTITY_ID);
+ artifactResolve.setIssuer(issuer);
+
+ artifactResolve.setIssueInstant(new DateTime());
+
+ artifactResolve.setID(OpenSAMLUtils.generateSecureRandomId());
+
+ artifactResolve.setDestination(IDPConstants.ARTIFACT_RESOLUTION_SERVICE);
+
+ artifactResolve.setArtifact(artifact);
+
+ return artifactResolve;
+ }
+
+}
diff --git a/src/main/java/no/steras/opensamlbook/sp/SPConstants.java b/src/main/java/no/steras/opensamlbook/sp/SPConstants.java
new file mode 100644
index 0000000..4f53112
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/sp/SPConstants.java
@@ -0,0 +1,15 @@
+package no.steras.opensamlbook.sp;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Created by Privat on 4/7/14.
+ */
+public class SPConstants {
+ public static final String SP_ENTITY_ID = "TestSP";
+ public static final String AUTHENTICATED_SESSION_ATTRIBUTE = "authenticated";
+ public static final String GOTO_URL_SESSION_ATTRIBUTE = "gotoURL";
+ public static final String ASSERTION_CONSUMER_SERVICE = "http://localhost:8080/webprofile-ref-project/sp/consumer";
+
+}
diff --git a/src/main/java/no/steras/opensamlbook/sp/SPCredentials.java b/src/main/java/no/steras/opensamlbook/sp/SPCredentials.java
new file mode 100644
index 0000000..5b425f7
--- /dev/null
+++ b/src/main/java/no/steras/opensamlbook/sp/SPCredentials.java
@@ -0,0 +1,63 @@
+package no.steras.opensamlbook.sp;
+
+import org.opensaml.xml.security.*;
+import org.opensaml.xml.security.credential.BasicCredential;
+import org.opensaml.xml.security.credential.Credential;
+import org.opensaml.xml.security.credential.KeyStoreCredentialResolver;
+import org.opensaml.xml.security.credential.UsageType;
+import org.opensaml.xml.security.criteria.EntityIDCriteria;
+import org.opensaml.xml.security.x509.X509Credential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.*;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by Privat on 13/05/14.
+ */
+public class SPCredentials {
+ private static final String KEY_STORE_PASSWORD = "password";
+ private static final String KEY_STORE_ENTRY_PASSWORD = "password";
+ private static final String KEY_STORE_PATH = "/SPKeystore.jks";
+ private static final String KEY_ENTRY_ID = "SPKey";
+
+ private static final Credential credential;
+
+ static {
+ try {
+ KeyStore keystore = readKeystoreFromFile(KEY_STORE_PATH, KEY_STORE_PASSWORD);
+ Map passwordMap = new HashMap();
+ passwordMap.put(KEY_ENTRY_ID, KEY_STORE_ENTRY_PASSWORD);
+ KeyStoreCredentialResolver resolver = new KeyStoreCredentialResolver(keystore, passwordMap);
+
+ Criteria criteria = new EntityIDCriteria(KEY_ENTRY_ID);
+ CriteriaSet criteriaSet = new CriteriaSet(criteria);
+
+ credential = resolver.resolveSingle(criteriaSet);
+ } catch (org.opensaml.xml.security.SecurityException e) {
+ throw new RuntimeException("Something went wrong reading credentials", e);
+ }
+ }
+
+ private static KeyStore readKeystoreFromFile(String pathToKeyStore, String keyStorePassword) {
+ try {
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ InputStream inputStream = SPCredentials.class.getResourceAsStream(pathToKeyStore);
+ keystore.load(inputStream, keyStorePassword.toCharArray());
+ inputStream.close();
+ return keystore;
+ } catch (Exception e) {
+ throw new RuntimeException("Something went wrong reading keystore", e);
+ }
+ }
+
+ public static Credential getCredential() {
+ return credential;
+ }
+}
diff --git a/src/main/resources/SPKeystore.jks b/src/main/resources/SPKeystore.jks
new file mode 100644
index 0000000..20ab06e
Binary files /dev/null and b/src/main/resources/SPKeystore.jks differ
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000..43a20f7
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..67db340
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,52 @@
+
+
+
+ Archetype Created Web Application
+
+
+ ApplicationServlet
+ no.steras.opensamlbook.app.ApplicationServlet
+
+
+ SingleSignOnService
+ no.steras.opensamlbook.idp.SingleSignOnServlet
+
+
+ ConsumerServlet
+ no.steras.opensamlbook.sp.ConsumerServlet
+
+
+ ArtifactResolutionServlet
+ no.steras.opensamlbook.idp.ArtifactResolutionServlet
+
+
+
+ ApplicationServlet
+ /app/appservlet
+
+
+ SingleSignOnService
+ /idp/singleSignOnService
+
+
+ ConsumerServlet
+ /sp/consumer
+
+
+ ArtifactResolutionServlet
+ /idp/artifactResolutionService
+
+
+
+ AccessFilter
+ no.steras.opensamlbook.sp.AccessFilter
+
+
+
+ AccessFilter
+ /app/*
+ REQUEST
+
+
diff --git a/webprofile-ref-project.iml b/webprofile-ref-project.iml
new file mode 100644
index 0000000..327259f
--- /dev/null
+++ b/webprofile-ref-project.iml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+