This is the eight part of Building ASP.NET Web API RESTful Service Series. The topics we’ll cover are:
Update (2014-March-5) Two new posts which cover ASP.NET Web API 2 new features:
Securing Web API
In this post, we’ll talk about securing our eLearning API, till this moment, all requests sent from client to the API are done over HTTP protocol (http://) and the communication is not encrypted, but in this post, we’ll implement authentication feature in “StudentsController
” so we’ll be sending Username
and Password
for authenticating students. It is well known that transmitting confidential information should be done using secure HTTP (https://).
Enforce HTTPS for Web API
We can enforce HTTPS on the entire Web API by configuring this on IIS level, but in some scenarios, you might enforce HTTPS on certain methods where we transmit confidential information and use HTTP for other methods.
In order to implement this, we need to use Web API filters; basically filters will allow us to execute some code in the pipeline before the execution of code in controller methods. This new filter will be responsible to examine if the URI scheme is secured, and if it is not secure, the filter will reject the call and send response back to the client informing him that request should be done over HTTPS.
We’ll add a new filter which derives from AuthorizationFilterAttribute
, this filter contains an overridden method called OnAuthorization
where we can inject a new response in case the request is not done over HTTPS.
Let’s add a new folder named Filters to project root, then add new class named ForceHttpsAttribute
which derives from System.Web.Http.Filters.AuthorizationFilterAttribute
.
The code for the filter class will be as below:
public class ForceHttpsAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var request = actionContext.Request;
if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
{
var html = "<p>Https is required</p>";
if (request.Method.Method == "GET")
{
actionContext.Response = request.CreateResponse(HttpStatusCode.Found);
actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html");
UriBuilder httpsNewUri = new UriBuilder(request.RequestUri);
httpsNewUri.Scheme = Uri.UriSchemeHttps;
httpsNewUri.Port = 443;
actionContext.Response.Headers.Location = httpsNewUri.Uri;
}
else
{
actionContext.Response = request.CreateResponse(HttpStatusCode.NotFound);
actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html");
}
}
}
}
By looking at the code above, we are using “actionContext
” parameter to get the request and response object from it. What we are doing here is examining the URI scheme of the request, so if it is not secure (http://) we need to return a small HTML message in the response body informing the client to send the request again over https.
As well, we are differentiating between the GET
method, and other methods (POST
, PUT
, DELETE
); because in case the client initiated a GET
request to existing resource over http, we need to construct the same request again using https scheme and use 443 SSL port, then inject this secure URI in response location header. By doing this, the client (browser) will initiate automatically another GET
request using https scheme .
In case of non GET
requests, we will return 404 status code (Not Found) and a small HTML message informing the client to send the request again over HTTPS.
Now, if we want to enforce this filter over the entire Web API, we need to add this filter globally in WebAPIConfig
class as the code below:
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ForceHttpsAttribute());
}
But if we want to enforce HTTPS for certain methods or certain controllers, we can add this filter attribute ForceHttps
as shown below:
[Learning.Web.Filters.ForceHttps()]
public class CoursesController : BaseApiController
{
[Learning.Web.Filters.ForceHttps()]
public HttpResponseMessage Post([FromBody] CourseModel courseModel)
{
}
}
Authenticate Users using Basic Authentication
Until this moment, all methods in our API are public
, and any user on the internet is able to request any resource, but in real life scenario, this is not correct, specific data should be accessed by specific people, so we need to authenticate requests on certain resources. A good candidate in our sample API for clients authentications are:
- Clients who make a
GET
request to the URI “http://{your_port}/api/students/{userName}“. This means that if a client issues a request to GET
detailed information about student with username “TaiseerJoudeh
”, then he needs to provide the username and password for this resource in the request to authenticate himself. As well, we won’t allow authenticated user “TaiseerJoudeh
” to GET
detailed information for another resource because he is not the resource owner and he shouldn’t be able to see his detailed information such as Email, Birth date, classes enrolled in, etc. - Clients who make a
POST
request the URI “http://{your_port}/api/courses/2/students/{userName}“. This method is used to enroll specific student in specific class. It makes sense to authenticate requests here because if student with username
“KhaledHassan
” wants to enroll in course with Id 2
, then he needs to authenticate himself by providing username
and password
. Otherwise, anyone will be able to enroll any student in any class.
In our scenario, we’ll use the Basic Authentication to authenticate users requesting the above two resources. To do so, we need to add new Web API filter which will be responsible to read authorization data from request header, check if the authentication type is “basic
”, validate credentials sent in the authorization headers, and finally authenticate user if all is good. Otherwise, it will return a response with status code 401 (Unauthorized) and it won’t return the resource.
Before digging into the code, let’s talk a little bit about basic authentication.
What is Basic Authentication?
It provides a means to authenticate the sender of the request before actually processing the HTTP request. This will protect the server against Denial of service attacks (DoS). The way it works is that the client who is initiating the HTTP request provides a username
and password
which is base64-encoded and placed in the HTTP header as string
on the form “username:password
”, the recipient of the message (server) is expected to first authenticate the credentials and process the request further only when the authentication is successful.
As the username
and password
are only base64-encoded and to avoid password
s from being exposed to others, Basic Authentication should always be used over SSL connection (HTTPS).
To apply this in our API, let’s add a new class named LearningAuthorizeAttribute
which derives from System.Web.Http.Filters.AuthorizationFilterAttribute
.
public class LearningAuthorizeAttribute : AuthorizationFilterAttribute
{
[Inject]
public LearningRepository TheRepository { get; set; }
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
{
return;
}
var authHeader = actionContext.Request.Headers.Authorization;
if (authHeader != null)
{
if (authHeader.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) &&
!String.IsNullOrWhiteSpace(authHeader.Parameter))
{
var credArray = GetCredentials(authHeader);
var userName = credArray[0];
var password = credArray[1];
if (IsResourceOwner(userName, actionContext))
{
if (TheRepository.LoginStudent(userName, password))
{
var currentPrincipal =
new GenericPrincipal(new GenericIdentity(userName), null);
Thread.CurrentPrincipal = currentPrincipal;
return;
}
}
}
}
HandleUnauthorizedRequest(actionContext);
}
private string[] GetCredentials(System.Net.Http.Headers.AuthenticationHeaderValue authHeader)
{
var rawCred = authHeader.Parameter;
var encoding = Encoding.GetEncoding("iso-8859-1");
var cred = encoding.GetString(Convert.FromBase64String(rawCred));
var credArray = cred.Split(':');
return credArray;
}
private bool IsResourceOwner(string userName,
System.Web.Http.Controllers.HttpActionContext actionContext)
{
var routeData = actionContext.Request.GetRouteData();
var resourceUserName = routeData.Values["userName"] as string;
if (resourceUserName == userName)
{
return true;
}
return false;
}
private void HandleUnauthorizedRequest
(System.Web.Http.Controllers.HttpActionContext actionContext)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
actionContext.Response.Headers.Add("WWW-Authenticate",
"Basic Scheme='eLearning' location='http://localhost:8323/account/login'");
}
}
In the code above, we’ve overridden the method OnAuthorization
and implemented the below:
- Getting the authorization data from request headers
- Making sure that authorization header scheme is set to “
basic
” authentication and contains base64-encoded string. - Converting the base64-encoded
string
to a string
on the form “username:password
” and get the username
and password
. - Validating that
username
sent in authentication header is the same for username
in URI to ensure that resource owner is only able to view his details. - Validating the credentials against our database.
- If the credentials are correct, we set the identity for current principal, so in subsequent requests, the user is already authenticated.
- If the credentials are incorrect, the server sends an HTTP response with a 401 status code (Unauthorized), and a WWW-Authenticate header. Web clients (browser) handles this response by requesting a user
ID
and password
from client.
Now, to apply basic authentication on the two methods we mentioned earlier, all we need to do is to add this attribute “LearningAuthorizeAttribute
” to those methods as the code below:
public class StudentsController : BaseApiController
{
[LearningAuthorizeAttribute]
public HttpResponseMessage Get(string userName)
{
}
}
public class EnrollmentsController : BaseApiController
{
[LearningAuthorizeAttribute]
public HttpResponseMessage Post(int courseId,
[FromUri]string userName, [FromBody]Enrollment enrollment)
{
}
}
Now, we need to test authentication by sending GET
request to URI: “http://localhost:{your_port}/api/students/TaiseerJoudeh” using two different clients, Firefox and Fiddler.
Testing User Firefox
We’ll issue get
request using the browser and the response returned will be 401 because we didn’t provide username
and password
and “Authentication Required” prompt will be displayed asking for the username
and password
. By providing the correct credentials, we’ll receive status code 200 along with complete JSON object graph which contains all specific data for this user. For any subsequent requests for the same resource, the code will not check credentials because we have created principal for this user and he is already authenticated.
Testing Using Fiddler
By using fiddler, we need to create the base64-encoded string
which contains the “username:password
” and send it along with the authorization header. To generate this string
, we will use http://www.base64encode.org/ as the image below:
Note that this is not encryption at all, so as we stated earlier, using Basic Authentication should be done over SSL only.
Now, we will use the encoded string
to pass it in authorization header “Authorization: Basic VGFpc2VlckpvdWRlaDpZRUFSVkZGTw==
” the request will be as the image below:
The response code will be 200 OK and will receive the specific data for this authenticated user.
In the next post, we’ll talk about versioning Web API and how we can implement different techniques to implement versioning.