Exam Objectives
Implement HTTPBasic authentication over SSL; implement Windows Auth; prevent cross-site request forgery (XSRF); design, implement, and extend authorization and authentication filters to control access to the application; implement Cross Origin Request Sharing (CORS); implement SSO by using OAuth 2.0; configure multiple authentication modes on a single endpointQuick Overview of Training Materials
Exam Ref 70-487 - Chapter 4.3
[Book] Pro ASP.NET Web API Security
[MSDN] Security, Authentication, and Authorization in ASP.NET Web API
- Basic Authentication in ASP.NET Web API
- Integrated Windows Authentication
- Preventing Cross-Site Request Forgery Attacks in ASP.NET Web API
- Authentication Filters in ASP.NET Web API 2
- Enabling Cross-Origin Requests in ASP.NET Web API 2
- External Authentication Services with ASP.NET Web API (OAuth)
[Video] Dominick Baier - Securing ASP.NET Web APIs
- Sample Code, Slide Deck
[DotNetCurry] Preventing CSRF Hacks in ASP.NET WebAPI
[Blog] A WebAPI Basic Authentication Authorization Filter
[Book] Pro ASP.NET Web API Security
[MSDN] Security, Authentication, and Authorization in ASP.NET Web API
- Basic Authentication in ASP.NET Web API
- Integrated Windows Authentication
- Preventing Cross-Site Request Forgery Attacks in ASP.NET Web API
- Authentication Filters in ASP.NET Web API 2
- Enabling Cross-Origin Requests in ASP.NET Web API 2
- External Authentication Services with ASP.NET Web API (OAuth)
[Video] Dominick Baier - Securing ASP.NET Web APIs
- Sample Code, Slide Deck
[DotNetCurry] Preventing CSRF Hacks in ASP.NET WebAPI
[Blog] A WebAPI Basic Authentication Authorization Filter
[PluralSight] Implementing an API in ASP.NET Web API (Securing APIs)
[PluralSight] Web API v2 Security
Previous posts on ASP.NET security:
Implement a secure site with ASP.NET
Secure a WCF service
Design and implement claims-based authentication
[PluralSight] Web API v2 Security
Previous posts on ASP.NET security:
Implement a secure site with ASP.NET
Secure a WCF service
Design and implement claims-based authentication
Implement Basic Authentication
Basic Authentication in ASP.NET Web API outlines how to use Basic Auth with IIS. Out of the box, IIS Basic Authentication uses the user's Windows credentials to authenticate them. This means that in order to grant a user access, they would need to be added to the server's domain. They provide a demo for implementing the IHttpModule interface and plugging it into the ASP.NET pipeline, (something covered in the previous test, Design HTTP modules and handlers). They recommend implementing an authentication filter or an OWIN middleware solutions for Web API 2 projects.
Dominick Baier's Web API v2 Security course on PluralSight provides an excellent walkthrough for creating a Basic Authentication middleware for OWIN. Bits and pieces of Dominicks code closely resembles the demo code from the documentation, this isn't really that surprising since they are trying to accomplish the same thing with different pipelines haha. The four classes needed to reproduce his solution are:
The DemoController:
The main server entry point:
Calling the API with no authentication will result in a "challenge" response. In the browser, this will automatically prompt for a username and password. The username and password are concatenated with a colon separator, base64 encoded, and attached to the request as the "Authentication" header:
Dominick Baier's Web API v2 Security course on PluralSight provides an excellent walkthrough for creating a Basic Authentication middleware for OWIN. Bits and pieces of Dominicks code closely resembles the demo code from the documentation, this isn't really that surprising since they are trying to accomplish the same thing with different pipelines haha. The four classes needed to reproduce his solution are:
- BasicAuthenticationOptions : AuthenticationOptions
- BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
- BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
- BasicAuthenticationMiddlewareExtensions (optional, but idiomatic)
The Microsoft.Owin.Security and Microsoft.Owin.Security.Infrastructure namespaces contain the extended classes, which come from the Microsoft.Owin.Security NuGet package. The Options class caries around the bits that vary (in this case, the realm and the validator function). The handler does the actual work, parsing the Authentication header and passing the credentials to the validation function.
My demo code uses most of the same underlying code as the OwinHosted example from the "Hosting" post. The "Startup" class uses the UseBasicAuthentication extension method, and the server doesn't bother with making making a request to itself. The "demo" controller just returns the string "Bingo!" and prints the authenticated users claims to the console:
The Startup class:
class Startup { public void Configuration(IAppBuilder appBuilder) { HttpConfiguration config = new HttpConfiguration(); //configure basic authentication //a valid username/password combination is one in which they match //hey, it's a demo, give me a break... appBuilder.UseBasicAuthentication("DemoRealm", (u,p) => u.Equals(p)); // Web API routes config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); appBuilder.UseWebApi(config); } }
The DemoController:
public class DemoController : ApiController { [Authorize] [Route("api/Demo")] public HttpResponseMessage Get() { var id = ClaimsPrincipal.Current; foreach(var claim in id.Claims) { Console.WriteLine(claim); } return Request.CreateResponse(HttpStatusCode.OK, "Bingo!"); } }
The main server entry point:
static void Main(string[] args) { string baseAddress = "http://localhost:9000/"; // Start OWIN host using (var app = WebApp.Start<Startup>(url: baseAddress)) { Console.WriteLine("Server started at " + baseAddress); Console.WriteLine("Press [ENTER] to close..."); Console.ReadLine(); } }
Calling the API with no authentication will result in a "challenge" response. In the browser, this will automatically prompt for a username and password. The username and password are concatenated with a colon separator, base64 encoded, and attached to the request as the "Authentication" header:
The attached claims are totally arbitrary, and were added by the BasicAuthenticationHandler. In a system using role based authentication, it would make sense to have role claims attached as part of the authentication/authorization pipeline (the Configure Authorization post from 70-486 looks at some ways of doing this...)
Implement Windows Authentication
Integrated Windows Authentication is about a page of content. It really doesn't need to be much longer because there is almost nothing to do in an IIS hosted Web API application to make Windows authentication work. It's literally one line in the web.config file:
Making it work in an OWIN self hosted app isn't any harder. StackOverflow had the answer this time, and it was twice as hard as the IIS version (i.e. two lines of code). In the Startup class, you need this bit of code:
Our little server console window will now have many more claims to print out since our identity is a Windows claims identity now:
And that is really all there is to it. Dominick's PluralSight course also looks at Windows auth, and the two approaches above are the ones he touches on. He notes that Windows authentication is really only suitable for fully intranet scenarios, not anything externally facing. Dominick, as well as the documentation, not that Windows auth is vulnerable to CSRF attacks. Which segues nicely into our next section...
<system.web> <authentication mode="Windows" /> </system.web>
Making it work in an OWIN self hosted app isn't any harder. StackOverflow had the answer this time, and it was twice as hard as the IIS version (i.e. two lines of code). In the Startup class, you need this bit of code:
HttpListener listener = (HttpListener)appBuilder.Properties["System.Net.HttpListener"]; listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;
Our little server console window will now have many more claims to print out since our identity is a Windows claims identity now:
And that is really all there is to it. Dominick's PluralSight course also looks at Windows auth, and the two approaches above are the ones he touches on. He notes that Windows authentication is really only suitable for fully intranet scenarios, not anything externally facing. Dominick, as well as the documentation, not that Windows auth is vulnerable to CSRF attacks. Which segues nicely into our next section...
Prevent Cross Site Request Forgery (CSRF)
Preventing Cross-Site Request Forgery Attacks in ASP.NET Web API doesn't actually have anything to do with preventing CSRF attacks in Web API, I was sad to discover. This article from the docs is focused on MVC, and protecting against CSRF attacks in MVC is something I covered in the Implement a Secure site in ASP.NET post for 70-486.
Preventing CSRF Hacks in ASP.NET WebAPI is an attempt at a walkthrough for implementing CSRF protection, and How to prevent CSRF in a RESTful application? is a question on Stack Overflow that generated some really interesting advise on what kind of approach would be appropriate. The .NET class that supports CSRF mitigation is called AntiForgery and is part of the System.Web.Helpers namespace.
Since Web API is stateless, the CSRF protection needs to be similarly stateless. The approach used by the DotNetCurry walk through, as well as Dominick's demo, is to use a cookie and a hidden form field. The site that generates the form has the anti forgery token embedded in a hidden field, and when the API receives a call it compares the value of this hidden field with the value in the cookie. If they correspond, then the request is legit. This correspondence is determined by passing both values into the AntiForgery.Validate() method.
Preventing CSRF Hacks in ASP.NET WebAPI is an attempt at a walkthrough for implementing CSRF protection, and How to prevent CSRF in a RESTful application? is a question on Stack Overflow that generated some really interesting advise on what kind of approach would be appropriate. The .NET class that supports CSRF mitigation is called AntiForgery and is part of the System.Web.Helpers namespace.
Since Web API is stateless, the CSRF protection needs to be similarly stateless. The approach used by the DotNetCurry walk through, as well as Dominick's demo, is to use a cookie and a hidden form field. The site that generates the form has the anti forgery token embedded in a hidden field, and when the API receives a call it compares the value of this hidden field with the value in the cookie. If they correspond, then the request is legit. This correspondence is determined by passing both values into the AntiForgery.Validate() method.
Implement Authn and Authz filters
Authentication is the process of proving who you are, and Authorization is the process of determining what you have permission to do. Authentication must preceed Authorization, because we can't determine what you are allowed to do if we don't know who you are.
One quick note on mapping authentication and authorization failures to HTTP status codes. Web API, out of the box, returns a 401 "Unauthorized" response when authentication fails. If you decorate an action method with [Authorize] and there isn't an authenticated Principle attached to the request by the time it gets there, you get this response. My intuition for authorization was that a 403 "Forbidden" status code made sense. Since you must be authenticated to even get to the authorization stage, it didn't make sense to return a 401. StackOverflow seems to concur.
Authentication Filters in ASP.NET Web API 2, unlike the last couple topics, actually has some serious meat on it's bones. In order to create an authentication filter, one must implement the IAuthenticationFilter interface, which includes three methods to override: AllowMultiple (technically a Property, pish posh), AuthenticateAsync, and ChallengeAsync. The demo code is pretty long so I'm not going to reproduce it here, but it basically follows these steps:
One thing to note is that the Challenge method is called on every request, not just those that fail authentication, which is probably a bit counterintuitive. In my slightly modified version of the demo, I replaced the Entity Framework based authentication (looking up a username and password in a database) with the toy scheme from Dominicks demos (password == username).
For authorization there is a class called AuthorizationFilterAttribute that you can extend and override the OnAuthorization method. My inspiration for this section came from this blog post by Rick Strahl: A WebAPI Basic Authentication Authorization Filter. The following toy example simply checks that the authenticated user belongs to the list of users passed into the constructor (which would be an utterly atrocious way to implement authorization... DO NOT DO THIS!):
We'd then decorate the controller or action with the attribute, passing in a list of the allowed users:
So "troy" and "bob" are allowed, but if I authenticate as "sam", I should expect to see a failure response:
- The "AuthenticateAsync" method is called
- if no authentication, or the scheme isn't "Basic", no-op
- if the scheme is Basic but there is missing or invalid creds, return 401
- if the username-password combo is incorrect, return 401
- otherwise, create a new IPrincipal and assign it to the context
- The "ChallengeAsync" method is called
- When this method is called, the controller action might not have been called yet, so it is necessary to wrap the existing IHttpActionResult with a new action that will check the status code and return a challenge response if the result is a 401
One thing to note is that the Challenge method is called on every request, not just those that fail authentication, which is probably a bit counterintuitive. In my slightly modified version of the demo, I replaced the Entity Framework based authentication (looking up a username and password in a database) with the toy scheme from Dominicks demos (password == username).
For authorization there is a class called AuthorizationFilterAttribute that you can extend and override the OnAuthorization method. My inspiration for this section came from this blog post by Rick Strahl: A WebAPI Basic Authentication Authorization Filter. The following toy example simply checks that the authenticated user belongs to the list of users passed into the constructor (which would be an utterly atrocious way to implement authorization... DO NOT DO THIS!):
class CheckAuthorizationAttribute : AuthorizationFilterAttribute { private IEnumerable<string> users; public CheckAuthorizationAttribute(params string[] users) { this.users = users; } public override void OnAuthorization(HttpActionContext actionContext) { string name = null; try { name = actionContext.ControllerContext.RequestContext.Principal.Identity.Name; if (!users.Contains(name, StringComparer.InvariantCultureIgnoreCase)) { throw new Exception("You are not on the list..."); } } catch (Exception ex) { var message = new HttpResponseMessage(HttpStatusCode.Forbidden); message.Content = new StringContent("You do not have permission, sorry...\n" + ex.GetType().Name + ": " + ex.Message); throw new HttpResponseException(message); } } public override Task OnAuthorizationAsync( HttpActionContext actionContext, CancellationToken cancellationToken) { OnAuthorization(actionContext); return Task.FromResult(0); } }
We'd then decorate the controller or action with the attribute, passing in a list of the allowed users:
public class DemoController : ApiController { [Authorize] [Route("api/Demo")] [CheckAuthorization("troy", "bob")] public HttpResponseMessage Get() { var id = ClaimsPrincipal.Current; foreach(var claim in id.Claims) { Console.WriteLine(claim); } return Request.CreateResponse(HttpStatusCode.OK, "Bingo!"); } }
So "troy" and "bob" are allowed, but if I authenticate as "sam", I should expect to see a failure response:
Implement Cross Origin Resource Sharing (CORS)
Modern browsers will not allow AJAX requests to another domain. If I am on "blank.org" and try to make an AJAX request to w3schools, I'll get an error:
Permitting and controlling this kind of cross origin use of our API is what CORS (Cross-Origin Resource Sharing) is about. Enabling Cross-Origin Requests in ASP.NET Web API 2 dives deep into the surrounding minutia, but really when you boil it down there are only a few steps that are needed to make CORS work in a Web API project:
- Install the Microsoft.AspNet.WebApi.Cors NuGet package
- call the EnableCors() extension method on the HttpConfiguration object
- apply the [EnableCors] attribute to the method or controller you want to expose across origin, or instantiate an attribute you want to apply globally and pass it to the EnableCors method above.
There is an override filter availble, [DisableCors] that does what it's name implies for when Cors is enabled at the controller or global level but you want to exclude certain actions.
The [EnableCors] attribute takes three parameters: origins, headers, and methods, which correspond to which values are allowed. All three can be assigned an asterisk "*", which means "Any".
Using credentials in a Cors request requires special handling on the client and server side. The client must set a "withCredentials" flag to true, and the [EnableCors] constructor needs another named parameter, "SupportsCredentials", set to true as well. One additional caveat with supporting credentials is that allowing any origin is not valid, i.e. cannot have origins: "*", AND SupportsCredentials = true.
The Exam Ref points out a less-than-useful way of enabling Cors in an IIS hosted API by setting custom headers in the web.config file, but at least the author has the integrity to point out that this is a bad idea since it applies the policy to the entire API.
Implement OAuth
OAuth is a huge topic... It could easily take up at least one post on its own, if not a series of them, so I'm punting. I covered a bit about setting up SSO in my Identity Federation post for 70-486, though I didn't get too much into the OAuth bits there. Setting up all the external accounts to property demonstrate this stuff would be an epic amount of work that I'm just not down to put in right now. Sorry.
External Authentication Services with ASP.NET Web API (OAuth) looks to be pretty dated, but it might still be valid.
However, the article created for ASP.NET Core, Enabling authentication using Facebook, Google, and other external providers, looks much more promising.
However, the article created for ASP.NET Core, Enabling authentication using Facebook, Google, and other external providers, looks much more promising.
Configure Multiple Authentication Modes
The most obvious avenue for configuring multiple authentication modes is through the use of Authentication Filters. The Basic Authentication demo filter could easily be adapted to work for an access token or some other more sophisticated authentication scheme. When a filter decides that the current request doesn't apply to it, it simply returns and lets other filters in the pipeline deal with the request. If no filter fits, the request remains anonymous and returns a 401 status if authentication is required.
I created a simple demo of this, building upon the BasicAuthFilter project. I created an access token filter that looks for an Authentication header with a scheme of "Bearer", and if a non-empty bearer token is passed in the header, it is compared to "abc123":
I created a simple demo of this, building upon the BasicAuthFilter project. I created an access token filter that looks for an Authentication header with a scheme of "Bearer", and if a non-empty bearer token is passed in the header, it is compared to "abc123":
Dominick Baier in his Securing ASP.NET Web APIs had multiple schemes active simultaneously and appeared to be using Handlers instead of filters, though the basic concepts would be similar.
No comments:
Post a Comment