OWIN setup similar to Google Service Accounts using JWT Bearer tokens

Sep 19, 2014 at 3:50 PM
Edited Sep 19, 2014 at 3:51 PM
I had originally posted this on the asp.net forums however it was suggested that here would be a better place to get answers.

I've got a WebAPI 2 service that I would like to have a security setup very similar to the Google Service Accounts. The consuming application would send a JWT to the token endpoint to get an access token which would then be sent on all subsequent calls to the service until that token expires.

Additional requirements:
  • The preference would be that token endpoint would be part of the service application and not a separately hosted application.
  • Because this is going to be load balanced and I can't rely on sticky sessions I would need to store the access tokens in a central place like a database.
  • I'd like to be able to control the expiration time of the access token.
  • Like Google Service Accounts each application authenticating will have a different client_id (Issuer) and a different signing key.
How would one set this up using OWIN?

I have tried a few things but i'm not sure if i'm doing things correctly.

First I tried using app.UseOAuthAuthorizationServer and providing a custom implementation of the OAuthAuthorizationServerProvider overriding the GrantCustomExtension method. However I don't appear to have access to the claims in provided in the JWT.

I also tried using app.UseJwtBearerAuthentication and providing a custom implementation of IOAuthBearerAuthenticationProvider however, I don't have access to the grant_type at all it seems and it appears that I can't specify the token endpoint in the options object provided to the above method, so all I was getting was 404 errors.

At this point i'm kinda looking for an example so that I can make sure I put this together correctly.
Sep 19, 2014 at 4:37 PM
I think those are the right pieces, it's just a question of how to customize them for your scenario. First try overriding OAuthAuthorizationServerProvider.OnValidateClientAuthentication to process the user credentials (JWT).

Folks over in https://jabbr.net/#/rooms/owin may be able to give you some more interactive guidance.
Marked as answer by willfkk on 9/25/2014 at 7:20 AM
Sep 25, 2014 at 2:19 PM
After looking through the Katana source I seemed to figure it out. For those also looking for examples here is what I did, could probably use some refining.

Public Sub ConfigureOAuth(app As IAppBuilder)
        Dim accounts As List(Of ServiceAccount) = New ServiceAccounts().GetServiceAccounts()
        Dim audiences As String() = {"http://localhost/Application123/token"}
        Dim authorizationProvider As New ServiceAccountsAuthorizationServerProvider(accounts, audiences)

        Dim options As New OAuthAuthorizationServerOptions With { _
            .TokenEndpointPath = New PathString("/token"),
            .AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
            .Provider = authorizationProvider}
#If DEBUG Then
        options.AllowInsecureHttp = True
#End If
        Dim bearerOptions As New OAuthBearerAuthenticationOptions 

    End Sub
Public Class ServiceAccountsAuthorizationServerProvider
    Inherits OAuthAuthorizationServerProvider

    Private _Accounts As List(Of ServiceAccount)
    Private _Handler As JwtSecurityTokenHandler
    Private Const JwtBearerGrantType As String = "urn:ietf:params:oauth:grant-type:jwt-bearer"
    Private _ValidAudiences As String()

    Public Sub New(ByVal accounts As List(Of ServiceAccount), audiences As String())
        _Accounts = accounts
        _Handler = New JwtSecurityTokenHandler()
        _ValidAudiences = audiences
    End Sub

    Public Overrides Function ValidateClientAuthentication(context As OAuthValidateClientAuthenticationContext) As Task
        Return Task.FromResult(0)
    End Function

    Public Overrides Function GrantCustomExtension(context As OAuthGrantCustomExtensionContext) As Task
        If context.GrantType = JwtBearerGrantType Then
            Dim token As JwtSecurityToken = Nothing
                token = _Handler.ReadToken(context.Parameters.Item("assertion"))

                If token IsNot Nothing Then
                    Dim serviceAccount As ServiceAccount = _Accounts.FirstOrDefault(Function(a) a.ApiKey = token.Issuer)
                    If IsValidServiceAccount(serviceAccount, token, context) Then
                        If token.Header.Alg = "HS256" Then
                            Dim validationParameters As New TokenValidationParameters
                            validationParameters.IssuerSigningKey = New InMemorySymmetricSecurityKey(Convert.FromBase64String(serviceAccount.SigningKey))
                            validationParameters.ValidAudiences = _ValidAudiences
                            validationParameters.ValidIssuer = serviceAccount.ApiKey
                            Dim principal As ClaimsPrincipal = _Handler.ValidateToken(context.Parameters.Item("assertion"),
                                                                              validationParameters, token)

                            If token IsNot Nothing AndAlso principal IsNot Nothing AndAlso principal.Identity IsNot Nothing Then
                                Dim identity As ClaimsIdentity = CType(principal.Identity, ClaimsIdentity)
                                context.SetError("invalid_grant", "Token validation failed")
                            End If
                            context.SetError("invalid_grant", "Invalid algorithm, only HS256 is supported")
                        End If
                    End If
                    context.SetError("invalid_grant", "Could not read token")
                End If
            Catch ex As Exception
                context.SetError("invalid_grant", ex.Message)
            End Try
            context.SetError("unsupported_grant_type", String.Format("Grant type of {0} is not valid", context.GrantType))
        End If
        Return Task.FromResult(0)
    End Function

    Private Function IsValidServiceAccount(serviceAccount As ServiceAccount, token As JwtSecurityToken, context As OAuthGrantCustomExtensionContext) As Boolean
        Dim isValid As Boolean = False
        If serviceAccount IsNot Nothing Then
            Dim subClaimValue As Integer
            If Not String.IsNullOrEmpty(token.Payload.Sub) AndAlso Integer.TryParse(token.Payload.Sub, subClaimValue) Then
                If subClaimValue = serviceAccount.ConsumerID Then
                    isValid = True
                    context.SetError("invalid_grant", "Invalid consumer")
                End If
                context.SetError("invalid_grant", "Missing or invalid sub claim")
            End If
            context.SetError("invalid_grant", "Invalid api key")
        End If
        Return isValid
    End Function
End Class
Marked as answer by willfkk on 9/25/2014 at 7:20 AM