Secure Web APIs With Swagger, Swashbuckle, and OAuth2 (Part 1)
I wanted to go down the path of creating a shiny new custom enterprise-grade API framework that includes the following features:
- Easy to navigate documentation and testability via a Swagger interface that can be tested/tried right on the site hosting the API
- Authenticated access for customers and clients using an OAuth2 endpoint (a custom OAuth2 endpoint using Thinktecture’s IdentityServer3)
- The Swagger interface should “fit well” in the rest of the API site — meaning a consistent look and feel and navigation options
None of the above would have been possible without a couple a “prerequisites” that I first reviewed:
- Two excellent Pluralsight courses from Dominick Baier regarding Web API security and OAuth2/OpenId-Connect
- The GitHub documentation for the Swashbuckle package
I can’t recommend these resources strongly enough. They’re great — especially Dominick’s courses and the Thinktecture IdentityServer3 documentation and samples.
Create your project
I got started by creating a brand new Web API project and selecting "No Authentication" for the initial project — I would be adding that manually to support the correct, modern token-based authentication favored by modern applications.
NuGet Packages
Right after the creation of the project, there were a couple of NuGet packages that I installed using the NuGet package manager:
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Jwt
- Microsoft.AspNet.WebApi.Owin
- Swashbuckle
- Thinktecture.IdentityServer3.AccessTokenValidation
Setting up the authentication
Authentication is pretty easy to setup, assuming you already have your OAUth server configured and ready. This assumption turns out to be non-trivial, but setting it up is not the subject of this post.
So, given an OAuth endpoint (especially IdentityServer3), configuring the API project to use it is pretty straightforward. You already have the required NuGet packages.
The next step is setting up the Owin Middleware to use this authentication for API calls. To do this, do a few things: Create a Startup class in your API project and make it look like this:
1using Owin;
2using Thinktecture.IdentityServer.AccessTokenValidation;
3
4namespace YourApiProject
5{
6 public partial class Startup
7 {
8 public void Configuration(IAppBuilder app)
9 {
10 ConfigureAuth(app);
11 }
12
13 public void ConfigureAuth(IAppBuilder app)
14 {
15 app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
16 {
17 Authority = SomeStaticHelperClass.GetIssuerUri(),
18 RequiredScopes = new[] { "sampleapi" }
19 });
20 }
21 }
22}
Recommendation: Create a GetIssuerUri
method in some helper class
You won’t have a SomeStaticHelperClass.GetIssuerUri
method defined but you should put this into a standalone class (we’ll see why
when we hit Swagger below), and it should look something like the method below. It’s basically returning the URL of your identity issuer, and
this MAY be conditional based on your environments or whether you are still developing it, etc. You could just as easily use a hard-coded
string or constant value in the above code if that suits your purpose (or read the value from a web.config file or whatever). As noted, though,
this same value will come into play when we configure Swagger, so I recommend not simply hard-coding it in the above code (or you’ll end up doing it twice).
1public static string GetIssuerUri()
2{
3 switch (MyEnvironment)
4 {
5 case "PRD":
6 return "https://id.mydomain.com/identity";
7 case "QA":
8 return "https://idqa.mydomain.com/identity";
9 default:
10 return "https://id.local/identity"; // probably locally-hosted within IIS on developer machine
11 }
12}
Update WebApiConfig.cs
to support your token-based authentication
The code in your WebApiConfig.cs
should look something like the code below. The operative lines for the authentication filters are the first two code lines – SuppressDefaultHostAuthentication
, and add the new HostAuthenticationFilter
.
1using System.Web.Http;
2using System.Web.Http.ExceptionHandling;
3using Microsoft.Owin.Security.OAuth;
4
5
6namespace YourApiProject
7{
8 public static class WebApiConfig
9 {
10 public static void Register(HttpConfiguration config)
11 {
12 config.SuppressDefaultHostAuthentication();
13 config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
14
15 // Web API routes
16 config.MapHttpAttributeRoutes();
17 config.Routes.MapHttpRoute(
18 name: "DefaultApi",
19 routeTemplate: "api/{controller}/{id}",
20 defaults: new { id = RouteParameter.Optional }
21 );
22
23 config.Services.Replace(typeof(IExceptionHandler), new CustomApiExceptionHandler());
24 config.Services.Add(typeof(IExceptionLogger), new CustomApiExceptionLogger());
25 }
26 }
27}
I’d like to specifically call out the LAST two lines as not necessarily required to support the authentication, but they are very valuable for logging and handling errors that occur in your API. See my On exception handling post and refer to the global exception handlers/loggers for Web API. By adding some code as shown below to the dictionary that is getting logged, you can get the entire claims principal logged whenever you encounter an error — which can be very useful.
1var cp = context.RequestContext.Principal as ClaimsPrincipal;
2if (cp != null)
3{
4 foreach (var claim in cp.Claims)
5 {
6 dict.Add(claim.Type, claim.Value);
7 }
8}
Creating an API controller with some APIs requiring authentication
Here are the details (quite simple, actually) to create APIs that require authentication in order to invoke. Basically all you need
to do is create a new ApiController
and add the [Authorize]
attribute to it.
Here is some code — it’s very simple for now. The controller itself has the Authorize attribute, which means that every method in it will require authentication — and is not role or user specific. As long as the caller is an authenticated caller they will be able to make the call. There are two actions in the controller: getstuff and getanonymous. The getanonymous action is just what it sounds like– you can call that method without being authenticated, but the getstuff method requires authentication.
1[RoutePrefix("api/keennewstuff")]
2[Authorize]
3public class KeenNewApiController : ApiController
4{
5 [Route("getstuff")]
6 public string GetStuff()
7 {
8 return "Here is some stuff";
9 }
10
11 [OverrideAuthorization]
12 [AllowAnonymous]
13 [Route("getanonymous")]
14 public string GetAnonymous()
15 {
16 return "yay!";
17 }
18}
So now you have an API that should be ready to accept calls, but no real way to test it without creating a client application of some sort. That’s where Swagger and Swashbuckle come in. Swagger provides a form of web-based UI on top of a web API that will provide information about the API methods, their http methods, their inputs, and their outputs and response types. Additionally it allows you to “test” the calls right on the page so you can see how the API behaves.
Configure Swagger and add a link to your navigation bar to it
When we added our NuGet packages above, one of those that we added was Swashbuckle, which is a .Net implementation
of Swagger. Adding this package will create a SwaggerConfig.cs
in your App_Start
folder. We now need to go into that folder and
configure it to actually expose a Swagger page for our API.
To do this you need to add the "Swagger" line into your _Layout.cshtml
.
1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8" />
5 <meta name="viewport" content="width=device-width" />
6 <title>@ViewBag.Title</title>
7 @Styles.Render("~/Content/css")
8 @Scripts.Render("~/bundles/modernizr")
9</head>
10<body>
11 <div class="navbar navbar-inverse navbar-fixed-top">
12 <div class="container">
13 <div class="navbar-header">
14 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
15 <span class="icon-bar"></span>
16 <span class="icon-bar"></span>
17 <span class="icon-bar"></span>
18 </button>
19 @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
20 </div>
21 <div class="navbar-collapse collapse">
22 <ul class="nav navbar-nav">
23 <li>@Html.ActionLink("Home", "Index", "Home", new {area = ""}, null)</li>
24 <li><a href="~/swagger">Swagger</a></li>
25 <li>@Html.ActionLink("API", "Index", "Help", new { area = "" }, null)</li>
26 </ul>
27 </div>
28 </div>
29 </div>
30 <div class="container body-content">
31 @RenderBody()
32 <hr />
33 <footer>
34 <p>© @DateTime.Now.Year - My ASP.NET Application</p>
35 </footer>
36 </div>
37
38 @Scripts.Render("~/bundles/jquery")
39 @Scripts.Render("~/bundles/bootstrap")
40 @RenderSection("scripts", required: false)
41</body>
42</html>
Testing things out so far….
I strongly recommend that at this point you set yourself up to run this within IIS locally (not IIS Express) and to use SSL to do it. This is made easier by some of the information in Dominick Baier’s Web API Security Pluralsight course — check out the part about Developers and SSL.
That aside, you can run with SSL inside of IIS Express as well and you can test this out.
When you click the Swagger link at the top of the page, you should see your API controller as a heading, and if you click on it the operations appear, as shown in the image below.
You’re probably noticing two things about the screenshot (among others):
- The page doesn’t have the navbar or look like the rest of the site. This is one of the things we’ll be addressing later. Hang tight. 🙂
- There is an “Error” reported at the bottom of the page. This is nothing to worry about. What’s happening is that the URL for your swagger data is being sent to Swagger.io for validation — and in my case that website’s servers cannot resolve my “sampleapi.local” url and so it shows an error. You can resolve this error in two ways: 1) make your site available on the internet, or 2) eliminate the validation process and badge entirely (I’ll come back to this later).
Trying the API from Swagger
If you click on the methods in the listing on the Swagger page, you will see more details about the method, including a button to “Try it out”. When you try out the anonymous method, you should see a response similar to the one shown here.
The regular method should give you a 401
unauthorized response, which shouldn’t surprise you. We require authentication, and this “try it out”
button does not send any authentication information when it tries to invoke the method.
So where are we headed from here? Three places: