I want to add 3D secure authentication to credit card payments taken through a website. I am using Sitefinity 8, the e-commerce plug-in and SagePay as the payment processor.
I have created a custom payment provider and can successfully redirect users to the 3D secure page. I am able to perform the second authentication call to SagePay using the SagePay integration kit (i.e. externally from the e-commerce plugin). However, I am struggling to find a way to complete the payment due the way the internal e-commerce classes function.
The difficulty is that the order processor treats the payment as declined if 3D secure authentication is required, but there does not seem to be a way to process the order correctly without using the inbuilt functionality. From my inspections of the ecommerce libraries, there seems to be no way to extend or modify these classes due to internal modifiers and concrete implementations.
How can I process the order once I have completed authentiation? Has anyone successfully implemented 3D secure with ecommerce? Or know if it is possible?
This is my custom payment provider at the moment.
public class CustomSagePayProvider : SagePayProvider
{
// Rest of code...
protected override IPaymentResponse ParseReponse(string uniqueTransactionCode, string responseXml)
{
var paymentResponse = base.ParseReponse(uniqueTransactionCode, responseXml);
if (Requires3DSecure(paymentResponse))
{
var responseFields = GetResponseAsDictionary(responseXml);
Set3DSecureFields(responseFields, paymentResponse);
}
return paymentResponse;
}
private bool Requires3DSecure(IPaymentResponse paymentResponse)
{
return paymentResponse.GatewayCSCResponse == "OK";
}
private void Set3DSecureFields(Dictionary<string, string> responseFields, IPaymentResponse paymentResponse)
{
var postValues = new NameValueCollection();
postValues.Add("MD", responseFields.ContainsKey("MD") ? responseFields["MD"] : string.Empty);
postValues.Add("PAReq", responseFields.ContainsKey("PAReq") ? responseFields["PAReq"] : string.Empty);
paymentResponse.GatewayRedirectUrlPostValues = postValues;
paymentResponse.GatewayRedirectUrl = responseFields.ContainsKey("ACSURL") ? responseFields["ACSURL"] : string.Empty;
}
}
And this is the 3D secure payment process using the .NET SagePay integration kit
using SagePay.IntegrationKit;
using SagePay.IntegrationKit.Messages;
// Rest of code
var sagePay = new SagePayIntegration();
IThreeDAuthRequest request = new DataObject();
request.Md = Request.Form["MD"];
request.PaRes = Request.Form["PaRes"];
sagePay.RequestQueryString = sagePay.BuildQueryString(request, ProtocolMessage.THREE_D_AUTH_REQUEST, ProtocolVersion.V_223);
sagePay.ResponseQueryString = sagePay.ProcessWebRequestToSagePay("https://test.sagepay.com/gateway/service/direct3dcallback.vsp", sagePay.RequestQueryString);
var result = sagePay.GetDirectPaymentResult(sagePay.ResponseQueryString);
if (result.Status == ResponseStatus.OK)
{
// Process order
}
I was able to add 3D secure authentication by treating the 2nd authentication call as an offsite payment and adding the IOffsitePaymentProcessorProvider interface to my payment provider class
public class CustomSagePayProvider : SagePayProvider, IOffsitePaymentProcessorProvider
{
// Triggered after payments that have been 3D Secure authenticated
public IPaymentResponse HandleOffsiteNotification(int orderNumber, HttpRequest request, PaymentMethod paymentMethod)
{
var paymentResponse = new PaymentResponse() { IsOffsitePayment = true };
var sagePay = new SagePayIntegration();
var result = sagePay.GetDirectPaymentResult(request.Params.ToString());
if (result.ThreeDSecureStatus == ThreeDSecureStatus.OK)
{
paymentResponse.IsSuccess = true;
paymentResponse.GatewayTransactionID = result.TxAuthNo.ToString();
}
return paymentResponse;
}
public IPaymentResponse HandleOffsiteReturn(int orderNumber, HttpRequest request, PaymentMethod paymentMethod)
{
throw new NotImplementedException();
}
I pass the notification url as a query string parameter in the termUrl value posted to SagePay when first requesting the authentication
(The url must be /Ecommerce/offsite-payment-notification/ to use the inbuilt offside payment notification handler).
var notificationUrl = request.Url.GetLeftPart(UriPartial.Authority) + "/Ecommerce/offsite-payment-notification/";
In the callback from SagePay after the user completes authentication, I use the SagePay integration kit to process the result of the authentication.
var sagePay = new SagePayIntegration();
IThreeDAuthRequest request = new DataObject();
request.Md = md;
request.PaRes = paRes;
sagePay.RequestQueryString = sagePay.BuildQueryString(request, ProtocolMessage.THREE_D_AUTH_REQUEST, ProtocolVersion.V_223);
sagePay.ResponseQueryString = sagePay.ProcessWebRequestToSagePay("https://test.sagepay.com/gateway/service/direct3dcallback.vsp", sagePay.RequestQueryString);
return sagePay.GetDirectPaymentResult(sagePay.ResponseQueryString);
Finally, I trigger the HandleOffsiteNotification event by posting to the url www.mysite.com/Ecommerce/offsite-payment-notification/. This marks the order as complete, updates stock levels and cleans up the user's basket. For simplicity in this example, I am using the SagePay integration kit object to build the query string and post to the url.
var sagePay = new SagePayIntegration();
var ordersManager = OrdersManager.GetManager();
var query = sagePay.ConvertSagePayMessageToNameValueCollection(ProtocolMessage.DIRECT_PAYMENT_RESULT, typeof(IDirectPaymentResult), result, ProtocolVersion.V_223);
// Required Sitefinity fields to trigger HandleOffsiteNotification in IOffsitePaymentProcessorProvider
query.Add("invoice", orderNumber.ToString());
query.Add("provider", ordersManager.Provider.Name);
var queryString = sagePay.BuildQueryString(query);
// Post 3d secure details to this site simulate an offsite payment processor response
sagePay.ProcessWebRequestToSagePay(notificationUrl, queryString);
Related
I'm working on a multilanguage project for accademic purpose. I've written a simple Python Client that make requests to an API server written in ASP.NET. The server retrives spotify info about users. The server interacts with a DB filled by a Golang server that only makes scraping on API's exposed from Spotify. I'm aware that it's a misuse and there are better solutions
Clearly, Golang server, in order to make requests to Spotify API's, needs to know the access token returned from spotify Authorization Code Flow. Overlooking about spotify token expire time, the idea is: after user authentication through Identity module of ASP.NET server (using JWT token), associate the access token obtained calling https://accounts.spotify.com/api/token to user's informations. So, i expose an API in ASP.NET server like this
[AllowAnonymous]
[HttpPost("token")]
public async Task<ContentResult> getTokenAsync(string? code = null)
{
//to retrive information about who is the user that making call -> need later for associate spotifytoken
string accessToken = Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", "");
JwtSecurityTokenHandler t = new JwtSecurityTokenHandler();
var token = t.ReadJwtToken(accessToken);
var user = _userManager.FindByIdAsync(token.Subject).Result;
string s = "https://accounts.spotify.com/api/token";
if (code == null)
{
var qb = new QueryBuilder();
qb.Add("response_type", "code");
qb.Add("client_id", _config["SpotiSetting:clientId"]);
qb.Add("scope", "user-read-private user-read-email user-library-read");
qb.Add("redirect_uri", _config["SpotiSetting:redirectUser"]);
qb.Add("show_dialog", "true");
return new ContentResult
{
ContentType = "text/html",
Content = "https://accounts.spotify.com/authorize/" + qb.ToQueryString().ToString()
//Content = JsonConvert.SerializeObject(user.Result)
};
} else
{
//if i'm here, api is the callback designed for spotify
var qb = new QueryBuilder();
qb.Add("grant_type", "authorization_code");
qb.Add("code", code);
qb.Add("redirect_uri", "https://localhost:44345/spotify/token");
var client = new HttpClient();
var req = new HttpRequestMessage(HttpMethod.Post, s);
req.Content = new FormUrlEncodedContent(qb);
req.Headers.Authorization = new AuthenticationHeaderValue("Basic", "here_my_secret_encoded_CLIENTID:CLIENT_SECRET");
var response = await client.SendAsync(req);
var result = response.Content.ReadAsStringAsync().Result;
AccessToken json = JsonConvert.DeserializeObject<AccessToken>(result);
user.spotifyInformation.authToken = code;
user.spotifyInformation.accessToken = json;
var res = _userManager.UpdateAsync(user);
if (res.IsCompletedSuccessfully)
{
return Content("ok");
}
else
{
Content("Problem");
}
} return Content("");
}
The problem is that the second time that API is invoked, it's spotify that is sending the first authorization token (needed to request access_token), so I lost user information retrived in the first request. Should be better write two distinct API and separate callback from user request?
It's my first question here, so please to have mercy
Using the backend of my app, I am attempting to capture information from Microsoft Graph for a user that has been authenticated and then add that user to a database. The authentication appears to be working correctly, but the user is never added to the database. I am really stuck on this. I've studied the online documentation extensively, but have been unable to find a solution. If I could just tell if the user properties were getting populated, I could figure out what's going on, but I've been unable to do that since the code runs on the server. (I've attempted to remote debug, but have been unable to successfully set a breakpoint.) Can anyone tell me what I'm doing wrong in the code below?
class MicrosoftAccountInfo
{
public string id { get; set; }
public string displayName { get; set; }
public string mail { get; set; }
}
[MobileAppController]
public class MicrosoftAccountController : ApiController
{
MicrosoftAccountCredentials credentials;
string msRequestUrl;
MyAppContext context;
EntityDomainManager<User> domainManager;
// GET api/<controller>
public async Task<User> Get()
{
if (credentials == null)
{
credentials = await this.User.GetAppServiceIdentityAsync<MicrosoftAccountCredentials>(this.Request);
}
msRequestUrl = "https://graph.microsoft.com/v1.0/me/?$select=id,displayName,mail";
var client = new System.Net.Http.HttpClient();
var headerValue = "Bearer" + credentials.AccessToken;
client.DefaultRequestHeaders.Add("Authorization", headerValue);
var resp = await client.GetAsync(msRequestUrl);
resp.EnsureSuccessStatusCode();
var msInfo = await resp.Content.ReadAsStringAsync();
MicrosoftAccountInfo info = JsonConvert.DeserializeObject<MicrosoftAccountInfo>(msInfo);
context = new MyAppContext();
domainManager = new EntityDomainManager<User>(context, Request);
var user = context.Users.FirstOrDefault(u => u.Email == info.mail);
if (user == null)
{
user = new DataObjects.User { Email = info.mail, UserName = info.displayName, ProviderId = info.id };
await domainManager.InsertAsync(user);
}
else if (string.IsNullOrEmpty(user.ProviderId))
{
user.UserName = info.displayName;
user.ProviderId = info.id;
await context.SaveChangesAsync();
}
return user;
}
}
As to why this is failing, it is difficult to determine without an actual error message. There are simply to many variables/potential failure points involved to say for sure.
That said, you can reduce the number of potential failure points by using the Microsoft Graph .NET Client Library. There is also a NuGet package available: Install-Package Microsoft.Graph.
This library will handle composing the Microsoft Graph and deserializing the response into an object. Along with removing a risk factor, it will greatly simplify your code:
Microsoft.Graph.GraphServiceClient graphClient =
new Microsoft.Graph.GraphServiceClient(new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", "{your-access-token}");
return Task.FromResult(0);
}));
Microsoft.Graph.User user = await graphClient.Me.Request().GetAsync();
I would also suggest implementing a monitoring solution that can trap exceptions on the server. This will help with debugging. If you're running on Azure, I strongly recommend using Application Insights. Aside from being free to get started, it is effectively a "click once, get monitoring" solution. It will handle wiring up the server and provide reporting for any exceptions it runs into.
Note that you can also use App Insights with your own servers or apps hosted on other services (i.e. AWS, RackSpace), there may however be some manual configuration required.
Over the last few days I've been playing with the micro service pattern and all is going well but security seems to baffle me.
So If I may ask a question:
How do I handle user authentication on an individual service? At the moment I pass a request to the Gateway API which in turns connects to the service.
Question Edited Please See Below
Bearing in mind that the individual services should not know about each other. The Gateway is the aggregator as such.
Current architecture.
A little code to simulate the request:
Frontend - Client App
public class EntityRepository<T>
{
private IGateway _gateway = null;
public EntityRepository(IGateway gateway)
{
this._gateway = gateway;
}
public IEnumerable<T> FindAll()
{
return this._gateway.Get(typeof(T)).Content.ReadAsAsync<IEnumerable<T>>().Result;
}
public T FindById(int id)
{
return this._gateway.Get(typeof(T)).Content.ReadAsAsync<T>().Result;
}
public void Add(T obj)
{
this._gateway.Post(typeof(T), obj);
}
public void Update(T obj)
{
this._gateway.Post(typeof(T), obj);
}
public void Save(T obj)
{
this._gateway.Post(typeof(T), obj);
}
}
//Logic lives elsewhere
public HttpResponseMessage Get(Type type)
{
return Connect().GetAsync(Path(type)).Result;
}
public HttpResponseMessage Post(Type type, dynamic obj)
{
return Connect().PostAsync(Path(type), obj);
}
private string Path(Type type)
{
var className = type.Name;
return "api/service/" + Application.Key + "/" + className;
}
private HttpClient Connect()
{
var client = new HttpClient();
client.BaseAddress = new Uri("X");
// Add an Accept header for JSON format.
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}
I use generics to determine where it needs to fire once it hit's the gateway.
So if the Type is Category it will fire the Category service thus calling:
public IEnumerable<dynamic> FindAll(string appKey, string cls)
{
var response = ConnectTo.Service(appKey, cls);
return (appKey == Application.Key) ? (response.IsSuccessStatusCode) ? response.Content.ReadAsAsync<IEnumerable<dynamic>>().Result : null : null;
}
The Gateway does not contain the physical files/Class's of the types.
After a little code, I was hoping someone could give me a little demonstration or the best approach to handle security/user authentication with the current architecture.
Case Scenario 1
User hits the web app and logs in, at that point the users encrypted email and password is sent to the Gateway API which is then passed to the User Service and decides whether the user is authenticated - all well and good but now I want to fetch all Messages from the Message Service that the user has received. I cannot really say in the Gateway if the user is authenticated, fetch the messages because that does not solve the issue of calling the Message Service outside of the Gateway API
I also cannot add authentication to each individual service because that would require all respective services talking to the User Service and that defeats the purpose of the pattern.
Fixes:
Only allow the Gateway to call the Services. Requests to services outside of the Gateway should be blocked.
I know security is a broad topic but within the current context, I'm hoping someone could direct me with the best course of action to resolve the issue.
Currently I have Hardcoded a Guid in all off the applications, which in turn fetches data if the app is equal.
Edit
This answer is about the Gateway <-> Micro service communication. The user should of course be properly authenticated when the App talks with the gateway
end edit
First of all, the micro services should not be reachable from internet. They should only be accessible from the gateway (which can be clustered).
Second, you do need to be able to identify the current user. You can do it by passing the UserId as a HTTP header. Create a WebApi filter which takes that header and creates a custom IPrincipal from it.
Finally you need some way to make sure that the request comes from the gateway or another micro service. An easy way to do that is to use HMAC authentication on a token.
Store the key in the web.config for each service and the gateway. Then just send a token with each request (which you can authenticate using a WebApi authentication filter)
To generate a hash, use the HMACSHA256 class in .NET:
private static string CreateToken(string message, string secret)
{
secret = secret ?? "";
var keyByte = Encoding.ASCII.GetBytes(secret);
var messageBytes = Encoding.ASCII.GetBytes(message);
using (var hasher = new HMACSHA256(keyByte))
{
var hashmessage = hasher.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
So in your MicroServiceClient you would do something like this:
var hash = CreateToken(userId.ToString(), mySharedSecret);
var myHttpRequest = HttpRequest.Create("yourUrl");
myHttpRequest.AddHeader("UserId", userId);
myHttpRequest.AddHeader("UserIdToken", hash);
//send request..
And in the micro service you create a filter like:
public class TokenAuthenticationFilterAttribute : Attribute, IAuthenticationFilter
{
protected string SharedSecret
{
get { return ConfigurationManager.AppSettings["SharedSecret"]; }
}
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
await Task.Run(() =>
{
var userId = context.Request.Headers.GetValues("UserId").FirstOrDefault();
if (userId == null)
{
context.ErrorResult = new StatusCodeResult(HttpStatusCode.Forbidden, context.Request);
return;
}
var userIdToken = context.Request.Headers.GetValues("UserIdToken").FirstOrDefault();
if (userIdToken == null)
{
context.ErrorResult = new StatusCodeResult(HttpStatusCode.Forbidden, context.Request);
return;
}
var token = CreateToken(userId, SharedSecret);
if (token != userIdToken)
{
context.ErrorResult = new StatusCodeResult(HttpStatusCode.Forbidden, context.Request);
return;
}
var principal = new GenericPrincipal(new GenericIdentity(userId, "CustomIdentification"),
new[] {"ServiceRole"});
context.Principal = principal;
});
}
public async Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
}
public bool AllowMultiple
{
get { return false; }
}
private static string CreateToken(string message, string secret)
{
secret = secret ?? "";
var keyByte = Encoding.ASCII.GetBytes(secret);
var messageBytes = Encoding.ASCII.GetBytes(message);
using (var hasher = new HMACSHA256(keyByte))
{
var hashmessage = hasher.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
}
Option 1 (Preferred)
The easy way is the micro services should be behind the gateway, hence you would whitelist services to connect to them, meaning only authorized and trusted parties have access (i.e. the gateway only). Clients shouldn't have direct access to them. The Gateway is your night club bouncer.
Option 2
You can use a JWT or some form of token and share the secret key between the services. I use JWT Authorization Bearer tokens.
The other services don't need to query the user service, they just need to know that the token is valid, then they have authorization to use the API. I get the JWT passed from the client to the gateway and inject it into the request that is sent to the other service behind, just a straight pass through.
The micro service behind needs to have the same JWT consumption as the gateway for authorization but as I mentioned that is just determining a valid token, not querying a valid user.
But this has an issue that once someone is authorized they can jump call upon other users data unless you include something like a claim in the token.
My Thoughts
The part that I found a challenge from Monolithic to Micro Services was that you needed to switch where you place your trust. In Monolithic you control everything you are in charge. The point of Micro Services is that other services are in complete control of their domain. You have to place your trust in that other service to fulfill its obligations and not want to recheck and reauthorize everything at every level beyond what is necessary.
I'm trying to understand if it is possible to post on the users' wall from my Facebook application.
At the moment I have:
One Facebook app with the permission to write on the users' wall
A BackEnd with Fairbooks SDK Installed
Actually I'm following this approach:
public static string GetToken()
{
var fb = new Facebook.FacebookClient();
dynamic result = fb.Get("oauth/access_token", new
{
client_id = APP_ID,
client_secret = APP_S,
grant_type = "client_credentials"
});
return result.access_token;
}
public static void Post(string Message, long UserID)
{
var token = GetToken();
var client = new FacebookClient(token);
client.Post("/" + UserID + "/photos", new { url = "url", caption = Message });
}
My final goal is to post on facebook when the user interact with my API without client-side popups. Is this possible?
This line of code calls for an application access token
dynamic result = fb.Get("oauth/access_token", new
{
client_id = APP_ID,
client_secret = APP_S,
grant_type = "client_credentials"
});
It makes no sense to use this if you haven't first retrieved a user access token in advance. Only then can you make calls on behalf of the user.
My final goal is to post on facebook when the user interact with my API without client-side popups. Is this possible?
This will never be possible by design. All 3rd party applications must invoke a client-side activity for the user in some format. It cannot be automated.
I was tasked with adding logging via external service (using SAML 2.0) to an MVC app (.Net 4.5) that uses SimpleMembership. To be honest I'm not even sure where to start. From what I found on the internet there are few points to the problem. Most of the materials I found dealt with communication with the SAML identity provider (frequently written from scratch). However before I can reach that point I need to make sure I can actually integrate it with the SimpleMembership which we are using.
I suspect for starters I would need something like SAMLWebSecurity (akin to OAuthWebSecurity which we also use). I have found no such thing* on the internet which makes me believe it does not exist (though I wouldn't mind being wrong here). This makes me believe I would have to write it myself, but can I do that without have to write my own membership provider?
*I'm not sure what would be a correct way to call this static class.
I'd recommend that you upgrade to ASP.NET Identity and the OWIN Based authentication middleware. Then you can use Kentor.AuthServices middleware that works with ASP.NET Identity (except that the XSRF-guard has to be commented out until bug #127 has been resolved).
You could also use the SAML classes from Kentor.AuthServices if you have to stick with SimpleMembership, so that you don't have to implement SAML from scratch.
Disclaimer: I'm the author of Kentor.AuthServices, but since it's open source, I'm not making money on people using it.
After discussing it with a colleague I think I figured out the course of actions. Both OAuthWebSecurity and WebSecurity appear to be a part of SimpleMembership, so what I wrote in the question would indicate I want to write a custom membership or reverse engineer SimpleMembership to copy OAuthWebSecurity (which doesn't sound like a fun activity to have).
My best bet here is hijacking the OAuthWebSecurity, by writing a custom client (one which implements the IAuthenticationClient interface). Normally one registers various OAuth clients using OAuthWebSecurity's built in methods (like RegisterFacebookClient). But it is also possible to register those clients using OAuthWebSecurity.RegisterClient which accepts IAuthenticationClient. This way I should be able to add this SAML login without writing a custom membership provider and keep using SimpleMembership.
I managed to do this. Thankfully the identity provider wasn't extremely complicated so all I had to do was redirect to a certain address (I didn't even need to request assertion). After a successful login, the IDP "redirects" the user using POST to my site with the base64 encoded SAMLResponse attached. So all I had to do was to parse and validate the response. I placed the code for this in my custom client (implementing IAuthenticationClient interface).
public class mySAMLClient : IAuthenticationClient
{
// I store the IDP certificate in App_Data
// This can by actually skipped. See VerifyAuthentication for more details
private static X509Certificate2 certificate = null;
private X509Certificate2 Certificate
{
get
{
if (certificate == null)
{
certificate = new X509Certificate2(Path.Combine(HttpContext.Current.ApplicationInstance.Server.MapPath("~/App_Data"), "idp.cer"));
}
return certificate;
}
}
private string providerName;
public string ProviderName
{
get
{
return providerName;
}
}
public mySAMLClient()
{
// This probably should be provided as a parameter for the constructor, but in my case this is enough
providerName = "mySAML";
}
public void RequestAuthentication(HttpContextBase context, Uri returnUrl)
{
// Normally you would need to request assertion here, but in my case redirecting to certain address was enough
context.Response.Redirect("IDP login address");
}
public AuthenticationResult VerifyAuthentication(HttpContextBase context)
{
// For one reason or another I had to redirect my SAML callback (POST) to my OAUTH callback (GET)
// Since I needed to retain the POST data, I temporarily copied it to session
var response = context.Session["SAMLResponse"].ToString();
context.Session.Remove("SAMLResponse");
if (response == null)
{
throw new Exception("Missing SAML response!");
}
// Decode the response
response = Encoding.UTF8.GetString(Convert.FromBase64String(response));
// Parse the response
var assertion = new XmlDocument { PreserveWhitespace = true };
assertion.LoadXml(response);
//Validating signature based on: http://stackoverflow.com/a/6139044
// adding namespaces
var ns = new XmlNamespaceManager(assertion.NameTable);
ns.AddNamespace("samlp", #"urn:oasis:names:tc:SAML:2.0:protocol");
ns.AddNamespace("saml", #"urn:oasis:names:tc:SAML:2.0:assertion");
ns.AddNamespace("ds", #"http://www.w3.org/2000/09/xmldsig#");
// extracting necessary nodes
var responseNode = assertion.SelectSingleNode("/samlp:Response", ns);
var assertionNode = responseNode.SelectSingleNode("saml:Assertion", ns);
var signNode = responseNode.SelectSingleNode("ds:Signature", ns);
// loading the signature node
var signedXml = new SignedXml(assertion.DocumentElement);
signedXml.LoadXml(signNode as XmlElement);
// You can extract the certificate from the response, but then you would have to check if the issuer is correct
// Here we only check if the signature is valid. Since I have a copy of the certificate, I know who the issuer is
// So if the signature is valid I then it was sent from the right place (probably).
//var certificateNode = signNode.SelectSingleNode(".//ds:X509Certificate", ns);
//var Certificate = new X509Certificate2(System.Text.Encoding.UTF8.GetBytes(certificateNode.InnerText));
// checking signature
bool isSigned = signedXml.CheckSignature(Certificate, true);
if (!isSigned)
{
throw new Exception("Certificate and signature mismatch!");
}
// If you extracted the signature, you would check the issuer here
// Here is the validation of the response
// Some of this might be unnecessary in your case, or might not be enough (especially if you plan to use SAML for more than just SSO)
var statusNode = responseNode.SelectSingleNode("samlp:Status/samlp:StatusCode", ns);
if (statusNode.Attributes["Value"].Value != "urn:oasis:names:tc:SAML:2.0:status:Success")
{
throw new Exception("Incorrect status code!");
}
var conditionsNode = assertionNode.SelectSingleNode("saml:Conditions", ns);
var audienceNode = conditionsNode.SelectSingleNode("//saml:Audience", ns);
if (audienceNode.InnerText != "Name of your app on the IDP")
{
throw new Exception("Incorrect audience!");
}
var startDate = XmlConvert.ToDateTime(conditionsNode.Attributes["NotBefore"].Value, XmlDateTimeSerializationMode.Utc);
var endDate = XmlConvert.ToDateTime(conditionsNode.Attributes["NotOnOrAfter"].Value, XmlDateTimeSerializationMode.Utc);
if (DateTime.UtcNow < startDate || DateTime.UtcNow > endDate)
{
throw new Exception("Conditions are not met!");
}
var fields = new Dictionary<string, string>();
var userId = assertionNode.SelectSingleNode("//saml:NameID", ns).InnerText;
var userName = assertionNode.SelectSingleNode("//saml:Attribute[#Name=\"urn:oid:1.2.840.113549.1.9.1\"]/saml:AttributeValue", ns).InnerText;
// you can also extract some of the other fields in similar fashion
var result = new AuthenticationResult(true, ProviderName, userId, userName, fields);
return result;
}
}
Then I just registered my client in App_Start\AuthConfig.cs using OAuthWebSecurity.RegisterClient and then I could reuse my existing external login code (which was originally made for OAUTH). For various reasons my SAML callback was a different action than my OAUTH callback. The code for this action was more or less this:
[AllowAnonymous]
public ActionResult Saml(string returnUrl)
{
Session["SAMLResponse"] = Request.Form["SAMLResponse"];
return Redirect(Url.Action("ExternalLoginCallback") + "?__provider__=mySAML");
}
Additionally OAuthWebSecurity.VerifyAuthentication didn't work with my client too well, so I had to conditionally run my own verification in the OAUTH callback.
AuthenticationResult result = null;
if (Request.QueryString["__provider__"] == "mySAML")
{
result = new mySAMLClient().VerifyAuthentication(HttpContext);
}
else
{
// use OAuthWebSecurity.VerifyAuthentication
}
This probably all looks very weird and might differ greatly in case of your IDP, but thanks to this I was able to reuse most of the existing code for handling external accounts.