Wednesday, September 10, 2014

Create a secure ASP.NET Web Api web service from scratch.

In a previous post, I described how to create a secure WCF web service.  Here, I will describe how to set up the same type of service using ASP.NET Web Api technology.

Initial Setup

First, create a new project using the ASP.NET Web Application template.  Choose the Web Api project template with No Authentication.

In the Web tab of the Project Properties, change it to Local IIS, and create a virtual directory.  Then change the application pool of this virtual directory to a pool that uses your own more powerful account.  While, you're in there, require SSL.

At this point, make sure you're everything is working by browsing to the api using this uri:  NameOfSite/api/values/5.  If you get json in return, you're good so far.

Authentication

Authentication and authorization can be configured automatically for you if you choose Local Accounts when you add the project.  However, if you want to implement your own custom authentication and authorization code, you need to jump through some hoops.

Authentication of a Web Api is done through what is called an Http Module.  This is a bit of code that you write that actually runs on IIS before the request hits your web service.  It's pretty heavy stuff, but is the only way I know how to do this at the time of this writing.  Add an Infrastructure folder to your project and add a class that implements the IHttpModule interface.  Like this:
Imports System.Net.Http.Headers
Imports System.Security.Principal

Public Class BasicAuthHttpModule
    Implements IHttpModule

    Public Sub Dispose() Implements IHttpModule.Dispose

    End Sub

    Public Sub Init(context As HttpApplication) Implements IHttpModule.Init
        AddHandler context.AuthenticateRequest, AddressOf OnAuthenticateRequest
        AddHandler context.EndRequest, AddressOf OnEndRequest
    End Sub

    Private Sub SetPrincipal(principal As IPrincipal)

        Threading.Thread.CurrentPrincipal = principal

        If HttpContext.Current IsNot Nothing Then
            HttpContext.Current.User = principal
        End If

    End Sub

    Private Function CheckUserNameAndPassword(userName As String, password As String) As Boolean

        If userName = "Steve" And password = "topsecret" Then
            Return True
        Else
            Return False
        End If

    End Function

    Private Function AuthenticateUser(userNamePasswordCombo As String) As Boolean

        Dim Validated As Boolean = False

        Try
            Dim MyEncoding = Encoding.GetEncoding("iso-8859-1")

            userNamePasswordCombo = MyEncoding.GetString(Convert.FromBase64String(userNamePasswordCombo))

            Dim Separator As Integer = userNamePasswordCombo.IndexOf(":")
            Dim UserName As String = userNamePasswordCombo.Substring(0, Separator)
            Dim Password As String = userNamePasswordCombo.Substring(Separator + 1)

            Validated = CheckUserNameAndPassword(UserName, Password)

            If Validated Then

                Dim MyIdentity = New GenericIdentity(UserName)
                SetPrincipal(New GenericPrincipal(MyIdentity, Nothing))

            End If

        Catch ex As Exception
            Validated = False
        End Try

        Return Validated

    End Function

    Private Sub OnAuthenticateRequest(sender As Object, e As EventArgs)

        Dim MyRequest As HttpRequest = HttpContext.Current.Request
        Dim AuthHeader As String = MyRequest.Headers("Authorization")

        If AuthHeader IsNot Nothing Then

            Dim AuthHeaderValue = AuthenticationHeaderValue.Parse(AuthHeader)

            If AuthHeaderValue.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) And AuthHeaderValue.Parameter IsNot Nothing Then
                AuthenticateUser(AuthHeaderValue.Parameter)
            End If

        End If

    End Sub

    Private Sub OnEndRequest(sender As Object, e As EventArgs)

        Dim MyResponse As HttpResponse = HttpContext.Current.Response

        If MyResponse.StatusCode = 401 Then
            MyResponse.Headers.Add("WWW-Authenticate", "Basic realm=""My Realm""")
        End If

    End Sub

End Class



To enable IIS to use this HTTP Module, add the following to your web.config:
<system.webServer>
    <modules>
      <add name="BasicAuthHttpModule"
        type="NameOfWebApplication.BasicAuthHttpModule, NameOfWebApplication"/>
    </modules>
</system.webServer>

In IIS, you also need to disable all authentication including anonymous on the site where the web api is hosted.

Do a quick test in the browser.  You should find that a login window comes up (in IE anyway) with which to provide your credentials.

Next, you'll want to create a little client application to test the web service.  A console application works great for this.  However, you will need to install a NuGet package called Microsoft ASP.NET Web API Client Libraries.  The id is Microsoft.AspNet.WebApi.Client.  Here is the code to call the Get method from your console:
    Async Function WebApiGetAsync() As Task

        Using Client As New HttpClient()

            Client.BaseAddress = New Uri("https://NameOfServer.Domain.com/NameOfSite/")
            Client.DefaultRequestHeaders.Accept.Clear()
            Client.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue("application/json"))

            Dim ByteArray = Encoding.ASCII.GetBytes("Steve:topsecret")
            Client.DefaultRequestHeaders.Authorization = New AuthenticationHeaderValue("Basic", Convert.ToBase64String(ByteArray))

            Dim Response As HttpResponseMessage = Await Client.GetAsync("api/values/5")

            If Response.IsSuccessStatusCode Then

                Dim Results As String = Await Response.Content.ReadAsStringAsync()

                Console.WriteLine(Results)

            Else

                Console.WriteLine(Response.StatusCode.ToString)
                Dim Results As String = Await Response.Content.ReadAsStringAsync()
                Console.WriteLine(Results)

            End If

        End Using

    End Function

Authorization

To create your own custom authorization, you just need to create your own Authorization Filter.  Just add a class called CustomAuthorizeAttribute to your Infrastructure folder like this:
Public Class CustomAuthorizeAttribute
    Inherits System.Web.Http.AuthorizeAttribute

    Protected Overrides Function IsAuthorized(actionContext As Http.Controllers.HttpActionContext) As Boolean
        Dim Authorized As Boolean = False

        For Each ThisRole As String In Roles.Split(",").ToList

            If SomeCustomLibrary.IsInRole(Threading.Thread.CurrentPrincipal.Identity.Name, ThisRole) Then
                Authorized = True
                Exit For
            End If

        Next

        Return Authorized
    End Function

End Class

There are a few things to note here.

  1. You must inherit System.Web.Http.AuthorizeAttribute and not System.Web.Mvc.AuthorizeAttribute.  
  2. This is invoked by using <CustomAuthorize(Roles:="RoleName1, RoleName2")>
How to call a POST from a client

To call a POST method from your client, do the following:
    Async Function WebApiPostAsync() As Task

        Using Client As New HttpClient()

            Client.BaseAddress = New Uri("https://NameOfServer.YourDomain.com/NameOfSite/")

            Dim MyModel As New ParameterModel With {.Parameter1 = "From the Web Service.", .Parameter2 = 484507, .Parameter3 = Now.Date}

            Dim ByteArray = Encoding.ASCII.GetBytes("Steve:topsecret")
            Client.DefaultRequestHeaders.Authorization = New AuthenticationHeaderValue("Basic", Convert.ToBase64String(ByteArray))

            Dim Response As HttpResponseMessage = Await Client.PostAsJsonAsync("api/NameOfController", MyModel)

            If Response.IsSuccessStatusCode Then

                Dim Results As String = Await Response.Content.ReadAsStringAsync()

                Console.WriteLine(Results)

            Else

                Console.WriteLine(Response.StatusCode.ToString)
                Dim Results As String = Await Response.Content.ReadAsStringAsync()
                Console.WriteLine(Results)

            End If

        End Using

    End Function

There are some key differences here between WCF and Web API.  With WCF, you can just call a function and specify parameters just like you would if the method was local.  With Web API, in cases of GETs the parameters are either provided via the URL with query parameters or the route.  With POSTs you pass a JSON object as data.  This JSON object has a "property" for each parameter that you need.





No comments:

Post a Comment