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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +