From b6505cda4ef46eecfe2121eb883d61024787d185 Mon Sep 17 00:00:00 2001 From: Aldo Mihasi Date: Wed, 21 Jun 2023 11:51:26 +0300 Subject: [PATCH] refactor signing of saml2 authentication request --- .../controllers/Saml2MetadataController.java | 7 +- .../configurableProvider/Saml2SSOUtils.java | 76 ++++++++++++------- .../models/data/saml2/AuthnRequestModel.java | 47 ++++++++++++ .../app/core/model/saml2/AuthnRequestModel.ts | 6 ++ .../app/core/services/saml-login.service.ts | 5 +- .../configurable-login.component.ts | 15 +--- 6 files changed, 112 insertions(+), 44 deletions(-) create mode 100644 dmp-backend/web/src/main/java/eu/eudat/models/data/saml2/AuthnRequestModel.java create mode 100644 dmp-frontend/src/app/core/model/saml2/AuthnRequestModel.ts diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/Saml2MetadataController.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/Saml2MetadataController.java index 9f95273c7..4563e2f9f 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/controllers/Saml2MetadataController.java +++ b/dmp-backend/web/src/main/java/eu/eudat/controllers/Saml2MetadataController.java @@ -5,6 +5,7 @@ import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.sam import eu.eudat.logic.security.validators.configurableProvider.Saml2SSOUtils; import eu.eudat.logic.services.ApiContext; import eu.eudat.models.data.helpers.responses.ResponseItem; +import eu.eudat.models.data.saml2.AuthnRequestModel; import eu.eudat.types.ApiMessageCode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -56,14 +57,14 @@ public class Saml2MetadataController extends BaseController { } @RequestMapping(method = RequestMethod.GET, value = {"authnRequest/{configurableProviderId}"}) public @ResponseBody - ResponseEntity> getAuthnRequest(@PathVariable String configurableProviderId) { + ResponseEntity getAuthnRequest(@PathVariable String configurableProviderId) { Saml2ConfigurableProvider saml2ConfigurableProvider = (Saml2ConfigurableProvider) this.configLoader.getConfigurableProviders().getProviders().stream() .filter(prov -> prov.getConfigurableLoginId().equals(configurableProviderId)) .findFirst().orElse(null); if (saml2ConfigurableProvider != null) { try { - String authnRequestXml = Saml2SSOUtils.getAuthnRequest(saml2ConfigurableProvider); - return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload(authnRequestXml)); + AuthnRequestModel authnRequest = Saml2SSOUtils.getAuthnRequest(saml2ConfigurableProvider); + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload(authnRequest)); } catch (Exception e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ResponseItem().status(ApiMessageCode.ERROR_MESSAGE).message("Failed to create authentication request.")); diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/Saml2SSOUtils.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/Saml2SSOUtils.java index 5f31b65a2..862996ea6 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/Saml2SSOUtils.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/Saml2SSOUtils.java @@ -3,6 +3,7 @@ package eu.eudat.logic.security.validators.configurableProvider; import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2.CertificateInfo; import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2.Saml2ConfigurableProvider; import eu.eudat.logic.utilities.builders.XmlBuilder; +import eu.eudat.models.data.saml2.AuthnRequestModel; import jakarta.xml.soap.*; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.resolver.CriteriaSet; @@ -34,7 +35,6 @@ import org.opensaml.core.xml.config.XMLObjectProviderRegistry; import org.opensaml.core.xml.io.*; import org.opensaml.core.xml.schema.*; import org.opensaml.saml.common.SAMLObject; -import org.opensaml.saml.common.SAMLObjectContentReference; import org.opensaml.saml.common.SAMLVersion; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.criterion.EntityRoleCriterion; @@ -64,8 +64,6 @@ import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory; import org.opensaml.xmlsec.signature.KeyInfo; import org.opensaml.xmlsec.signature.Signature; import org.opensaml.xmlsec.signature.X509Data; -import org.opensaml.xmlsec.signature.impl.SignatureBuilder; -import org.opensaml.xmlsec.signature.support.SignatureConstants; import org.opensaml.xmlsec.signature.support.SignatureValidator; import org.opensaml.xmlsec.signature.support.Signer; import org.slf4j.Logger; @@ -88,6 +86,7 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.*; +import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.security.*; @@ -794,47 +793,68 @@ public class Saml2SSOUtils { } - public static String getAuthnRequest(Saml2ConfigurableProvider provider) throws Exception { + public static AuthnRequestModel getAuthnRequest(Saml2ConfigurableProvider provider) throws Exception { AuthnRequest authnRequest = buildAuthnRequest(provider); + String relayState = "spId=" + provider.getSpEntityId() + "&configurableLoginId=" + provider.getConfigurableLoginId(); String authnRequestXml = null; - DocumentBuilder builder; - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + String signatureBase64 = null; try { - Signature signature = (Signature) buildXMLObject(Signature.DEFAULT_ELEMENT_NAME); - if(provider.isAuthnRequestsSigned()){ - - Credential credential = getCredential(provider.getSigningCert()); - signature.setSigningCredential(credential); - signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); - signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); - - X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory(); - keyInfoGeneratorFactory.setEmitEntityCertificate(true); - KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); - signature.setKeyInfo(keyInfoGenerator.generate(getCredential(provider.getSigningCert()))); - - authnRequest.setSignature(signature); - } - - builder = factory.newDocumentBuilder(); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.newDocument(); Marshaller out = registry.getMarshallerFactory().getMarshaller(authnRequest); out.marshall(authnRequest, document); - if(provider.isAuthnRequestsSigned()) { - Signer.signObject(signature); - } - authnRequestXml = XmlBuilder.generateXml(document); + if(provider.isAuthnRequestsSigned()) { + signatureBase64 = buildSignature(authnRequestXml, relayState, provider.getSigningCert()); + } } catch (MarshallingException | ParserConfigurationException e) { logger.error(e.getMessage(), e); } - return authnRequestXml; + AuthnRequestModel authnRequestModel = new AuthnRequestModel(); + authnRequestModel.setAuthnRequestXml(authnRequestXml); + authnRequestModel.setRelayState(relayState); + authnRequestModel.setAlgorithm("http://www.w3.org/2000/09/xmldsig#rsa-sha1"); + authnRequestModel.setSignature(signatureBase64); + return authnRequestModel; + + } + + private static String buildSignature(String authnRequest, String relayState, CertificateInfo signingCertInfo) throws Exception{ + + KeyStore ks = (signingCertInfo.getKeyFormat().getType().equals("JKS")) ? KeyStore.getInstance("JKS") : KeyStore.getInstance("PKCS12"); + String archivePassword = signingCertInfo.getKeystorePassword(); + char[] pwdArray = (archivePassword != null) ? archivePassword.toCharArray() : "changeit".toCharArray(); + ks.load(new FileInputStream(signingCertInfo.getKeystorePath()), pwdArray); + PrivateKey pk = (PrivateKey) ks.getKey(signingCertInfo.getAlias(), signingCertInfo.getPassword().toCharArray()); + + String signAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + String message = "SAMLRequest=" + URLEncoder.encode(authnRequest, "UTF-8") + + "&RelayState=" + URLEncoder.encode(relayState, "UTF-8") + + "&SigAlg=" + URLEncoder.encode(signAlgorithm, "UTF-8"); + + String signature = null; + try{ + signature = new String(org.apache.commons.codec.binary.Base64.encodeBase64(sign(message, pk)), StandardCharsets.UTF_8); + } + catch(InvalidKeyException | SignatureException | NoSuchAlgorithmException e){ + logger.error(e.getMessage(), e); + } + + return signature; + } + + private static byte[] sign(String message, PrivateKey key) throws InvalidKeyException, SignatureException, NoSuchAlgorithmException { + java.security.Signature instance = java.security.Signature.getInstance("SHA1withRSA"); + instance.initSign(key); + instance.update(message.getBytes()); + return instance.sign(); } private static AuthnRequest buildAuthnRequest(Saml2ConfigurableProvider provider) throws Exception { diff --git a/dmp-backend/web/src/main/java/eu/eudat/models/data/saml2/AuthnRequestModel.java b/dmp-backend/web/src/main/java/eu/eudat/models/data/saml2/AuthnRequestModel.java new file mode 100644 index 000000000..90ba44eb9 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/models/data/saml2/AuthnRequestModel.java @@ -0,0 +1,47 @@ +package eu.eudat.models.data.saml2; + +public class AuthnRequestModel { + + String authnRequestXml; + String relayState; + String algorithm; + String signature; + + public AuthnRequestModel() {} + + public AuthnRequestModel(String authnRequestXml, String relayState, String algorithm, String signature) { + this.authnRequestXml = authnRequestXml; + this.relayState = relayState; + this.algorithm = algorithm; + this.signature = signature; + } + + public String getAuthnRequestXml() { + return authnRequestXml; + } + public void setAuthnRequestXml(String authnRequestXml) { + this.authnRequestXml = authnRequestXml; + } + + public String getRelayState() { + return relayState; + } + public void setRelayState(String relayState) { + this.relayState = relayState; + } + + public String getAlgorithm() { + return algorithm; + } + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String getSignature() { + return signature; + } + public void setSignature(String signature) { + this.signature = signature; + } + +} diff --git a/dmp-frontend/src/app/core/model/saml2/AuthnRequestModel.ts b/dmp-frontend/src/app/core/model/saml2/AuthnRequestModel.ts new file mode 100644 index 000000000..683dba6fa --- /dev/null +++ b/dmp-frontend/src/app/core/model/saml2/AuthnRequestModel.ts @@ -0,0 +1,6 @@ +export interface AuthnRequestModel { + authnRequestXml: string; + relayState: string; + algorithm: string; + signature: string; +} \ No newline at end of file diff --git a/dmp-frontend/src/app/core/services/saml-login.service.ts b/dmp-frontend/src/app/core/services/saml-login.service.ts index c3db23658..d052205d6 100644 --- a/dmp-frontend/src/app/core/services/saml-login.service.ts +++ b/dmp-frontend/src/app/core/services/saml-login.service.ts @@ -4,6 +4,7 @@ import { ConfigurationService } from './configuration/configuration.service'; import { BaseHttpService } from './http/base-http.service'; import { Observable } from 'rxjs'; import { BaseComponent } from '@common/base/base.component'; +import { AuthnRequestModel } from '../model/saml2/AuthnRequestModel'; @Injectable() export class SamlLoginService extends BaseComponent { @@ -27,8 +28,8 @@ export class SamlLoginService extends BaseComponent { return routeParams.has('spId') ? routeParams.get('spId') : ''; } - getAuthnRequest(configurableLoginId: string): Observable { - return this.http.get(this.actionUrl + 'authnRequest/' + configurableLoginId, { headers: this.headers }); + getAuthnRequest(configurableLoginId: string): Observable { + return this.http.get(this.actionUrl + 'authnRequest/' + configurableLoginId, { headers: this.headers }); } } \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/auth/login/configurable-login/configurable-login.component.ts b/dmp-frontend/src/app/ui/auth/login/configurable-login/configurable-login.component.ts index 4997571dd..00902cdfa 100644 --- a/dmp-frontend/src/app/ui/auth/login/configurable-login/configurable-login.component.ts +++ b/dmp-frontend/src/app/ui/auth/login/configurable-login/configurable-login.component.ts @@ -90,25 +90,18 @@ export class ConfigurableLoginComponent extends BaseComponent implements OnInit this.samlLoginService.getAuthnRequest(provider.configurableLoginId).pipe(takeUntil(this._destroyed)) .subscribe( authenticationRequest => { - const uint = new Uint8Array(authenticationRequest.length); - for (let i = 0, j = authenticationRequest.length; i < j; ++i) { - uint[i] = authenticationRequest.charCodeAt(i); + const uint = new Uint8Array(authenticationRequest.authnRequestXml.length); + for (let i = 0, j = authenticationRequest.authnRequestXml.length; i < j; ++i) { + uint[i] = authenticationRequest.authnRequestXml.charCodeAt(i); } const base64String = btoa(pk.deflateRaw(uint, { to: 'string' })); - const relayState = this.buildRelayState(provider.spEntityId, provider.configurableLoginId); - const url = provider.idpUrl + '?RelayState=' + relayState + '&SAMLRequest=' + encodeURIComponent(base64String); + const url = provider.idpUrl + '?SAMLRequest=' + encodeURIComponent(base64String) + '&RelayState=' + encodeURIComponent(authenticationRequest.relayState) + '&SigAlg=' + encodeURIComponent(authenticationRequest.algorithm) + '&Signature=' + encodeURIComponent(authenticationRequest.signature); window.location.href = url; } ); } } - buildRelayState(spId: string, configurableLoginId: string): string { - let uri = 'spId=' + spId; - uri += '&configurableLoginId=' + configurableLoginId; - return encodeURIComponent(uri); - } - public configurableLoginUser(code: string, state: string) { if (state !== (this.provider).state) { this.router.navigate(['/login'])