I'm studying how to secure ASP.NET Web Api applications. I've started with Basic Authentication (yes - I know it's not recommended, yes - My final plan is to use Token Based Authentication. But I need to first learn and understand the basics).
At first, what I did was to create an Attribute that inherits from AuthorizeAttribute and use is on the controller I want to secure. It's very basic (and working):
public class SimpleUserNamePasswordAuthorizeAttribute : AuthorizeAttribute
{
public string UserName { get; set; }
public string Password { get; set; }
protected override bool IsAuthorized(HttpActionContext actionContext)
{
string query = actionContext.Request.RequestUri.Query;
var nvc = HttpUtility.ParseQueryString(query);
string securityQueryToken = nvc["_auth"];
if (string.IsNullOrEmpty(securityQueryToken) && actionContext.Request.Headers.Authorization == null)
{
return false;
}
string authToken = "";
if (actionContext.Request.Headers.Authorization != null)
authToken = actionContext.Request.Headers.Authorization.Parameter;
else
authToken = securityQueryToken;
if (string.IsNullOrWhiteSpace(authToken))
{
return false;
}
// Decode the token from BASE64
string decodedToken = Encoding.UTF8.GetString(Convert.FromBase64String(authToken));
if(string.IsNullOrWhiteSpace(decodedToken))
{
return false;
}
// Extract username and password from decoded token
int index = decodedToken.IndexOf(":", StringComparison.Ordinal);
if(index == -1)
{
return false;
}
string userName = decodedToken.Substring(0, decodedToken.IndexOf(":", StringComparison.Ordinal));
string password = decodedToken.Substring(decodedToken.IndexOf(":", StringComparison.Ordinal) + 1);
return ((userName == UserName) && (password == Password));
}
}
I read somewhere that this way of working is more common for for Web Api v1. And found this project that implements Basic Authentication: https://github.com/IdentityModel/IdentityModel.Owin.BasicAuthentication
From that I learned that they are using a different approach (that seems more correct) that I don't need my own Attribute, and use the [Authorize] that is part of ASP.NET.
They do that by inheriting AuthenticationHandler and AuthenticationMiddleware and using it as an Owin middleware. At first it didn't work until I removed the following from my WebApiConfig.cs in the App_Start directory:
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
I also notice that it's being called on every request to the server, while with my own attribute calls are made only to relevant controller that uses my attribute.
I'm wondering, what is the difference between the 2 approaches, and which is more "correct" way to use? Which gives better security?
Why did I have to remove those lines from WebApiConfig.cs so it could work?
Does the second method have some performance impact that the first method doesn't have?
2: Why did I have to remove those lines from WebApiConfig.cs so it could work?
By removing those lines it suppress of the host's default authentication mechanism (i.e working of [Authroize] attribute) and enables your custom filter authentication mechanism.
1: I'm wondering, what is the difference between the 2 approaches, and which is more "correct" way to use? Which gives better security?
Using the Asp.net [Authorize] provides the default functionality of authentication by asp.net but by inheriting the AuthorizeAttribute gives you extensibility of writing your own functionality. While both gives you security it's up to you to decide which suits the best to you.
3: Does the second method have some performance impact that the first method doesn't have?
Don't have any technical idea, but it shouldn't have the performance impact.
Related
I have made my API's secure using Authorize attribute.
If the request does not have valid OAuth token, it does not process and returns authorization error.
In my User table I have added 'Status' column to check if the user is Active(Soft delete) or not.
I want to use that 'Status' value(Of User Entity) to authenticate my API's.
Suppose Status is false, I want framework should not process the request as like in case 'Authorize attribute'.
Can it be possible ? If so then How ?
Note:
Now I am checking 'Status' value in my Action and decide to proceed or
not.
If you have other best alternatives then those are also welcome.
Thanks.
You could implement your own AuthorizeAttribute
Here's an example I used to implement my own Authorize method based on Roles:
public class AuthorizeRoles : AuthorizeAttribute
{
private readonly List<string> _rolesAllowed;
public AuthorizeRoles(string rolesAllowed)
{
this._rolesAllowed = rolesAllowed.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var controller = actionContext.ControllerContext.Controller;
if (!this._rolesAllowed.Contains("*"))
{
string errorMessage = "Authorization denied. Missing required role";
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
}
}
else
{
string errorMessage = "Authorization denied. Request is missing context header";
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
}
base.OnAuthorization(actionContext);
}
you then use it in your api methods like this :
[AuthorizeRoles("some value")] or [AuthorizeRoles("*")]
You could effectively specify which methods allow access to people with a certain value in their status.
You have access to the controller, you can check any values you need and allow or deny access based on the results of your check.
I have removed some of my less interesting code but what's left should give you an indication of how to proceed.
How I would do it is I would create a Roles table with different access levels. On my controller I would use the Roles feature of the Authorize attribute to allow or block users from accessing the Action.
[Authorize(Roles = "isActive")]
How can I redirect a user from one Asp.net Mvc site to another Asp.net MVC site and automatically log them in?
The situation is that we will have some customers that need to go to one site and some that will need to go to the other. I've been asked to make it so that when customers are redirected to the correct site that they are also auto logged in to the site they are redirected to.
Assuming you don't want to integrate existing single sign-on solution and that you are using forms authentication for both sites and those sites are not on the same domain. The forms authentication in MVC is done via cookie. So the task is when you're logged in to Site1 to create authentication cookie on Site2.
Usually you craft a request to Site2 like:
/Impersonate/Start?encryptedToken=some_encrypted_stuff
And Site2 handling it like:
[DataContract]
public class Token
{
[DataMember(Name = "u")]
public string UserName { get; set; }
[DataMember(Name = "t")]
public DateTime TimeStamp { get; set; }
[DataMember(Name = "m")]
public string Magic { get; set; }
public Token()
{
Magic = MAGIC;
TimeStamp = DateTime.Now;
}
public const string MAGIC = "SOME_RANDOM_STRING";
}
public class ImpersonateController : Controller
{
[HttpGet]
public ActionResult Start(string encryptedToken)
{
// symmetric encryption - hopefully you know how to do it :)
string decryptedToken = Decrypt(encryptedToken);
var serializer = new DataContractJsonSerializer(typeof(Token));
Token token;
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(decryptedToken)))
{
token = serializer.ReadObject(stream);
}
if (!string.Equals(token.Magic, Token.MAGIC) ||
(DateTime.Now - token.TimeStap).TotalMinutes > 1))
{
// magic doesn't match or timestamp is too old
throw new Exception("Invalid token.");
}
FormsAuthentication.SetAuthCookie(token.UserName, true);
return new HttpStatusCodeResult(HttpStatusCode.OK);
}
}
Maybe needless to say /Impersonate/Start should be under https.
As for crafting the request - you can put it directly into view & make request via json.
In Site1:
public class LoginController : Controller
{
public ActionResult Login(string userName, string password)
{
// ... validate user logs to site 1 etc
var site2UserName = userName;
var token = new Token { UserName = site2UserName };
var serializer = new DataContractJsonSerializer(typeof(Token));
string decryptedToken;
using (var stream = new MemoryStream())
{
serializer.WriteObject(stream, token);
decryptedToken = Encoding.UTF8.GetString(stream.ToArray());
}
// symmetrical encryption
return new View(new LoginMode { Token = HttpUtility.UrlEncode(Encrypt(decryptedToken)) });
}
}
View (assuming you have jQuery)
$(function() {
$.get("https://site2.com/Impersonate/Start?token=#Html.Raw(Model.Token)");
});
So right after you log-in to Site1 you serve view that uses AJAX to send request to Site2 that will authenticate user also there. It is usually better idea to do it on request - the form authentication cookie for Site2 will eventually expire. So I'd favor something like like this on Site1:
Continue to site 2
And
[HttpGet]
[Authorize]
public ActionResult StartImpersonation()
{
// this is essentially similar to Login action
string encryptedToken = "";
string redirectUrl = string.Format(CultureInfo.InvariantCulture,
"https://site2.com/Impersonate/Start?encryptedToken={0}",
HttpUtility.UrlEncode(encryptedToken));
return Redirect(redirectUrl);
}
Which is better because a) cookie on Site2 can't expire b) if there is an error in impersonation user will see why (if there is an error in AJAX impersonation you can show some error to user, but it will look weird - authentication to site 2 haven't succeeded - why they're trying to authenticate me there ? :).
You want a single-sign-on (SSO) solution. This may be done any number of ways. OpenID is popular: https://en.wikipedia.org/wiki/OpenID This goes into plenty of details on a slightly older approach: http://www.codeproject.com/Articles/114484/Single-Sign-On-SSO-for-cross-domain-ASP-NET-appl Even more stuff here: C# ASP.NET Single Sign-On Implementation
HTH
On the link that would take someone from one site to the other, here's some of what you could put in the JS in a few places that may do what you want:
On clicking the link, a $.get is done to grab the HTML of the log-in page.
Then put into JS variables the login and password of the user for the second site's security into that HTML.
Post the data through a $.ajax request so that the person is logged in through the JS behind the scenes.
Now, either redirect in the current window or open a new window with the other site's home page and voila they are signed in without having to do any extra lifting if their own.
Note that some of this could be done earlier if you want to make the transition easier as when the page with the link loads, this could be done in the JS when the document is ready. The key point here is to have the cookies required for the authentication on the second site done without the user having to do any extra clicks.
I am working on a cross platform web app using angular and webapi. The problem is when the angular app runs in a cordova container. To play nice with the rest of the applications on the device, I am required to use a plugin for SSO.. This plugin is what is causing me issues, because it does a few things. It intercepts all the http requests and adds a bearer token to the header, which is generated by a 3rd party Token provider, so I can't decode it, and overwrites any bearer token I have set in the header.It also seems to block cookies..
So it makes it a bit tricky when you can't send you own local credentials.
So I started with https://coding.abel.nu/2014/06/writing-an-owin-authentication-middleware/ and http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.OAuth/OAuthBearerAuthenticationHandler.cs
So I figured I should write my own middleware to take care of this; I thought since the standard oauth middleware can work without cookies, I should not have too hard a time getting my slightly different bearer token middleware to do it.. But that has not been the case... Writing my own middleware.. so I'm able to get the header, validate with the external token provider, but I can't actually sign in.
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
try
{
// Find token in default location
string requestToken = null;
string authorization = Request.Headers.Get("Authorization");
if (!string.IsNullOrEmpty(authorization))
{
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
requestToken = authorization.Substring("Bearer ".Length).Trim();
}
}
.... Take the Request token call other Server, verify token...
Also
public override async Task<bool> InvokeAsync()
{
var ticket = await this.AuthenticateAsync();
if(ticket != null)
{
this.Context.Authentication.SignIn(new AuthenticationProperties(), grantIdentity);
return false;
}
}
So in the end the SignIn does not cause a error or anything, but does not actually signin. As soon as I get to a controller action with an [Authorize] attribute, I get a 401. I not have any external cookies enabled. There is a high probability that I am on the wrong track or I am making it way too hard.
You are doing it way too hard.
Instead of creating your own bearer authentication middleware you should change the default OAuthBearerAuthenticationProvider.
Here is a sample for sending the token in the query string.
//in Startup class
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new QueryStringOAuthBearerProvider(),
//your settings
});
//implementation
public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
private const string AccessTokenQueryKey = "access_token";
public override Task RequestToken(OAuthRequestTokenContext context)
{
//check if token found in the default location - "Authorization: Bearer <token>" header
if (string.IsNullOrEmpty(context.Token))
{
var token = context.Request.Query.Get(AccessTokenQueryKey);
if (!string.IsNullOrEmpty(token))
{
context.Token = token;
}
}
return Task.FromResult<object>(null);
}
}
So… I ment to answer it earlier, but I was able to figure it out, without override the authorize attribute. I ended up looking at the source for the OWIN security code. The trick is, you really need 2 OWIN middleware components. One is the what I call (and I stole this from the owin source) the server middleware. The server middleware responds to the challenge and/or if you are feeling crazy generate local credentials for you. This middleware is also a PASSIVE middleware component. I won’t get in to generating the local credentials unless someone asks , because it’s a bit off point, but if someone thinks it will be helpful, I can update.
public class LowCalorieAuthenticationServerHandler : AuthenticationHandler<LowCalorieAuthenticationServerOptions>
{
//Important this needs to be overriden, but just calls the base.
protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
{
return Task.FromResult<AuthenticationTicket>(null);
}
/// <summary>The apply response challenge async.</summary>
/// <returns>The <see cref="Task"/>.</returns>
protected override async Task ApplyResponseChallengeAsync()
{
if (this.Response.StatusCode != 401)
{
Task.FromResult<object>(null);
return;
}
var challenge = this.Helper.LookupChallenge(
this.Options.AuthenticationType,
this.Options.AuthenticationMode);
if (challenge != null)
{
//OK in here you call the rediret to the 3rd party
//return a redirect to some endpoint
}
Task.FromResult<object>(null);
return;
}
}
Anyway notice how the override AuthenticateCoreAsync() just returns
return Task.FromResult(null);
This is because we don’t want this middleware to modify the request. ApplyResponseChallengeAsync will wait for a Challenge and redirect you to the 3rd party login. IF you want to create a local token of some sort you would override the InvokeAsync method
The second middle ware you need is the token/external credentials validator. This will then authenticate the user somehow. In the case of the local bearer token that is built into the OWIN security, it simple deserializes the token and if it can, and the token is not expired it authenticates the user. So in the case that you want to verify the token with a 3rd part sso, such as google or anything, you insert you logic here. In my case I not only wanted to call the 3rd party provider to get the user info, but to check if they token was still valid for single sign out, and to prevent multiple sessions.
public class LowCalorieAuthenticationHandler : AuthenticationHandler<LowCalorieAuthenticationOptions>
{
//Going to give you the user for the request.. You Need to do 3 things here
//1. Get the user claim from teh request somehow, either froma header, request string, or cookie what ever you want
//2. validate the user with whatever user store or 3rd party SSO you want
//3. Generate a AuthenticationTicket to send to on to the request, you can use that to see if the user is valid in any Identity collection you want.
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
//Good to throw in a point of override here.. but to keep it simple-ish
string requestToken = null;
string authorization = Request.Headers.Get("Authorization");
//TOTAL FAKEOUT.. I am going to add a bearer token just so the simple sample works, but your client would have to provide this
authorization = "Bearer 1234567869";
//STEP 1
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
requestToken = authorization.Substring("Bearer ".Length).Trim();
return await FakeExternalBearer(requestToken);
}
return null;
}
private async Task<AuthenticationTicket> FakeExternalBearer(string token)
{
var authenticationType = Options.AuthenticationType;
//pretend to call extenal Resource server to get user //STEP 2
//CallExternal(token)
//Create the AuthTicket from the return.. I will fake it out
var identity = new ClaimsIdentity(
authenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier,"user1", null, authenticationType));
identity.AddClaim(new Claim(ClaimTypes.Name, "Jon",null, authenticationType));
var properties = new AuthenticationProperties();
properties.ExpiresUtc = DateTime.UtcNow.AddMinutes(1);
properties.IssuedUtc = DateTime.UtcNow;
var ticket = new AuthenticationTicket(identity, properties);
return ticket;
}
}
Ok here we override AuthenticateCoreAsync, but we actually do something now. This this were your do you user authentication. This is the ACTIVE part of the middleware. Note it needs to return a valid AuthenticationTicket. This will run on each request so be careful what you call and how often.
So I have a very simple example here https://github.com/jzoss/LowCalorieOwin If anyone is interested in more detail, please ask. I can add more. I did make it too hard, because now that I understand it, it’s pretty easy, but there is really no good examples on how to do this.
So I have a C# MVC app using Identity for its authentication. I now have a need to expose a few things via Web API to some of my clients. Instead of building a separate app, project, deployment... I've simply added an API Controller to my existing project. To keep things simple for my clients, I've decided to use Basic Auth, opting rather to force my clients into using SSL connections to my API.
I've followed this very useful tutorial to implement the Basic Auth in my API:
http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/
Problem is, that their instructions take over Auth for the entire app...
I need my MVC app to keep using the Identity Auth that it is currently using and hopefully roll my own custom attribute (like [APIAuthorize]) so that it only applies to my API Controller.
I can probably hack around and try to get this to work, but as this is concerning security, I decided to ask for some pro help on how to best implement this. Specifically, I need to know 1) what do I do in my Global.asax (if anything) as the above URL suggests I do this:
protected void Application_Start()
{
GlobalConfiguration.Configuration.MessageHandlers
.Add(new BasicAuthMessageHandler(){
PrincipalProvider = new DummyPrincipalProvider()
});
//...
}
But again, this would take over the Authentication to the entire app... 2) What do I need to do in my custom auth attribute to make all of this work seamlessly.
And of course, if there's a better way to do all of this (without creating a separate app or increasing the implementation difficulty to my clients) then I'm all ears.
I us a filter attribute to adorn the actions i wanted to expose to Simple Auth. I cant remember where i got this code from (probably stackoverflow i just don't have the link so i cant claim credit for it)
public class BasicHttpAuthorizeAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
if (Thread.CurrentPrincipal.Identity.Name.Length == 0)
{
// Get the header value
AuthenticationHeaderValue auth = actionContext.Request.Headers.Authorization;
// ensure its schema is correct
if (auth != null && string.Compare(auth.Scheme, "Basic", StringComparison.OrdinalIgnoreCase) == 0)
{
// get the credientials
string credentials = UTF8Encoding.UTF8.GetString(Convert.FromBase64String(auth.Parameter));
int separatorIndex = credentials.IndexOf(':');
if (separatorIndex >= 0)
{
// get user and password
string passedUserName = credentials.Substring(0, separatorIndex);
string passedPassword = credentials.Substring(separatorIndex + 1);
SimpleAES crypto = new SimpleAES();
string userName = crypto.DecryptString(ConfigurationManager.AppSettings.Get(Constants.SIMPLEUSERNAME));
string password = crypto.DecryptString(ConfigurationManager.AppSettings.Get(Constants.SIMPLEUSERPASSWORD));
// validate
if (passedUserName == userName && passedPassword == password)
{
Thread.CurrentPrincipal = actionContext.ControllerContext.RequestContext.Principal = new GenericPrincipal(new GenericIdentity(userName, "Basic"), new string[] { });
}
}
}
}
return base.IsAuthorized(actionContext);
}
}
Then i use it as so
[BasicHttpAuthorize]
public HttpResponseMessage MyExposedSimpleAuthAction()
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.