Objectives:
- Make a true RESTful Web API (enable CRUD functions by HTTP POST, GET, PUT, and DELETE).
- Enable Cross-Origin Resource Sharing, i.e., CORS (the accessibility of the API by JavaScript can be controlled).
- Enable Secure Authorization for API calls (use the OAuth 2.0 authorization framework).
- Enable Transport Layer Security, i.e., SSL (reject every non-HTTPS request).
Required Tools:
- A Windows Computer.
- Microsoft Visual Studio Express 2013 for Web (free download).
Recommended Knowledge:
- Securing (ASP.NET) Web API based architectures (video).
AND/OR
- ASP.NET Web API 2 (tutorials).
- OAuth 2.0 Authorization Framework (specification).
- Open Web Interface for .NET (OWIN).
- Writing an OWIN Middleware in the IIS integrated pipeline (tutorial).
- 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 |
|
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 |
|
Then open your API Controller, ‘Controllers/ValuesController.cs’ and add the following line before the class definition:
123 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
When we add this class to our project, we have [RequireHttps] available. Add this attribute to all the Controller class. For example,
12345 |
|
1234 |
|
Finally, we also want the Token request to be enforced over HTTPS. To do so, remove ‘AllowInsecureHttp = true’ from Startup.Auth.cs.
123456 |
|
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].
The final code can be downloaded fromhttps://github.com/rfaisal/ASPWebAPI_Example_OAuth_CORs/tree/crud_cors_oauth_ssl.
A screenshot of the client, [screenshot].
Rate this:
9 Votes
Share this:
Related
oAuth Flow for Mobile Apps with An External Identity ServerIn "Computer Science"
Override the Default Login to provide Customized Identities in Windows Azure Mobile ServicesIn "Computer Science"
Getting started with Apache ThriftIn "Computer Science"
Tagged: api, asp-net, authentication, authorization, C-Sharp, code, cors, design patterns, git, github,https, oauth, quality code, restfull, security, source control, ssl, visual-studio, web-api, windows
install-package Microsoft.AspNet.Identity.EntityFramework
remove the AspNetUsers table and execute the new query on server explorer.
[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)
);
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.
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.
Sorry, I haven’t had a chance to look into this yet. I will do it tonight, please check back tomorrow.
I tested it and it worked just fine for me. This is what I did:
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.
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.
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
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?
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;
}
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;
}
}