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 0acfe4ac5..9f95273c7 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,7 +5,6 @@ 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.security.Principal; import eu.eudat.types.ApiMessageCode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -18,7 +17,7 @@ import java.nio.charset.StandardCharsets; @RestController @CrossOrigin -@RequestMapping(value = {"/api/saml2/metadata"}) +@RequestMapping(value = {"/api/saml2/"}) public class Saml2MetadataController extends BaseController { private final ConfigLoader configLoader; @@ -29,7 +28,7 @@ public class Saml2MetadataController extends BaseController { this.configLoader = configLoader; } - @RequestMapping(method = RequestMethod.GET, value = {"/{configurableProviderId}"}) + @RequestMapping(method = RequestMethod.GET, value = {"metadata/{configurableProviderId}"}) public @ResponseBody ResponseEntity getMetadata(@PathVariable String configurableProviderId) { Saml2ConfigurableProvider saml2ConfigurableProvider = (Saml2ConfigurableProvider) this.configLoader.getConfigurableProviders().getProviders().stream() @@ -55,5 +54,25 @@ public class Saml2MetadataController extends BaseController { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ResponseItem().status(ApiMessageCode.ERROR_MESSAGE).message("Failed to fetch metadata.")); } } + @RequestMapping(method = RequestMethod.GET, value = {"authnRequest/{configurableProviderId}"}) + public @ResponseBody + 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)); + } + catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ResponseItem().status(ApiMessageCode.ERROR_MESSAGE).message("Failed to create authentication request.")); + } + + } + else { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ResponseItem().status(ApiMessageCode.ERROR_MESSAGE).message("Unknown provider.")); + } + } } 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 dd6b77294..454557103 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 @@ -794,4 +794,85 @@ public class Saml2SSOUtils { } + public static String getAuthnRequest(Saml2ConfigurableProvider provider) throws Exception { + + AuthnRequest authnRequest = buildAuthnRequest(provider); + + String authnRequestXml = null; + DocumentBuilder builder; + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + 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_DSA_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(); + Document document = builder.newDocument(); + Marshaller out = registry.getMarshallerFactory().getMarshaller(authnRequest); + out.marshall(authnRequest, document); + + if(provider.isAuthnRequestsSigned()) { + Signer.signObject(signature); + } + + authnRequestXml = XmlBuilder.generateXml(document); + + } + catch (MarshallingException | ParserConfigurationException e) { + logger.error(e.getMessage(), e); + } + return authnRequestXml; + + } + + private static AuthnRequest buildAuthnRequest(Saml2ConfigurableProvider provider) throws Exception { + AuthnRequest authnRequest = (AuthnRequest) buildXMLObject(AuthnRequest.DEFAULT_ELEMENT_NAME); + authnRequest.setIssueInstant(Instant.now()); + authnRequest.setDestination(provider.getIdpUrl()); + switch (provider.getBinding()) { + case "Redirect": + authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + break; + case "Artifact": + authnRequest.setProtocolBinding(SAMLConstants.SAML2_ARTIFACT_BINDING_URI); + break; + case "Post": + authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); + break; + } + authnRequest.setAssertionConsumerServiceURL(provider.getAssertionConsumerServiceUrl()); + authnRequest.setID(UUID.randomUUID().toString()); + authnRequest.setIssuer(buildIssuer(provider.getSpEntityId())); + authnRequest.setNameIDPolicy(buildNameIdPolicy()); + + return authnRequest; + } + + private static NameIDPolicy buildNameIdPolicy() throws Exception { + NameIDPolicy nameIDPolicy = (NameIDPolicy) buildXMLObject(NameIDPolicy.DEFAULT_ELEMENT_NAME); + nameIDPolicy.setAllowCreate(true); + nameIDPolicy.setFormat(NameIDType.TRANSIENT); + + return nameIDPolicy; + } + + private static Issuer buildIssuer(String spEntityId) throws Exception { + Issuer issuer = (Issuer) buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue(spEntityId); + + return issuer; + } + } \ 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 b0802de76..c3db23658 100644 --- a/dmp-frontend/src/app/core/services/saml-login.service.ts +++ b/dmp-frontend/src/app/core/services/saml-login.service.ts @@ -1,18 +1,21 @@ +import { HttpHeaders, HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Guid } from '@common/types/guid'; -import * as pk from 'pako'; +import { ConfigurationService } from './configuration/configuration.service'; +import { BaseHttpService } from './http/base-http.service'; +import { Observable } from 'rxjs'; +import { BaseComponent } from '@common/base/base.component'; @Injectable() -export class SamlLoginService { +export class SamlLoginService extends BaseComponent { - constructor() {} + private actionUrl: string; + private headers = new HttpHeaders(); + + constructor(private http: BaseHttpService, private httpClient: HttpClient, private configurationService: ConfigurationService) { + super(); + this.actionUrl = configurationService.server + 'saml2/'; + } - buildRelayState(spId: string, configurableLoginId: string): string { - let uri = 'spId=' + spId; - uri += '&configurableLoginId=' + configurableLoginId; - return encodeURIComponent(uri); - } - resolveConfigurableLoginId(relayState: string): string { const decoded = decodeURIComponent(relayState); const routeParams = new URLSearchParams(decoded); @@ -23,30 +26,9 @@ export class SamlLoginService { const routeParams = new URLSearchParams(decoded); return routeParams.has('spId') ? routeParams.get('spId') : ''; } - - getSamlLoginUrl(spEntityID: string, idpUrl: string, binding: string, assertionConsumerServiceUrl: string, configurableLoginId: string) { - const now = new Date(); - let protocolBinding = ''; - switch (binding) { - case "Redirect": protocolBinding = 'ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" '; break; - case "Artifact": protocolBinding = 'ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" '; break; - case "Post": protocolBinding = 'ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" '; break; - } - const authenticationRequest = '' + - '' + spEntityID + '' + - ''; - const uint = new Uint8Array(authenticationRequest.length); - for (let i = 0, j = authenticationRequest.length; i < j; ++i) { - uint[i] = authenticationRequest.charCodeAt(i); - } - const base64String = btoa(pk.deflateRaw(uint, { to: 'string' })); - const relayState = this.buildRelayState(spEntityID, configurableLoginId); - const url = idpUrl + '?RelayState=' + relayState + '&SAMLRequest=' + encodeURIComponent(base64String); - return url; + + 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 e0ed91f84..4997571dd 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 @@ -8,6 +8,7 @@ import { ConfigurableProvidersService } from '@app/ui/auth/login/utilities/confi import { LoginService } from '@app/ui/auth/login/utilities/login.service'; import { BaseComponent } from '@common/base/base.component'; import { environment } from 'environments/environment'; +import * as pk from 'pako'; import { takeUntil } from 'rxjs/operators'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; import { SamlLoginService } from '@app/core/services/saml-login.service'; @@ -86,10 +87,28 @@ export class ConfigurableLoginComponent extends BaseComponent implements OnInit } else if(this.provider.type === ConfigurableProviderType.Saml2){ let provider = this.provider as Saml2ConfigurableProvider; - window.location.href = this.samlLoginService.getSamlLoginUrl(provider.spEntityId, provider.idpUrl, provider.binding, provider.assertionConsumerServiceUrl, provider.configurableLoginId); + 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 base64String = btoa(pk.deflateRaw(uint, { to: 'string' })); + const relayState = this.buildRelayState(provider.spEntityId, provider.configurableLoginId); + const url = provider.idpUrl + '?RelayState=' + relayState + '&SAMLRequest=' + encodeURIComponent(base64String); + 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'])