Commit 3568acfe authored by michael.simon's avatar michael.simon
Browse files

major refactoring, moving ecp functionality in service layer.

parent bf69aacd
......@@ -11,43 +11,222 @@
package edu.kit.scc.webreg.service.saml;
import java.io.IOException;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.opensaml.common.SAMLObject;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.EncryptedAssertion;
import org.opensaml.saml2.core.EncryptedID;
import org.opensaml.saml2.core.NameID;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.encryption.Decrypter;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.xml.encryption.DecryptionException;
import org.opensaml.xml.encryption.InlineEncryptedKeyResolver;
import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xml.security.keyinfo.StaticKeyInfoCredentialResolver;
import org.opensaml.xml.security.x509.BasicX509Credential;
import org.slf4j.Logger;
import edu.kit.scc.webreg.entity.SamlMetadataEntity;
import edu.kit.scc.webreg.entity.SamlSpConfigurationEntity;
import edu.kit.scc.webreg.exc.NoAssertionException;
import edu.kit.scc.webreg.exc.SamlAuthenticationException;
public interface Saml2AssertionService {
@ApplicationScoped
public class Saml2AssertionService {
@Inject
private Logger logger;
@Inject
private CryptoHelper cryptoHelper;
@Inject
private SamlHelper samlHelper;
@Inject
private Saml2ResponseValidationService saml2ValidationService;
public Assertion processSamlResponse(Response samlResponse, SamlMetadataEntity idpEntity,
EntityDescriptor idpEntityDescriptor, SamlSpConfigurationEntity spEntity)
throws IOException, DecryptionException, SamlAuthenticationException {
return processSamlResponse(samlResponse, idpEntity, idpEntityDescriptor, spEntity, true);
}
public Assertion processSamlResponse(Response samlResponse, SamlMetadataEntity idpEntity,
EntityDescriptor idpEntityDescriptor, SamlSpConfigurationEntity spEntity, boolean checkSignature)
throws IOException, DecryptionException, SamlAuthenticationException {
saml2ValidationService.verifyStatus(samlResponse);
saml2ValidationService.verifyIssuer(idpEntity, samlResponse);
saml2ValidationService.verifyExpiration(samlResponse, 1000L * 60L * 10L);
Boolean responseSignatureValid = false;
public Assertion decryptAssertion(EncryptedAssertion encryptedAssertion, String privateKey)
throws IOException, DecryptionException, SamlAuthenticationException;
if (checkSignature) {
try {
logger.debug("Validating SamlResponse Signature for " + samlResponse.getID());
saml2ValidationService.validateIdpSignature(samlResponse, samlResponse.getIssuer(), idpEntityDescriptor);
logger.debug("Validating SamlResponse Signature success for " + samlResponse.getID());
responseSignatureValid = true;
} catch (SamlAuthenticationException e) {
logger.debug("SamlResponse doesn't contain a signature");
}
}
else {
logger.debug("Skipping signature check for SamlResponse");
}
Map<String, List<Object>> extractAttributes(Assertion assertion);
List<Assertion> assertionList = samlResponse.getAssertions();
List<EncryptedAssertion> encryptedAssertionList = samlResponse.getEncryptedAssertions();
logger.debug("Got {} assertion and {} encrypted assertion", assertionList.size(), encryptedAssertionList.size());
SAMLObject decryptNameID(EncryptedID encryptedID, String privateKey)
throws IOException, DecryptionException, SamlAuthenticationException;
Assertion assertion;
/**
* take first encrypted assertion, then first assertion, ignore all other
*/
if (encryptedAssertionList.size() > 0) {
assertion = decryptAssertion(
encryptedAssertionList.get(0), spEntity.getPrivateKey());
}
else if (assertionList.size() > 0) {
assertion = assertionList.get(0);
}
else {
throw new NoAssertionException("SAML2 Response contained no Assertion");
}
Assertion processSamlResponse(Response samlResponse,
SamlMetadataEntity idpEntity,
EntityDescriptor idpEntityDescriptor,
SamlSpConfigurationEntity spEntity) throws IOException, DecryptionException, SamlAuthenticationException;
if (checkSignature) {
if (! responseSignatureValid) {
logger.debug("Validating Assertion Signature for " + assertion.getID());
saml2ValidationService.validateIdpSignature(assertion, assertion.getIssuer(), idpEntityDescriptor);
logger.debug("Validating Assertion Signature success for " + assertion.getID());
}
else {
logger.debug("Skipping assertion signature validation. SamlResponse was signed");
}
}
else {
logger.debug("Skipping signature check for Assertion");
}
return assertion;
}
public String extractPersistentId(Assertion assertion, SamlSpConfigurationEntity spEntity)
throws IOException, DecryptionException, SamlAuthenticationException {
logger.debug("Fetching name Id from assertion");
String persistentId;
/*
* Assertion needs a Subject and a NameID, encrypted or not
*/
if (assertion.getSubject() == null)
throw new SamlAuthenticationException("No Subject in assertion!");
if (assertion.getSubject().getNameID() == null &&
assertion.getSubject().getEncryptedID() == null)
throw new SamlAuthenticationException("SAML2 NameID is missing.");
String extractPersistentId(Assertion assertion,
SamlSpConfigurationEntity spEntity) throws IOException, DecryptionException, SamlAuthenticationException;
/*
* If the NameID is encrypted, decrypt it
*/
NameID nid;
if (assertion.getSubject().getEncryptedID() != null) {
EncryptedID eid = assertion.getSubject().getEncryptedID();
SAMLObject samlObject = decryptNameID(eid, spEntity.getPrivateKey());
if (samlObject instanceof NameID)
nid = (NameID) samlObject;
else
throw new SamlAuthenticationException("Only Encrypted NameIDs are supoorted. Encrypted BaseIDs or embedded Assertions are not supported");
}
else
nid = assertion.getSubject().getNameID();
logger.debug("NameId format {} value {}", nid.getFormat(), nid.getValue());
if (nid.getFormat().equals(NameID.TRANSIENT)) {
throw new SamlAuthenticationException("NameID is Transient but must be Persistent");
}
else if (nid.getFormat().equals(NameID.PERSISTENT)) {
persistentId = nid.getValue();
}
else
throw new SamlAuthenticationException("Unsupported SAML2 NameID Type");
Assertion processSamlResponse(Response samlResponse,
SamlMetadataEntity idpEntity,
EntityDescriptor idpEntityDescriptor,
SamlSpConfigurationEntity spEntity, boolean checkSignature)
throws IOException, DecryptionException, SamlAuthenticationException;
return persistentId;
}
public Assertion decryptAssertion(EncryptedAssertion encryptedAssertion,
String privateKey) throws IOException, DecryptionException, SamlAuthenticationException {
logger.debug("Decrypting assertion...");
PrivateKey pk;
try {
pk = cryptoHelper.getPrivateKey(privateKey);
} catch (IOException e) {
throw new SamlAuthenticationException("Private key is not set up properly", e);
}
if (pk == null) {
throw new SamlAuthenticationException("Private key is not set up properly (is null)");
}
BasicX509Credential decryptCredential = new BasicX509Credential();
decryptCredential.setPrivateKey(pk);
KeyInfoCredentialResolver keyResolver = new StaticKeyInfoCredentialResolver(decryptCredential);
InlineEncryptedKeyResolver encryptionKeyResolver = new InlineEncryptedKeyResolver();
Decrypter decrypter = new Decrypter(null, keyResolver, encryptionKeyResolver);
decrypter.setRootInNewDocument(true);
Assertion assertion = decrypter.decrypt(encryptedAssertion);
return assertion;
}
public SAMLObject decryptNameID(EncryptedID encryptedID,
String privateKey) throws IOException, DecryptionException, SamlAuthenticationException {
logger.debug("Decrypting nameID...");
PrivateKey pk;
try {
pk = cryptoHelper.getPrivateKey(privateKey);
} catch (IOException e) {
throw new SamlAuthenticationException("Private key is not set up properly", e);
}
if (pk == null) {
throw new SamlAuthenticationException("Private key is not set up properly");
}
BasicX509Credential decryptCredential = new BasicX509Credential();
decryptCredential.setPrivateKey(pk);
KeyInfoCredentialResolver keyResolver = new StaticKeyInfoCredentialResolver(decryptCredential);
InlineEncryptedKeyResolver encryptionKeyResolver = new InlineEncryptedKeyResolver();
Decrypter decrypter = new Decrypter(null, keyResolver, encryptionKeyResolver);
decrypter.setRootInNewDocument(true);
SAMLObject samlObject = decrypter.decrypt(encryptedID);
return samlObject;
}
public Map<String, List<Object>> extractAttributes(Assertion assertion) {
if (assertion == null)
return null;
Map<String, Attribute> attributes = samlHelper.assertionToAttributeMap(assertion);
Map<String, List<Object>> attributeMap = new HashMap<String, List<Object>>();
for (Entry<String, Attribute> entry : attributes.entrySet()) {
attributeMap.put(entry.getKey(), samlHelper.getAttribute(entry.getValue()));
}
return attributeMap;
}
}
......@@ -10,22 +10,54 @@
******************************************************************************/
package edu.kit.scc.webreg.service.saml;
import javax.enterprise.context.ApplicationScoped;
import javax.servlet.http.HttpServletRequest;
import org.opensaml.common.SAMLObject;
import org.opensaml.common.binding.BasicSAMLMessageContext;
import org.opensaml.saml2.binding.decoding.HTTPPostDecoder;
import org.opensaml.saml2.binding.decoding.HTTPSOAP11Decoder;
import org.opensaml.saml2.core.AttributeQuery;
import org.opensaml.saml2.core.Response;
import org.opensaml.ws.message.decoder.MessageDecodingException;
import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
import org.opensaml.xml.security.SecurityException;
import edu.kit.scc.webreg.exc.SamlAuthenticationException;
public interface Saml2DecoderService {
@ApplicationScoped
public class Saml2DecoderService {
public Response decodePostMessage(HttpServletRequest request)
throws MessageDecodingException, SecurityException, SamlAuthenticationException;
throws MessageDecodingException, SecurityException, SamlAuthenticationException {
AttributeQuery decodeAttributeQuery(HttpServletRequest request)
throws MessageDecodingException, SecurityException,
SamlAuthenticationException;
HTTPPostDecoder decoder = new HTTPPostDecoder();
BasicSAMLMessageContext<SAMLObject, SAMLObject, SAMLObject> messageContext =
new BasicSAMLMessageContext<SAMLObject, SAMLObject, SAMLObject>();
HttpServletRequestAdapter adapter = new HttpServletRequestAdapter(request);
messageContext.setInboundMessageTransport(adapter);
decoder.decode(messageContext);
SAMLObject obj = messageContext.getInboundSAMLMessage();
if (obj instanceof Response)
return (Response) obj;
else
throw new SamlAuthenticationException("Not a valid SAML2 Post Response");
}
public AttributeQuery decodeAttributeQuery(HttpServletRequest request)
throws MessageDecodingException, SecurityException, SamlAuthenticationException {
HTTPSOAP11Decoder decoder = new HTTPSOAP11Decoder();
BasicSAMLMessageContext<SAMLObject, SAMLObject, SAMLObject> messageContext =
new BasicSAMLMessageContext<SAMLObject, SAMLObject, SAMLObject>();
HttpServletRequestAdapter adapter = new HttpServletRequestAdapter(request);
messageContext.setInboundMessageTransport(adapter);
decoder.decode(messageContext);
SAMLObject obj = messageContext.getInboundSAMLMessage();
if (obj instanceof AttributeQuery)
return (AttributeQuery) obj;
else
throw new SamlAuthenticationException("Not a valid SAML2 Attribute Query");
}
}
......@@ -10,17 +10,61 @@
******************************************************************************/
package edu.kit.scc.webreg.service.saml;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;
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.AuthnRequest;
import org.opensaml.saml2.core.NameID;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.SingleSignOnService;
import org.opensaml.ws.message.encoder.MessageEncodingException;
import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
import org.slf4j.Logger;
import edu.kit.scc.webreg.entity.SamlIdpMetadataEntity;
import edu.kit.scc.webreg.entity.SamlSpConfigurationEntity;
public interface Saml2RedirectService {
@ApplicationScoped
public class Saml2RedirectService {
@Inject
private Logger logger;
@Inject
private SamlHelper samlHelper;
@Inject
private MetadataHelper metadataHelper;
@Inject
private SsoHelper ssoHelper;
public void redirectClient(SamlIdpMetadataEntity idpEntity,
SamlSpConfigurationEntity spEntity, HttpServletResponse response)
throws MessageEncodingException;
SamlSpConfigurationEntity spEntity, HttpServletResponse response)
throws MessageEncodingException {
EntityDescriptor entityDesc = samlHelper.unmarshal(
idpEntity.getEntityDescriptor(), EntityDescriptor.class);
SingleSignOnService sso = metadataHelper.getSSO(entityDesc, SAMLConstants.SAML2_REDIRECT_BINDING_URI);
AuthnRequest authnRequest = ssoHelper.buildAuthnRequest(
spEntity.getEntityId(), spEntity.getAcs(), SAMLConstants.SAML2_POST_BINDING_URI);
logger.debug("Sending client to idp {} endpoint {}", idpEntity.getEntityId(), sso.getLocation());
HTTPRedirectDeflateEncoder encoder = new HTTPRedirectDeflateEncoder();
BasicSAMLMessageContext<SAMLObject, AuthnRequest, NameID> messageContext =
new BasicSAMLMessageContext<SAMLObject, AuthnRequest, NameID>();
messageContext.setOutboundSAMLMessage(authnRequest);
messageContext.setPeerEntityEndpoint(sso);
messageContext.setOutboundMessageTransport(new HttpServletResponseAdapter(response, true));
encoder.encode(messageContext);
}
}
......@@ -10,37 +10,148 @@
******************************************************************************/
package edu.kit.scc.webreg.service.saml;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.xml.namespace.QName;
import org.joda.time.Duration;
import org.joda.time.Instant;
import org.opensaml.Configuration;
import org.opensaml.common.SignableSAMLObject;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.core.AttributeQuery;
import org.opensaml.saml2.core.Issuer;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.Status;
import org.opensaml.saml2.core.StatusCode;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml2.metadata.provider.DOMMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.security.MetadataCredentialResolver;
import org.opensaml.security.MetadataCriteria;
import org.opensaml.security.SAMLSignatureProfileValidator;
import org.opensaml.xml.security.CriteriaSet;
import org.opensaml.xml.security.SecurityException;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.criteria.EntityIDCriteria;
import org.opensaml.xml.security.criteria.UsageCriteria;
import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xml.signature.impl.ExplicitKeySignatureTrustEngine;
import org.opensaml.xml.validation.ValidationException;
import org.slf4j.Logger;
import edu.kit.scc.webreg.entity.SamlMetadataEntity;
import edu.kit.scc.webreg.exc.SamlAuthenticationException;
public interface Saml2ResponseValidationService {
@ApplicationScoped
public class Saml2ResponseValidationService {
@Inject
private Logger logger;
@Inject
private SamlHelper samlHelper;
public void verifyIssuer(SamlMetadataEntity metadataEntity,
Response samlResponse) throws SamlAuthenticationException {
verifyIssuer(metadataEntity, samlResponse.getIssuer());
}
public void verifyIssuer(SamlMetadataEntity metadataEntity,
AttributeQuery attributeQuery) throws SamlAuthenticationException {
verifyIssuer(metadataEntity, attributeQuery.getIssuer());
}
public void verifyIssuer(SamlMetadataEntity metadataEntity,
Issuer issuer) throws SamlAuthenticationException {
public void verifyIssuer(SamlMetadataEntity metadataEntity, Response samlResponse)
throws SamlAuthenticationException;
if (issuer == null)
throw new SamlAuthenticationException("Response issuer is not set");
void verifyExpiration(Response samlResponse, Long expiryMillis)
throws SamlAuthenticationException;
String issuerString = issuer.getValue();
if (! issuerString.equals(metadataEntity.getEntityId()))
throw new SamlAuthenticationException("Response issuer " + issuerString +
" differs from excpected " + metadataEntity.getEntityId());
void verifyStatus(Response samlResponse) throws SamlAuthenticationException;
}
void verifyIssuer(SamlMetadataEntity metadataEntity,
AttributeQuery attributeQuery) throws SamlAuthenticationException;
public void verifyExpiration(Response samlResponse, Long expiryMillis)
throws SamlAuthenticationException {
void verifyIssuer(SamlMetadataEntity metadataEntity, Issuer issuer)
throws SamlAuthenticationException;
Duration duration = new Duration(samlResponse.getIssueInstant(), new Instant());
if (duration.isLongerThan(new Duration(expiryMillis)))
throw new SamlAuthenticationException("Response is already expired after " + duration.getStandardSeconds() + " seconds");
}
void validateIdpSignature(SignableSAMLObject signableSamlObject,
Issuer issuer, EntityDescriptor entityDescriptor)
throws SamlAuthenticationException;
public void verifyStatus(Response samlResponse)
throws SamlAuthenticationException {
void validateSpSignature(SignableSAMLObject signableSamlObject,
Issuer issuer, EntityDescriptor entityDescriptor)
throws SamlAuthenticationException;
if (samlResponse.getStatus() == null || samlResponse.getStatus().getStatusCode() == null)
throw new SamlAuthenticationException("SAML Response does not contain a status code");
Status status = samlResponse.getStatus();
if (! status.getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) {
String s = samlHelper.prettyPrint(status);
logger.info("SAML Response Status: {}", s);
throw new SamlAuthenticationException("SAML Response: Login was not successful " + status.getStatusCode().getValue());
}
}
public void validateIdpSignature(SignableSAMLObject signableSamlObject, Issuer issuer, EntityDescriptor entityDescriptor)
throws SamlAuthenticationException {
validateSignature(signableSamlObject, issuer, entityDescriptor,
IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS);
}
public void validateSpSignature(SignableSAMLObject signableSamlObject, Issuer issuer, EntityDescriptor entityDescriptor)
throws SamlAuthenticationException {
validateSignature(signableSamlObject, issuer, entityDescriptor,
SPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS);
}
protected void validateSignature(SignableSAMLObject signableSamlObject, Issuer issuer, EntityDescriptor entityDescriptor,
QName role, String protocol)
throws SamlAuthenticationException {
if (signableSamlObject.getSignature() == null)
throw new SamlAuthenticationException("No Signature on SignableSamlObject");
DOMMetadataProvider mp = new DOMMetadataProvider(entityDescriptor.getDOM());
try {
mp.initialize();
} catch (MetadataProviderException e) {
throw new SamlAuthenticationException("Metadata for IDP " + entityDescriptor.getEntityID() + " could not be established");
}
MetadataCredentialResolver mdCredResolver = new MetadataCredentialResolver(mp);
KeyInfoCredentialResolver keyInfoCredResolver =
Configuration.getGlobalSecurityConfiguration().getDefaultKeyInfoCredentialResolver();
ExplicitKeySignatureTrustEngine trustEngine = new ExplicitKeySignatureTrustEngine(mdCredResolver, keyInfoCredResolver);
SAMLSignatureProfileValidator sigValidator = new SAMLSignatureProfileValidator();
try {
sigValidator.validate(signableSamlObject.getSignature());
} catch (ValidationException e) {
throw new SamlAuthenticationException("SAMLSignableObject signature is not valid");
}
CriteriaSet criteriaSet = new CriteriaSet();
criteriaSet.add(new EntityIDCriteria(issuer.getValue()));
criteriaSet.add(new MetadataCriteria(role, protocol));
criteriaSet.add(new UsageCriteria(UsageType.SIGNING));
try {
if (trustEngine.validate(signableSamlObject.getSignature(), criteriaSet))
logger.info("Signutare validation success for " + entityDescriptor.getEntityID());
else {
throw new SamlAuthenticationException("SAMLSignableObject could not be validated.");
}
} catch (SecurityException e) {
throw new SamlAuthenticationException("SAMLSignableObject could not be validated.");
}
}
}
/*******************************************************************************
* Copyright (c) 2014 Michael Simon.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* Michael Simon - initial
******************************************************************************/
package edu.kit.scc.webreg.service.saml.impl;
import java.io.IOException;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.ejb.Stateless;
import javax.inject.Inject;
import org.opensaml.common.SAMLObject;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.EncryptedAssertion;
import org.opensaml.saml2.core.EncryptedID;
import org.opensaml.saml2.core.NameID;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.encryption.Decrypter;