Sunday, 23 November 2014

Building your own API and Securing it with oAuth 2.0 in ASP.NET WebAPI 2

Objectives:

  1. Make a true RESTful Web API (enable CRUD functions by HTTP POST, GET, PUT, and DELETE).
  2. Enable Cross-Origin Resource Sharing, i.e., CORS (the accessibility of the API by JavaScript can be controlled).
  3. Enable Secure Authorization for API calls (use the OAuth 2.0 authorization framework).
  4. Enable Transport Layer Security, i.e., SSL (reject every non-HTTPS request).
Required Tools:
  1. A Windows Computer.
  2. Microsoft Visual Studio Express 2013 for Web (free download).
Recommended Knowledge:
  1. Securing (ASP.NET) Web API based architectures (video).
AND/OR
  1. ASP.NET Web API 2 (tutorials).
  2. OAuth 2.0 Authorization Framework (specification).
  3. Open Web Interface for .NET (OWIN).
  4. Writing an OWIN Middleware in the IIS integrated pipeline (tutorial).
  5. Enabling Cross-Origin Requests in ASP.NET Web API (tutorial).

An Skeleton Project by ASP.NET Web API 2 (oAuth 2.0 and CORS support)

1. Creating the Project
Create an ASP.NET Web Application (Visual C#) in Microsoft Visual Studio Express 2013 for Web. Let’s name it Skeleton.WebAPI. Select the Web API template and Change Authentication from ‘No Authentication’ to ‘Individual User Accounts’, [screenshot].
The ‘Controllers/ValuesController.cs’ holds our sample API endpoints. Run the project and in the browser go to ‘http://localhost:port/api/values‘ and you should get a 401 Unauthorized.
2. Writing the Sample API Endpoints
In ‘Controllers/ValuesController.cs’, comment out [Authorize] to disable authorization. Run the project and in the browser go to ‘http://localhost:port/api/values‘ and now you should get 2 values.
Change ‘Controllers/ValuesController.cs’ with the following code to enable CRUD operations, [Link]. This controller now accepts HTTP GET, POST, PUT, and DELETE to receive a list of values, add a value, update a value, and delete a value. Currently, these values are stored in the Web Server memory, but ideally they will be saved in the database. I recommend using Repository Pattern for it, [More info].
Let’s write a simple html page to call these api endpoints. For example, the following code can be used for receiving the list of current values:
12345
$.ajax({
type: "GET",
url: 'http://localhost:port/api/values',
success: function (_d) { alert(JSON.stringify(_d));}
}).fail(function (_d) { alert(JSON.stringify(_d)); });
view rawapi_tester.js hosted with ❤ by GitHub
The full list of API calls by JavaScript can be found here. At this point, you will notice that all the API calls are returning errors. That’s because we haven’t added Cross-Origin Resource Sharing (CORS) support to the server.
3. Enabling Cross-Origin Resource Sharing (CORS) support 
To Enable CORS support, first install Microsoft.AspNet.WebApi.Cors by NuGet. Open up ‘App_Start/WebApiConfig.cs’ and add the following line in the Register method:
12345
public static void Register(HttpConfiguration config)
{
config.EnableCors();
// other stuff
}
view rawWebApiConfig.cs hosted with ❤ by GitHub
Then open your API Controller, ‘Controllers/ValuesController.cs’ and add the following line before the class definition:
123
[EnableCors(origins: "*", headers: "*", methods: "*")]
// [Authorize]
public class ValuesController : ApiController { /*Class Definition*/}
view rawValuesController.cs hosted with ❤ by GitHub
It tells the server the accept all types requests. You can obviously filter out the requests. For more customization, read this.
4. Cleaning up the Views from the project
You should notice that the VS template created some views in the project. As we are building a pure Web API, there will be no views. So, delete App_Start/[BundleConfig,RouteConfig].cs, Areas/*, Content/*, Controllers/HomeController.cs, fonts/*, Scripts/*, Views/*, Project_Readme.html, andfavicon.ico. Finally, remove the following lines from Global.asax.cs:
12
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
view rawGlobal.asax.cs hosted with ❤ by GitHub
At this point, we have completed Objectives 1 and 2. The code up to this point can be  downloaded fromhttps://github.com/rfaisal/ASPWebAPI_Example_OAuth_CORs/tree/crud_cors_only_v1.
5. Adding oAuth 2.0 support
In the simplest terms, here is how oAuth (or most token based authorization) works. The client request an Access Token from the Authorization Server. The Authorization Server verifies the identity of the client somehow and returns an Access Token, which is valid for a limited time. The client can use the Access Token to request resources from the Resource Server as long as the Access Token is valid. In our case, both Authorization Server and the Resource Server are the same server, but they can be separated easily.
Ideally, you need to write an OWIN Middleware in the IIS integrated pipeline for oAuth. But, the VS template generates the necessary codes for the OWIN Middleware. Watch this video to learn more about these implementations.
Now, we will add 2 more functionality, namely registering a new user and requesting a Access Token based on the username and password.
The user registration controller (Controllers/AccountController.cs) is already generated by the Template. First add the following line in the AccountController to allow CORS:
1
[EnableCors(origins: "*", headers: "*", methods: "*")]
Then you can call the end point http://localhost:port/api/Account/Register/ and HTTP POST an object consisting of UserName, Password, and ConfirmPassword to register an account.
Now, the endpoint for requesting an Access Token is also generated by the template. The code is in App_Start/Startup.Auth.cs :
12345678910
//other stuff
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
//other stuff
view rawStartup.Auth.cs hosted with ❤ by GitHub
It says that the client can request an Access Token by calling http://localhost:port/Token endpoint and HTTP POSTing grant_type, username, and password as Url Encoded String. This endpoint, however, is not an API endpoint. To enable CORS to this endpoint we need to add the following code segment to the ConfigureAuth function of App_Start/Startup.Auth.cs file.
1234567891011121314151617181920212223242526
public void ConfigureAuth(IAppBuilder app)
{
//other stuff
app.Use(async (context, next) =>
{
IOwinRequest req = context.Request;
IOwinResponse res = context.Response;
if (req.Path.StartsWithSegments(new PathString("/Token")))
{
var origin = req.Headers.Get("Origin");
if (!string.IsNullOrEmpty(origin))
{
res.Headers.Set("Access-Control-Allow-Origin", origin);
}
if (req.Method == "OPTIONS")
{
res.StatusCode = 200;
res.Headers.AppendCommaSeparatedValues("Access-Control-Allow-Methods", "GET", "POST");
res.Headers.AppendCommaSeparatedValues("Access-Control-Allow-Headers", "authorization", "content-type");
return;
}
}
await next();
});
//other stuff
}
view rawStartup.Auth.cs hosted with ❤ by GitHub
We are almost done. We have to modify all the API calls to pass this Access Token. First of all, un-comment [Authorize] attribute on the ValuesController and run to make sure that all of the API calls are returning 401 Unauthorized. Add the following property with each ajax call to pass the Access Token as the Bearer Token:
123
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", 'Bearer ' + _access_token);
},
view rawbearer_token.js hosted with ❤ by GitHub
At this point, we have completed Objectives 1, 2, and 3. The code up to this point can be  downloaded from https://github.com/rfaisal/ASPWebAPI_Example_OAuth_CORs/tree/crud_cors_oauth.
6. Enforce SSL for all API calls
Any information that is not transferred over SSL is not secure. So, we are going to reject any request that doesn’t come over HTTPS. First we will write a filter that can be used to filter out non-https requests. Here is the code:
1234567891011121314151617
public class RequireHttpsAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
{
ReasonPhrase = "HTTPS Required"
};
}
else
{
base.OnAuthorization(actionContext);
}
}
}
When we add this class to our project, we have [RequireHttps] available. Add this attribute to all the Controller class. For example,
12345
[RequireHttps]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[Authorize]
[RoutePrefix("api/Account")]
public class AccountController : ApiController {/*class definition*/}
1234
[RequireHttps]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[Authorize]
public class ValuesController : ApiController {/*class definition*/}
view rawValuesController.cs hosted with ❤ by GitHub
Finally, we also want the Token request to be enforced over HTTPS. To do so, remove ‘AllowInsecureHttp = true’ from Startup.Auth.cs.
123456
OAuthOptions = new OAuthAuthorizationServerOptions
{
//other stuff
TokenEndpointPath = new PathString("/Token")
// AllowInsecureHttp = true
};
view rawStartup.Auth.cs hosted with ❤ by GitHub
To test the SSL enforcement, change the SSL Enabled property to true for the Development Server in Visual Studio and use the SSL Url, [screenshot].
A screenshot of the client, [screenshot].

22 thoughts on “Building your own API and Securing it with oAuth 2.0 in ASP.NET WebAPI 2

  1. Anas KVC April 3, 2014 at 11:49 pm Reply
    Great article.
    To run the sample project in VS 2012 we need to do the following changes.
    1. Add the missing nuget packages using package manager console
    install-package Microsoft.AspNet.Identity.Core
    install-package Microsoft.AspNet.Identity.EntityFramework
    2. Open the local db on vs server explorer.
    It looks like AspNetUsers table doesn’t have the default columns which is mapped from IdentityUser object. (It will throw the error when we try to register a new user)
    So,
    remove the __MigrationHistory table.
    remove the AspNetUsers table and execute the new query on server explorer.
    CREATE TABLE [dbo].[AspNetUsers] (
    [Id] NVARCHAR (128) NOT NULL,
    [Email] NVARCHAR (256) NULL,
    [EmailConfirmed] BIT NOT NULL,
    [PasswordHash] NVARCHAR (MAX) NULL,
    [SecurityStamp] NVARCHAR (MAX) NULL,
    [PhoneNumber] NVARCHAR (MAX) NULL,
    [PhoneNumberConfirmed] BIT NOT NULL,
    [TwoFactorEnabled] BIT NOT NULL,
    [LockoutEndDateUtc] DATETIME NULL,
    [LockoutEnabled] BIT NOT NULL,
    [AccessFailedCount] INT NOT NULL,
    [UserName] NVARCHAR (256) NOT NULL,
    CONSTRAINT [PK_dbo.AspNetUsers] PRIMARY KEY CLUSTERED ([Id] ASC)
    );
    This will resolve the issues.
  2. Faisal R. April 4, 2014 at 6:45 am Reply
    Thanks Anas!
  3. dave April 21, 2014 at 5:38 am Reply
    Is it possible to use Web API 2′s build in external authentication with Angular JS being hosted on a different web-server? If so does anyone know of an example?
    I’m struggling to get it working. The facebook request is giving me a CORS issue. I’m not sure what is going on.
    My only guess is the redirect is coming from a different domain than the web api is hosted on. Which is what is doing the facebook configuration.
    • Faisal R. April 21, 2014 at 6:53 am Reply
      Hi Dave-
      I am assuming you just uncomment app.UseFacebookAuthentication inStartup.Auth.cs and using it the same way the Web API template example is using, except the WebAPI and client is hosted in different servers.
      In that case, adding the domain urls for both your hosts to facebook app settings should work. If not, can you please send me your login flow.
  4. dave April 21, 2014 at 11:02 am Reply
    Sure. My login flow is as follows:
    1.) User navigates to my login page at http://localhost:8000/app/index.html#/login
    2.) On page load I make a request to my web api to load the ExternalLogins at api/Account/ExternalLogins?returnUrl=%2F&generateState=true. This loads the URLS for when the user clicks the login buttons (exactly how its done with Microsoft SPA template)
    3.) When the user clicks login with facebook button, the web api endpoint api/Account/ExternalLogin. The user is not authenticated to the ChallengeResult is returned. This is where the redirect to the facebook login page should happen. I see the request being made in chrome dev tools but the redirect gives me a No Access-Control-Allowed-Origin error.
    I notice the redirect request has a Origin header with a null value. I think this is the problem but I don’t know why its there. I can duplicate the api call in fiddler to my api/Account/ExternalLogin api, but remove the Origin header, and I dont get the No Access-Control-Allowed-Origin error.
    The origin header gets sent to my API from my angular js client, but im not sure why its on the redirect, or why the value is null.
    Any idea on what could be going on? Thanks
  5. dave April 21, 2014 at 11:47 am Reply
    Ok. Thanks for the response. I did find things like this –https://bugs.webkit.org/show_bug.cgi?id=98838
    Not sure if thats the problem.
  6. dave April 22, 2014 at 11:56 am Reply
    Any luck?
    • Faisal R. April 22, 2014 at 11:58 am Reply
      Hi Dave-
      Sorry, I haven’t had a chance to look into this yet. I will do it tonight, please check back tomorrow.
    • Faisal R. April 22, 2014 at 6:02 pm Reply
      Hi Dave-
      I tested it and it worked just fine for me. This is what I did:
      1. Added my facebook appId and appSecret to the project of the blog post. and run it
      2. Added http://localhost:49811/ in my facebook app’s “site url”
      3. Run api/Account/ExternalLogins?returnUrl=%2F&generateState=true to get a list of urls
      4. Created a new SPA project and its login.viewmodel.js–>self.loginnction –> set window.location to the url I get from Step 3 (hard coded)
      5. Run the SPA and click on the login with facebook button.
      It took me to WebAPI’s root url with a access_token in the url (meaning successful authentication)
  7. dave April 22, 2014 at 7:43 pm Reply
    Faisal,
    I’m going to start fresh and try exactly what you just did. Thanks for the response. I will let you know how it goes.
  8. Jimmy May 2, 2014 at 4:42 am Reply
    Hello
    is there a second part or has this tutorial changed after getting published ?
    I can’t see any details about DB in here so I’m confused how to authenticate and store user data for this application?
    I can see one comment regarding a table called ‘AspNetUsers’ and EF framework but not mentioned in the tutorial it self
    Cheers
    • Faisal R. May 2, 2014 at 12:45 pm Reply
      Hi Jimmy-
      No there is no 2nd part. You can change the database info in Web.config file. By default, it uses a .mdf file in your local file system for data storage
  9. Alexander Smotritsky (@gyrevik) August 10, 2014 at 8:59 pm Reply
    I think Jimmy may have asked his question because he and other readers may not know that when you create an asp.net web api project you can specify authentication — individual users accounts and this will generate the database/mdf and entity framework layer for it.
  10. mohit September 4, 2014 at 3:29 am Reply
    this post is very helpful
    question 1: But instead of using entity framework …how can we implement it with simple ado.net ?
    question 2 : can it be done without using MVC?
    • Faisal R. September 4, 2014 at 9:41 am Reply
      1. It doesn’t matter what data-source you use and how you use it. It will still work with MySQL, MongoDB and of course, ADO.Net. You need to modify theAccountController.cs file only.
      2. Yes, it can be done. In fact, in the example, there is no view only Model and Controller.
  11. Sultan Mehmood September 20, 2014 at 4:12 am Reply
    Hi Faisal,
    I am newbie to Web APIs. I wanted to know if we dont have Owin Middleware, still your code will work ? Because I am trying to run your code and its giving me 404 for Requested URL localhost:(myport)/Token.
    Your response will be highly appreciated.
    Thanks
    • Faisal R. September 20, 2014 at 11:28 am Reply
      I am pretty sure you need OWIN. 404 is a “Not Found” error. Without looking into details I can only advise to make sure that the API project is running.
      • Sultan Mehmood September 20, 2014 at 11:50 am
        Thanks fir your reply. I was able to figure it out.
    • Paco October 7, 2014 at 1:23 pm Reply
      You mentioned you resolved your issue…care to elaborate? I’m having the same issue where I get a 404 when hitting my TokenEndpointPath at /token.
      • Sultan Mehmood October 8, 2014 at 11:56 pm
        Can you please post your code here ? without looking your code I can not tell what exactly you are having….
  12. monique ma October 8, 2014 at 2:51 pm Reply
    I tried the same structure but using the MVC as test site. When I tried to get token, I got bad request error.
    Code as following:
    public string GetToken(string userName, string userPW)
    {
    var content = “grant_type=password&username=” + userName + “&password=” + userPW;
    string uri = baseUri + “Token”;
    using (HttpClient client = new HttpClient())
    {
    client.BaseAddress = new Uri(baseUri);
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(“application/x-www-form-urlencoded”));
    var response = client.PostAsJsonAsync(uri, content).Result;
    return response.Content.ToString();
    }
    Where is my problem, I need get this token to set further request.
    Like this:
    public Member GetMemberByID (int memberId)
    {
    var token = GetToken(“xinm”, “Test1234″);
    string uri = baseUri +”api/member/170/GetMember”;
    using (HttpClient client = new HttpClient())
    {
    client.BaseAddress = new Uri(baseUri);
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(newMediaTypeWithQualityHeaderValue(“application/json”));
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(token);
    Task response = client.GetStringAsync(uri);
    return JsonConvert.DeserializeObjectAsync(response.Result).Result;
    }
    }

1 comment:

  1. Thank you for sharing this article, it is very easy to understand and informative. Excellent!

    Melbourne SEO Services

    ReplyDelete

Angular Tutorial (Update to Angular 7)

As Angular 7 has just been released a few days ago. This tutorial is updated to show you how to create an Angular 7 project and the new fe...