/// <summary> /// Handles the sign in. /// </summary> /// <returns></returns> /// <exception cref="SecurityTokenException">No token validator was found for the given token.</exception> private async Task <HandleRequestResult> HandleSignIn() { if (Request.Method != HttpMethods.Post) { return(HandleRequestResult.Fail("Request method must be an HTTP-Post Method")); } var form = await Request.ReadFormAsync(); var response = form[Saml2Constants.Parameters.SamlResponse]; var relayState = form[Saml2Constants.Parameters.RelayState].ToString()?.DeflateDecompress(); AuthenticationProperties authenticationProperties = Options.StateDataFormat.Unprotect(relayState); try { if (authenticationProperties == null) { if (!Options.AllowUnsolicitedLogins) { return(HandleRequestResult.Fail("Unsolicited logins are not allowed.")); } } if (authenticationProperties.Items.TryGetValue(CorrelationProperty, out string correlationId) && !ValidateCorrelationId(authenticationProperties)) { return(HandleRequestResult.Fail("Correlation failed.", authenticationProperties)); } string base64EncodedSamlResponse = response; ResponseType idpSamlResponseToken = _saml2Service.GetSamlResponseToken(base64EncodedSamlResponse, Saml2Constants.ResponseTypes.AuthnResponse, Options); IRequestCookieCollection cookies = Request.Cookies; string originalSamlRequestId = cookies[cookies.Keys.FirstOrDefault(key => key.StartsWith(Options.AuthenticationScheme))]; _saml2Service.CheckIfReplayAttack(idpSamlResponseToken.InResponseTo, originalSamlRequestId); _saml2Service.CheckStatus(idpSamlResponseToken); string token = _saml2Service.GetAssertion(idpSamlResponseToken, Options); AssertionType assertion = new AssertionType(); XmlSerializer xmlSerializer = new XmlSerializer(typeof(AssertionType)); using (MemoryStream memStm = new MemoryStream(Encoding.UTF8.GetBytes(token))) { assertion = (AssertionType)xmlSerializer.Deserialize(memStm); } if (Options.WantAssertionsSigned) { var doc = new XmlDocument { XmlResolver = null, PreserveWhitespace = true }; doc.LoadXml(token); if (!_saml2Service.ValidateX509CertificateSignature(doc, Options)) { throw new Exception("Assertion signature is not valid"); } } AuthnStatementType session = new AuthnStatementType(); if (assertion.Items.Any(x => x.GetType() == typeof(AuthnStatementType))) { session = (AuthnStatementType)assertion.Items.FirstOrDefault(x => x.GetType() == typeof(AuthnStatementType)); } if (assertion.Subject.Items.Any(x => x.GetType() == typeof(NameIDType))) { Options.NameIDType = (NameIDType)assertion.Subject.Items.FirstOrDefault(x => x.GetType() == typeof(NameIDType)); } if (_configuration == null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } var tvp = Options.TokenValidationParameters.Clone(); var validator = Options.Saml2SecurityTokenHandler; ClaimsPrincipal principal = null; SecurityToken parsedToken = null; var issuers = new[] { _configuration.Issuer }; tvp.ValidateIssuerSigningKey = Options.WantAssertionsSigned; tvp.ValidateTokenReplay = !Options.IsPassive; tvp.ValidateIssuer = true; tvp.ValidateAudience = true; tvp.ValidIssuers = (tvp.ValidIssuers == null ? issuers : tvp.ValidIssuers.Concat(issuers)); tvp.IssuerSigningKeys = (tvp.IssuerSigningKeys == null ? _configuration.SigningKeys : tvp.IssuerSigningKeys.Concat(_configuration.SigningKeys)); if (!Options.WantAssertionsSigned) // in case they aren't signed { tvp.RequireSignedTokens = false; } if (validator.CanReadToken(token)) { principal = validator.ValidateToken(token, tvp, out parsedToken); } if (principal == null) { throw new SecurityTokenException("No token validator was found for the given token."); } if (Options.UseTokenLifetime && parsedToken != null) { // Override any session persistence to match the token lifetime. var issued = parsedToken.ValidFrom; if (issued != DateTime.MinValue) { authenticationProperties.IssuedUtc = issued.ToUniversalTime(); } var expires = parsedToken.ValidTo; if (expires != DateTime.MinValue) { authenticationProperties.ExpiresUtc = expires.ToUniversalTime(); } authenticationProperties.AllowRefresh = false; } ClaimsIdentity identity = new ClaimsIdentity(principal.Claims, Scheme.Name); session.SessionIndex = !String.IsNullOrEmpty(session.SessionIndex) ? session.SessionIndex : assertion.ID; //get the session index from assertion so you can use it to logout later identity.AddClaim(new Claim(Saml2ClaimTypes.SessionIndex, session.SessionIndex)); identity.AddClaim(new Claim(ClaimTypes.Name, principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value)); string redirectUrl = !string.IsNullOrEmpty(authenticationProperties.RedirectUri) ? authenticationProperties.RedirectUri : Options.CallbackPath.ToString(); Context.Response.Redirect(redirectUrl, true); Context.User = new ClaimsPrincipal(identity); await Context.SignInAsync(Options.SignInScheme, Context.User, authenticationProperties); return(HandleRequestResult.Success(new AuthenticationTicket(Context.User, authenticationProperties, Scheme.Name))); } catch (Exception exception) { return(HandleRequestResult.Fail(exception, authenticationProperties)); } }
/// <summary> /// Handles the sign in. /// </summary> /// <returns></returns> /// <exception cref="SecurityTokenException">No token validator was found for the given token.</exception> private async Task <HandleRequestResult> HandleSignIn() { if (Request.Method != HttpMethods.Post) { return(HandleRequestResult.Fail("Request method must be an HTTP-Post Method")); } var form = await Request.ReadFormAsync(); var response = form[Saml2Constants.Parameters.SamlResponse]; var relayState = form[Saml2Constants.Parameters.RelayState].ToString()?.DeflateDecompress(); AuthenticationProperties authenticationProperties = Options.StateDataFormat.Unprotect(relayState); try { if (authenticationProperties == null) { if (!Options.AllowUnsolicitedLogins) { return(HandleRequestResult.Fail("Unsolicited logins are not allowed.")); } } if (authenticationProperties.Items.TryGetValue(CorrelationProperty, out string correlationId) && !ValidateCorrelationId(authenticationProperties)) { return(HandleRequestResult.Fail("Correlation failed.", authenticationProperties)); } string base64EncodedSamlResponse = response; ResponseType idpSamlResponseToken = _saml2Service.GetSamlResponseToken(base64EncodedSamlResponse, Saml2Constants.ResponseTypes.AuthnResponse, Options); // Write To XML Doc `_xmlDoc` Object, This Step Is Critical For The ValidateToken Call Below. LoadXmlFromBase64(response); IRequestCookieCollection cookies = Request.Cookies; string originalSamlRequestId = cookies[cookies.Keys.FirstOrDefault(key => key.StartsWith(Options.AuthenticationScheme))]; _saml2Service.CheckIfReplayAttack(idpSamlResponseToken.InResponseTo, originalSamlRequestId); _saml2Service.CheckStatus(idpSamlResponseToken); string token = _saml2Service.GetAssertion(idpSamlResponseToken, Options); AssertionType assertion = new AssertionType(); XmlSerializer xmlSerializer = new XmlSerializer(typeof(AssertionType)); using (MemoryStream memStm = new MemoryStream(Encoding.UTF8.GetBytes(token))) { assertion = (AssertionType)xmlSerializer.Deserialize(memStm); } if (Options.WantAssertionsSigned) { var doc = new XmlDocument { XmlResolver = null, PreserveWhitespace = true }; doc.LoadXml(token); if (!_saml2Service.ValidateX509CertificateSignature(doc, Options)) { throw new Exception("Assertion signature is not valid"); } } AuthnStatementType session = new AuthnStatementType(); if (assertion.Items.Any(x => x.GetType() == typeof(AuthnStatementType))) { session = (AuthnStatementType)assertion.Items.FirstOrDefault(x => x.GetType() == typeof(AuthnStatementType)); } if (assertion.Subject.Items.Any(x => x.GetType() == typeof(NameIDType))) { Options.NameIDType = (NameIDType)assertion.Subject.Items.FirstOrDefault(x => x.GetType() == typeof(NameIDType)); } if (_configuration == null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } var tvp = Options.TokenValidationParameters.Clone(); var validator = Options.Saml2SecurityTokenHandler; ClaimsPrincipal principal = null; SecurityToken parsedToken = null; var issuers = new[] { _configuration.Issuer }; tvp.ValidateIssuerSigningKey = Options.WantAssertionsSigned; tvp.ValidateTokenReplay = !Options.IsPassive; tvp.ValidateIssuer = true; tvp.ValidateAudience = true; tvp.ValidIssuers = (tvp.ValidIssuers == null ? issuers : tvp.ValidIssuers.Concat(issuers)); tvp.IssuerSigningKeys = (tvp.IssuerSigningKeys == null ? _configuration.SigningKeys : tvp.IssuerSigningKeys.Concat(_configuration.SigningKeys)); if (!Options.WantAssertionsSigned) // in case they aren't signed { tvp.RequireSignedTokens = false; } if (Options.EnablePIILogging) { IdentityModelEventSource.ShowPII = true; } if (validator.CanReadToken(token)) { // Our Token Is Valid, Now Check Signaures. var doc = new XmlDocument { XmlResolver = null, PreserveWhitespace = true }; doc.LoadXml(token); // Pull Signatures. XmlNodeList XMLSignatures = doc.GetElementsByTagName(Saml2Constants.Parameters.Signature, Saml2Constants.Namespaces.DsNamespace); var signedXmlDoc = new SignedXml(doc); signedXmlDoc.LoadXml((XmlElement)XMLSignatures[0]); KeyInfoX509Data x509data = signedXmlDoc.Signature.KeyInfo.OfType <KeyInfoX509Data>().First(); X509Certificate2 cert = (X509Certificate2)x509data.Certificates[0]; string serialNumber = cert.SerialNumber; X509Certificate2 _idpcert = Options.Configuration.X509Certificate2.Where(c => c.SerialNumber == serialNumber).FirstOrDefault(); var _xmlNameSpaceManager = GetNamespaceManager(); // Manager For XPath Queries. // Important: To Check Signatures We Pull The Signature XML Node. // Critically This Check Differers From The ValidateToken Call Below, See Note. XmlNodeList nodeList = _xmlDoc.SelectNodes(".//ds:Signature", _xmlNameSpaceManager); SignedXml signedXml = new SignedXml(_xmlDoc); signedXml.LoadXml((XmlElement)nodeList[0]); var check_reference = ValidateSignatureReference(signedXml); var check_signature = signedXml.CheckSignature(cert, true); if (check_reference && check_signature) { // Tech Note: At This Point We've: // 1. Verified The Token. // 2. Verified The Signature Reference. // 3. Verified The Signature For The Entire Document, And The Specific Assertion. // The Final Step Is To Create Our Security Principal Via The ValidateToken() Call. // The Big "Trick" Here Is This Call Is Known To Fail When The XML Source Is Not .NET. // Specifically, The Signature Validation Process Requires The XML Byte Stream To Be Identical, // And Unfortunately Our Token Call Above, Among Other Things, Strips XML Namespaces And Changes Line Feeds. // To Address This We Pass This Call The "Original" XML From The LoadXmlFromBase64() Call Above. try { XmlNodeList nodeListAssertion = _xmlDoc.SelectNodes(".//saml:Assertion", _xmlNameSpaceManager); // This Call Maps Our SAML <saml2:AttributeStatement> Items To Our Identity As Claims. principal = validator.ValidateToken(nodeListAssertion[0].OuterXml, tvp, out parsedToken); } catch (Exception e) { } } } if (principal == null) { throw new SecurityTokenException("No token validator was found for the given token."); } if (Options.UseTokenLifetime && parsedToken != null) { // Override any session persistence to match the token lifetime. var issued = parsedToken.ValidFrom; if (issued != DateTime.MinValue) { authenticationProperties.IssuedUtc = issued.ToUniversalTime(); } var expires = parsedToken.ValidTo; if (expires != DateTime.MinValue) { authenticationProperties.ExpiresUtc = expires.ToUniversalTime(); } authenticationProperties.AllowRefresh = false; } ClaimsIdentity identity = new ClaimsIdentity(principal.Claims, Scheme.Name); session.SessionIndex = !String.IsNullOrEmpty(session.SessionIndex) ? session.SessionIndex : assertion.ID; //get the session index from assertion so you can use it to logout later identity.AddClaim(new Claim(Saml2ClaimTypes.SessionIndex, session.SessionIndex)); // Create Entry For User.Identity.Name if (principal.Claims.Any(c => c.Type == ClaimTypes.NameIdentifier)) { identity.AddClaim(new Claim(ClaimTypes.Name, principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value)); } string redirectUrl = !string.IsNullOrEmpty(authenticationProperties.RedirectUri) ? authenticationProperties.RedirectUri : Options.CallbackPath.ToString(); Context.Response.Redirect(redirectUrl, true); Context.User = new ClaimsPrincipal(identity); await Context.SignInAsync(Options.SignInScheme, Context.User, authenticationProperties); return(HandleRequestResult.Success(new AuthenticationTicket(Context.User, authenticationProperties, Scheme.Name))); } catch (Exception exception) { return(HandleRequestResult.Fail(exception, authenticationProperties)); } }