From 09be824c6a7cac98b5388eefd4795f4c076a385d Mon Sep 17 00:00:00 2001 From: Alexander Kreutz Date: Wed, 19 Jun 2024 11:23:20 +0200 Subject: [PATCH] implementation of IDP renew transaction --- pom.xml | 7 +- .../AssertionFromIdpTokenRouteBuilder.java | 2 +- .../ti/i4mi/mag/xua/AuthRequestConverter.java | 15 +- .../i4mi/mag/xua/AuthResponseConverter.java | 7 +- .../i4mi/mag/xua/AuthenticationRequest.java | 2 + .../ti/i4mi/mag/xua/Iti71RouteBuilder.java | 6 +- .../ti/i4mi/mag/xua/OAuth2TokenResponse.java | 2 + .../xua/SAMLRenewSecurityTokenBuilder.java | 398 ++++++++++++++++++ .../ch/bfh/ti/i4mi/mag/xua/TokenEndpoint.java | 15 +- .../ch/bfh/ti/i4mi/mag/xua/TokenRenew.java | 80 ++++ .../i4mi/mag/xua/TokenRenewRouteBuilder.java | 90 ++++ 11 files changed, 613 insertions(+), 11 deletions(-) create mode 100644 src/main/java/ch/bfh/ti/i4mi/mag/xua/SAMLRenewSecurityTokenBuilder.java create mode 100644 src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenRenew.java create mode 100644 src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenRenewRouteBuilder.java diff --git a/pom.xml b/pom.xml index a555320b..7817f582 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 1.11 yyyy-MM-dd HH:mm:ss - 11 + 15 3.10.1 @@ -258,6 +258,11 @@ opensaml ${opensaml-version} + + org.glassfish.jersey.core + jersey-common + 2.43 + org.apache.camel diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/AssertionFromIdpTokenRouteBuilder.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/AssertionFromIdpTokenRouteBuilder.java index 3cf542e6..8e15553f 100644 --- a/src/main/java/ch/bfh/ti/i4mi/mag/xua/AssertionFromIdpTokenRouteBuilder.java +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/AssertionFromIdpTokenRouteBuilder.java @@ -60,7 +60,7 @@ public void configure() throws Exception { // identity provider assertions cached in spring security session // this is unrelated to the IDP provider cookie set by the IDP itself .process(Utils.endHttpSession()) - + .setProperty("oauthrequest").method(TokenRenew.class, "emptyAuthRequest") .bean(AuthRequestConverter.class, "buildAssertionRequestFromIdp") .bean(Iti40RequestGenerator.class, "buildAssertion") .removeHeaders("*", "scope") diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthRequestConverter.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthRequestConverter.java index 8e9c697d..14457690 100644 --- a/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthRequestConverter.java +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthRequestConverter.java @@ -16,12 +16,15 @@ package ch.bfh.ti.i4mi.mag.xua; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Map; import javax.ws.rs.BadRequestException; import org.apache.camel.Body; +import org.apache.camel.ExchangeProperty; import org.apache.camel.Header; import org.apache.camel.Headers; import org.springframework.beans.factory.annotation.Autowired; @@ -31,6 +34,7 @@ import org.springframework.stereotype.Component; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import net.sourceforge.plantuml.utils.Log; /** * Convert OAuth2 request to SAML Assertion Request @@ -86,7 +90,7 @@ public AuthenticationRequest buildAuthenticationRequest( return request; } - public AssertionRequest buildAssertionRequest(@Header("response_type") String responseType, @Header("oauthrequest") AuthenticationRequest request) throws AuthException { + public AssertionRequest buildAssertionRequest(@Header("response_type") String responseType, @ExchangeProperty("oauthrequest") AuthenticationRequest request) throws AuthException { if (!"code".equals(responseType)) throw new AuthException(400, "invalid_request", "response_type must be 'code'"); @@ -107,6 +111,15 @@ private String decode(String in) { public AssertionRequest buildAssertionRequestFromIdp(@Body String authorization, @Header("scope") String scope) throws AuthException { return buildAssertionRequestInternal(authorization, scope); } + + public AssertionRequest buildAssertionRequestFromToken(@Body String authorization, @Header("scope") String scope) throws AuthException { + + try { + authorization = new String(Base64.getDecoder().decode(authorization), "UTF-8"); + } catch (UnsupportedEncodingException e) {} + + return buildAssertionRequestInternal(authorization, scope); + } private AssertionRequest buildAssertionRequestInternal(Object authorization, String scope) throws AuthException { diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthResponseConverter.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthResponseConverter.java index 46fb1313..774307f1 100644 --- a/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthResponseConverter.java +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthResponseConverter.java @@ -21,6 +21,7 @@ import java.net.URLEncoder; import org.apache.camel.Body; +import org.apache.camel.ExchangeProperty; import org.apache.camel.Header; import org.apache.commons.lang.RandomStringUtils; import org.apache.cxf.binding.soap.SoapFault; @@ -37,7 +38,7 @@ public class AuthResponseConverter { @Autowired private Cache codeToToken; - public String handle(@Body String assertion, @Header("oauthrequest") AuthenticationRequest request) throws UnsupportedEncodingException { + public String handle(@Body String assertion, @ExchangeProperty("oauthrequest") AuthenticationRequest request) throws UnsupportedEncodingException { String returnurl = request.getRedirect_uri(); String state = request.getState(); @@ -54,7 +55,7 @@ public String handle(@Body String assertion, @Header("oauthrequest") Authenticat return returnurl; } - public String handleerror(@Header("oauthrequest") AuthenticationRequest request, @Body AuthException exception) throws UnsupportedEncodingException{ + public String handleerror(@ExchangeProperty("oauthrequest") AuthenticationRequest request, @Body AuthException exception) throws UnsupportedEncodingException{ String returnurl = request.getRedirect_uri(); String state = request.getState(); @@ -68,7 +69,7 @@ public String handleerror(@Header("oauthrequest") AuthenticationRequest request, return returnurl; } - public String handlesoaperror(@Header("oauthrequest") AuthenticationRequest request, @Body SoapFault exception) throws UnsupportedEncodingException{ + public String handlesoaperror(@ExchangeProperty("oauthrequest") AuthenticationRequest request, @Body SoapFault exception) throws UnsupportedEncodingException{ String returnurl = request.getRedirect_uri(); String state = request.getState(); diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthenticationRequest.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthenticationRequest.java index 6f3e039b..cf99c030 100644 --- a/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthenticationRequest.java +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/AuthenticationRequest.java @@ -39,6 +39,8 @@ public class AuthenticationRequest { private String assertion; + private String idpAssertion; + private String code_challenge; } diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/Iti71RouteBuilder.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/Iti71RouteBuilder.java index 36ac4843..9f0b9c49 100644 --- a/src/main/java/ch/bfh/ti/i4mi/mag/xua/Iti71RouteBuilder.java +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/Iti71RouteBuilder.java @@ -64,7 +64,7 @@ public void configure() throws Exception { from(String.format("servlet://%s?matchOnUriPrefix=true", AUTHORIZE_PATH)).routeId("iti71") .doTry() - .setHeader("oauthrequest").method(converter, "buildAuthenticationRequest") + .setProperty("oauthrequest").method(converter, "buildAuthenticationRequest") // end spring security session in order to prevent use of already expired // identity provider assertions cached in spring security session @@ -72,8 +72,8 @@ public void configure() throws Exception { .process(Utils.endHttpSession()) .bean(AuthRequestConverter.class, "buildAssertionRequest") - .bean(Iti40RequestGenerator.class, "buildAssertion") - + .bean(TokenRenew.class, "keepIdpAssertion") + .bean(Iti40RequestGenerator.class, "buildAssertion") .removeHeaders("*","oauthrequest") .setHeader(CxfConstants.OPERATION_NAME, constant("Issue")) diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/OAuth2TokenResponse.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/OAuth2TokenResponse.java index cb013a8a..9c27f05a 100644 --- a/src/main/java/ch/bfh/ti/i4mi/mag/xua/OAuth2TokenResponse.java +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/OAuth2TokenResponse.java @@ -29,6 +29,8 @@ public class OAuth2TokenResponse { private String access_token; + private String refresh_token; + private String token_type; private long expires_in; diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/SAMLRenewSecurityTokenBuilder.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/SAMLRenewSecurityTokenBuilder.java new file mode 100644 index 00000000..c0faa4df --- /dev/null +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/SAMLRenewSecurityTokenBuilder.java @@ -0,0 +1,398 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.bfh.ti.i4mi.mag.xua; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.TrustManager; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.camel.Body; +import org.apache.camel.Exchange; +import org.apache.camel.ExchangeProperty; +import org.apache.camel.Processor; +import org.apache.camel.http.common.HttpMessage; +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.URI; +import org.apache.commons.httpclient.URIException; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.apache.commons.httpclient.protocol.SSLProtocolSocketFactory; +import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; +import org.apache.cxf.staxutils.StaxUtils; +import org.apache.http.impl.client.HttpClients; +import org.joda.time.DateTime; +import org.opensaml.Configuration; +import org.opensaml.common.SAMLException; +import org.opensaml.common.SignableSAMLObject; +import org.opensaml.ws.message.decoder.MessageDecodingException; +import org.opensaml.ws.message.encoder.MessageEncodingException; +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.client.http.TLSProtocolSocketFactory; +import org.opensaml.ws.soap.common.SOAPException; +import org.opensaml.ws.soap.soap11.Envelope; +import org.opensaml.ws.soap.soap11.Header; +import org.opensaml.ws.soap.util.SOAPHelper; +import org.opensaml.ws.transport.http.HttpClientInTransport; +import org.opensaml.ws.transport.http.HttpClientOutTransport; +import org.opensaml.ws.wssecurity.BinarySecurityToken; +import org.opensaml.ws.wssecurity.Created; +import org.opensaml.ws.wssecurity.EncodedString; +import org.opensaml.ws.wssecurity.Expires; +import org.opensaml.ws.wssecurity.Security; +import org.opensaml.ws.wssecurity.SecurityTokenReference; +import org.opensaml.ws.wssecurity.Timestamp; +import org.opensaml.ws.wssecurity.WSSecurityConstants; +import org.opensaml.ws.wssecurity.util.WSSecurityHelper; +import org.opensaml.ws.wstrust.RenewTarget; +import org.opensaml.ws.wstrust.RequestSecurityToken; +import org.opensaml.ws.wstrust.RequestType; +import org.opensaml.ws.wstrust.TokenType; +import org.opensaml.xml.XMLObject; +import org.opensaml.xml.XMLObjectBuilderFactory; +import org.opensaml.xml.io.Marshaller; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.io.Unmarshaller; +import org.opensaml.xml.io.UnmarshallingException; +import org.opensaml.xml.parse.BasicParserPool; +import org.opensaml.xml.security.CriteriaSet; +import org.opensaml.xml.security.credential.Credential; +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.KeyInfoHelper; +import org.opensaml.xml.security.x509.BasicX509Credential; +import org.opensaml.xml.security.x509.X509Credential; + +import org.opensaml.xml.signature.DocumentInternalIDContentReference; +import org.opensaml.xml.signature.KeyInfo; +import org.opensaml.xml.signature.SignableXMLObject; +import org.opensaml.xml.signature.Signature; +import org.opensaml.xml.signature.SignatureConstants; +import org.opensaml.xml.signature.SignatureException; +import org.opensaml.xml.signature.Signer; +import org.opensaml.xml.signature.X509Data; +import org.opensaml.xml.signature.X509IssuerSerial; +import org.opensaml.xml.signature.impl.X509IssuerSerialBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.saml.context.SAMLContextProvider; +import org.springframework.security.saml.context.SAMLMessageContext; +import org.springframework.security.saml.key.KeyManager; +import org.springframework.security.saml.processor.SAMLProcessor; +import org.springframework.security.saml.trust.X509KeyManager; +import org.springframework.security.saml.trust.X509TrustManager; +import org.springframework.stereotype.Component; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.opensaml.common.impl.SecureRandomIdentifierGenerator; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.binding.encoding.HTTPSOAP11Encoder; +import org.opensaml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.security.MetadataCriteria; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.extern.slf4j.Slf4j; + +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.Base64; + +@Slf4j +@Component +public class SAMLRenewSecurityTokenBuilder { + + private String renewEndpointUrl = "https://samlservices.test.epr.fed.hin.ch/saml/2.0/renewassertion"; + //private String renewEndpointUrl = "https://test.ahdis.ch/eprik-cara/camel/hin/ahdis/saml/2.0/renewassertion"; + + private static SecureRandomIdentifierGenerator secureRandomIdGenerator; + + private String destination; + + static { + try { + secureRandomIdGenerator = new SecureRandomIdentifierGenerator(); + } catch (NoSuchAlgorithmException e) { + log.error(e.getMessage(), e); + } + } + + + @Autowired + private KeyManager keyManager; + + @Autowired + private SAMLContextProvider contextProvider; + + @Autowired + SAMLProcessor processor; + + @Value("${mag.iua.idp.key-alias}") + private String keyAlias; + + public Element addSecurityHeader(String input) throws XMLStreamException { + return (Element) StaxUtils.read(new StringReader(input)).getDocumentElement(); + } + + public static Processor keepRequest() { + return exchange -> { + HttpServletRequest request = exchange.getIn(HttpMessage.class).getRequest(); + HttpServletResponse response = exchange.getIn(HttpMessage.class).getResponse(); + exchange.setProperty("request", request); + exchange.setProperty("response", response); + }; + } + + public String requestRenewToken(@Body ch.bfh.ti.i4mi.mag.xua.AssertionRequest request, @ExchangeProperty("request") HttpServletRequest hrequest, @ExchangeProperty("response") HttpServletResponse hresponse) throws Exception { + + SAMLMessageContext context = contextProvider.getLocalAndPeerEntity(hrequest, hresponse); + + Object token = request.getSamlToken(); + + if (token instanceof String) { + token = addSecurityHeader((String) token); + } + + RequestSecurityToken renewSecurityTokenRequestMessage; + + renewSecurityTokenRequestMessage = createSAMLObject(RequestSecurityToken.class); + RequestType requestType = createSAMLObject(RequestType.class); + requestType.setValue(RequestType.RENEW); + renewSecurityTokenRequestMessage.getUnknownXMLObjects().add(requestType); + TokenType tokenType = createSAMLObject(TokenType.class); + tokenType.setValue("http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0"); + renewSecurityTokenRequestMessage.getUnknownXMLObjects().add(tokenType); + + RenewTarget renewTarget = createSAMLObject(RenewTarget.class); + renewTarget.setUnknownXMLObject(unmarshall((Element) token)); + renewSecurityTokenRequestMessage.getUnknownXMLObjects().add(renewTarget); + + + Envelope envelope = createSAMLObject(Envelope.class); + // The body element contains the actual SAML message. + org.opensaml.ws.soap.soap11.Body body = createSAMLObject(org.opensaml.ws.soap.soap11.Body.class); + + String ref = randomId(); + WSSecurityHelper.addWSUId(body, ref); + + //WSSecuritySupport.addWSUId(body, securityModule.buildId()); + body.getUnknownXMLObjects().add(renewSecurityTokenRequestMessage); + envelope.setBody(body); + // Application-specific context information (for example, security or encryption information). + Header header = createSAMLObject(Header.class); + envelope.setHeader(header); + Security security = createSAMLObject(Security.class); + header.getUnknownXMLObjects().add(security); + // Security timestamp defining the lifetime of the message. + Timestamp timestamp = createSAMLObject(Timestamp.class); + timestamp.setWSUId(randomId()); + Created created = createSAMLObject(Created.class); + created.setDateTime(DateTime.now()); + timestamp.setCreated(created); + Expires expires = createSAMLObject(Expires.class); + expires.setDateTime(created.getDateTime().plusSeconds(5*60)); + timestamp.setExpires(expires); + security.getUnknownXMLObjects().add(timestamp); + + X509Certificate publicCertificate = keyManager.getCertificate(keyAlias); + log.info("CERT NOT NULL:"+publicCertificate.toString()); + + BinarySecurityToken binarySecurityToken = createSAMLObject(BinarySecurityToken.class); + binarySecurityToken.setEncodingType(EncodedString.ENCODING_TYPE_BASE64_BINARY); + // Why not use BinarySecurityToken.setValueType()? It was not being valued in the XML by the marshaller. + binarySecurityToken.getUnknownAttributes().put(new QName("ValueType"), WSSecurityConstants.X509_V3); + binarySecurityToken.setValue(encode(publicCertificate)); + security.getUnknownXMLObjects().add(binarySecurityToken); + // Digital signature to verify the identity of the signer. + + + Signature signature = createSAMLObject(Signature.class); + Credential cred = keyManager.getCredential(keyAlias); + signature.setSigningCredential(cred); + signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512); + signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + DocumentInternalIDContentReference timestampReference = + new DocumentInternalIDContentReference(timestamp.getWSUId()); + timestampReference.getTransforms().add(CanonicalizationMethod.EXCLUSIVE); + timestampReference.setDigestAlgorithm("http://www.w3.org/2001/04/xmlenc#sha256"); + signature.getContentReferences().add(timestampReference); + + DocumentInternalIDContentReference bodyReference = + new DocumentInternalIDContentReference(WSSecurityHelper.getWSUId(body)); + bodyReference.getTransforms().add(CanonicalizationMethod.EXCLUSIVE); + bodyReference.setDigestAlgorithm("http://www.w3.org/2001/04/xmlenc#sha256"); + signature.getContentReferences().add(bodyReference); + KeyInfo keyInfo = createSAMLObject(KeyInfo.class); + SecurityTokenReference securityTokenReference = createSAMLObject(SecurityTokenReference.class); + keyInfo.getXMLObjects().add(securityTokenReference); + + + X509Data x509Data = createSAMLObject(X509Data.class); + + x509Data.getX509IssuerSerials().add(KeyInfoHelper.buildX509IssuerSerial( + publicCertificate.getIssuerX500Principal().getName(), publicCertificate.getSerialNumber())); + securityTokenReference.getUnknownXMLObjects().add(x509Data); + + signature.setKeyInfo(keyInfo); + security.getUnknownXMLObjects().add(signature); + + marshall(envelope); + sign(signature); + + // Build the W3C DOM representing the SOAP message. + Element elem = marshall(envelope); + + log.info(StaxUtils.toString(elem)); + + Envelope result = send(renewEndpointUrl, context, envelope); + NodeList lst = result.getBody().getDOM().getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion","Assertion"); + + Node node = lst.item(0); + log.debug("NODE: "+node.toString()); + + StringWriter writer = new StringWriter(); + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.transform(new DOMSource(node), new StreamResult(writer)); + + String xml = writer.toString(); + + return xml; + } + + public static String encode(final X509Certificate certificate) throws CertificateEncodingException { + final Base64.Encoder encoder = Base64.getEncoder(); + + final byte[] rawCrtText = certificate.getEncoded(); + final String encodedCertText = new String(encoder.encode(rawCrtText)); + + return encodedCertText; + } + + public static void sign(Signature signature) { + + try { + Signer.signObject(signature); + } catch (SignatureException e) { + throw new RuntimeException(e); + } + } + + public static T createSAMLObject(final Class clazz) { + T object = null; + try { + XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory(); + QName defaultElementName; + + try { + defaultElementName = (QName)clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); + } catch (NoSuchFieldException e) { + defaultElementName = (QName)clazz.getDeclaredField("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) { + log.error("Exception", e); + log.error("CLASSNAME: "+clazz.getName()); + throw new IllegalArgumentException("Could not create SAML object"); + } + + return object; + } + + public Element marshall(XMLObject object) { + if (object instanceof SignableSAMLObject && ((SignableSAMLObject) object).isSigned() + && object.getDOM() != null) { + return object.getDOM(); + } else { + try { + Marshaller out = Configuration.getMarshallerFactory().getMarshaller(object); + out.marshall(object); + return object.getDOM(); + } catch (MarshallingException e) { + return null; + } + } + } + + public XMLObject unmarshall(Element input) throws UnmarshallingException { + Unmarshaller un = Configuration.getUnmarshallerFactory().getUnmarshaller(input); + return un.unmarshall(input); + } + + + public static String randomId() { + return secureRandomIdGenerator.generateIdentifier(); + } + + + private Envelope send(String targetUrl, SAMLMessageContext context, Envelope envelope) throws SOAPException, CertificateEncodingException, + MarshallingException, SignatureException, IllegalAccessException, org.opensaml.xml.security.SecurityException, URIException, MessageEncodingException { + HttpClientBuilder clientBuilder = new HttpClientBuilder(); + + CriteriaSet criteriaSet = new CriteriaSet(); + criteriaSet.add(new EntityIDCriteria(context.getPeerEntityId())); + criteriaSet.add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS)); + criteriaSet.add(new UsageCriteria(UsageType.UNSPECIFIED)); + + X509TrustManager trustManager = new X509TrustManager(criteriaSet, context.getLocalSSLTrustEngine()); + X509KeyManager manager = new X509KeyManager((X509Credential) keyManager.getCredential("hintls")); + + + clientBuilder.setHttpsProtocolSocketFactory( + new TLSProtocolSocketFactory(manager, trustManager)); + + + HttpClient httpClient = clientBuilder.buildClient(); + HttpSOAPClient soapClient = new HttpSOAPClient(httpClient, new BasicParserPool()); + + BasicSOAPMessageContext soapContext = new BasicSOAPMessageContext(); + soapContext.setOutboundMessage(envelope); + + soapClient.send(targetUrl, soapContext); + + + Envelope soapResponse = (Envelope)soapContext.getInboundMessage(); + + return soapResponse; + } + + +} diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenEndpoint.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenEndpoint.java index 6d3381e1..7f37a1b9 100644 --- a/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenEndpoint.java +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenEndpoint.java @@ -27,6 +27,7 @@ import java.util.Base64; import org.apache.camel.Body; +import org.apache.camel.ExchangeProperty; import org.apache.camel.Header; import org.ehcache.Cache; import org.opensaml.Configuration; @@ -121,9 +122,15 @@ public OAuth2TokenResponse handle( String encoded = Base64.getEncoder().encodeToString(assertion.getBytes("UTF-8")); log.debug("Encoded token: "+encoded); + String idpAssertion = request.getIdpAssertion(); + String encodedIdp = Base64.getEncoder().encodeToString(idpAssertion.getBytes("UTF-8")); + OAuth2TokenResponse result = new OAuth2TokenResponse(); result.setAccess_token(encoded); + + result.setRefresh_token(encodedIdp); result.setExpires_in(this.computeExpiresInFromNotOnOrAfter(assertion)); // In seconds + result.setScope(request.getScope()); result.setToken_type("Bearer" /*request.getToken_type()*/); return result; @@ -143,14 +150,18 @@ public static String sha256ThenBase64(String input) throws AuthException { } } - public OAuth2TokenResponse handleFromIdp(@Body String assertion, @Header("scope") String scope) - throws UnsupportedEncodingException, XMLParserException, UnmarshallingException { + public OAuth2TokenResponse handleFromIdp(@ExchangeProperty("oauthrequest") AuthenticationRequest authRequest, @Body String assertion, @Header("scope") String scope) throws UnsupportedEncodingException, AuthException, XMLParserException, UnmarshallingException { String encoded = Base64.getEncoder().encodeToString(assertion.getBytes("UTF-8")); OAuth2TokenResponse result = new OAuth2TokenResponse(); result.setAccess_token(encoded); + + String idpAssertion = authRequest.getIdpAssertion(); + String encodedIdp = Base64.getEncoder().encodeToString(idpAssertion.getBytes("UTF-8")); + result.setRefresh_token(encodedIdp); result.setExpires_in(this.computeExpiresInFromNotOnOrAfter(assertion)); // In seconds + result.setScope(scope); result.setToken_type("Bearer" /*request.getToken_type()*/); return result; diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenRenew.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenRenew.java new file mode 100644 index 00000000..344811df --- /dev/null +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenRenew.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.bfh.ti.i4mi.mag.xua; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.util.UUID; + +import javax.xml.soap.MessageFactory; +import javax.xml.soap.MimeHeaders; +import javax.xml.soap.SOAPConstants; +import javax.xml.soap.SOAPElement; +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPMessage; +import javax.xml.stream.XMLStreamException; + +import org.apache.camel.Body; +import org.apache.camel.ExchangeProperty; +import org.apache.camel.Header; +import org.apache.cxf.staxutils.StaxUtils; +import org.apache.xml.security.utils.XMLUtils; +import org.opensaml.xml.util.XMLHelper; +import org.springframework.stereotype.Component; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; + +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.namespace.QName; + +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; + + + +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +public class TokenRenew { + + + public AssertionRequest buildAssertionRequest(@Header("assertionRequest") AssertionRequest assertionRequest, @Body String renewedIdpAssertion) { + assertionRequest.setSamlToken(renewedIdpAssertion); + return assertionRequest; + } + + public AuthenticationRequest emptyAuthRequest() { + return new AuthenticationRequest(); + } + + public AssertionRequest keepIdpAssertion(@ExchangeProperty("oauthrequest") AuthenticationRequest authRequest, @Body AssertionRequest assertionRequest) { + String idpAssertion; + if (assertionRequest.getSamlToken() instanceof String) { + idpAssertion = (String) assertionRequest.getSamlToken(); + } else { + idpAssertion = XMLHelper.nodeToString((Node) assertionRequest.getSamlToken()); + } + authRequest.setIdpAssertion(idpAssertion); + return assertionRequest; + } +} diff --git a/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenRenewRouteBuilder.java b/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenRenewRouteBuilder.java new file mode 100644 index 00000000..e92a57ab --- /dev/null +++ b/src/main/java/ch/bfh/ti/i4mi/mag/xua/TokenRenewRouteBuilder.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.bfh.ti.i4mi.mag.xua; + +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.cxf.common.message.CxfConstants; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class TokenRenewRouteBuilder extends RouteBuilder { + + public static final String RENEW_PATH = "renew"; + + @Value("${mag.iua.ap.url}") + private String assertionEndpointUrl; + + @Value("${mag.iua.ap.wsdl}") + private String wsdl; + + @Value("${mag.iua.ap.endpoint-name:}") + private String endpointName; + + + @Value("${mag.client-ssl.enabled}") + private boolean clientSsl; + + + @Override + public void configure() throws Exception { + + final String assertionEndpoint = String.format("cxf://%s?dataFormat=CXF_MESSAGE&wsdlURL=%s&loggingFeatureEnabled=true"+ + ((endpointName!=null && endpointName.length()>0) ? "&endpointName="+endpointName : "")+ + "&inInterceptors=#soapResponseLogger" + + "&inFaultInterceptors=#soapResponseLogger"+ + "&outInterceptors=#soapRequestLogger" + + "&outFaultInterceptors=#soapRequestLogger"+ + (clientSsl ? "&sslContextParameters=#sslContext" : ""), + assertionEndpointUrl, wsdl); + + log.info("Assertion-URL: "+assertionEndpoint); + + from(String.format("servlet://%s?httpMethodRestrict=POST&matchOnUriPrefix=true", RENEW_PATH)) + .routeId("renewEndpoint") + .process(SAMLRenewSecurityTokenBuilder.keepRequest()) + .setProperty("oauthrequest").method(TokenRenew.class, "emptyAuthRequest") + .doTry() + .bean(AuthRequestConverter.class, "buildAssertionRequestFromToken") + .setHeader("assertionRequest", body()) + .bean(SAMLRenewSecurityTokenBuilder.class, "requestRenewToken") + + .bean(TokenRenew.class, "buildAssertionRequest") + .bean(TokenRenew.class, "keepIdpAssertion") + .bean(Iti40RequestGenerator.class, "buildAssertion") + .removeHeaders("*", "scope") + .setHeader(CxfConstants.OPERATION_NAME, + constant("Issue")) + .setHeader(CxfConstants.OPERATION_NAMESPACE, + constant("http://docs.oasis-open.org/ws-sx/ws-trust/200512/wsdl")) + .to(assertionEndpoint) + .bean(AssertionExtractor.class) + .bean(TokenEndpoint.class, "handleFromIdp") + + .doCatch(AuthException.class) + .setBody(simple("${exception}")) + .bean(TokenEndpoint.class, "handleError") + .setHeader(Exchange.HTTP_RESPONSE_CODE, simple("${exception.status}")) + .end() + .removeHeaders("*", Exchange.HTTP_RESPONSE_CODE) + .setHeader("Cache-Control", constant("no-store")) + .setHeader("Pragma", constant("no-cache")) + .marshal() + .json(); + } +}