Very Simple Security in AspNet.WebApi

on Monday, April 23, 2018

Sometimes you just need to get a real simple security implementation up and running to provide a service. The security isn’t ideal, but it’s a temporary bandaid until you can get a more robust system in place. Here is a real simple model for a security system like that.

image

In this scenario you have a web application that exists outside your local network/firewall and requests will need to be authenticated and authorized in order to access an internal resource (data). This can be achieved with some old, but beautiful, web standards.

  • Starting with the assumption that you’ll be using an older style ASP.NET WebApi 2 (AspNet.WebApi) web service as the entry point for requests …
  • And, assuming that your environment has an external LDAP server for authentication (this could be a CAS) …
  • We can use Basic Authentication to send across a username/password combination that’s Base64 encoded string (the glory days of security)
  • From there, the external LDAP server can be used to authenticate the credentials.
  • And, finally, permissions/authorization can be looked up in a simple database.

As a secondary goal, it’s good to have an audit log of when authentication/authorization was attempted and what the result was. So, add this on too.

  • All requests should also be logged in the Simple Security database.

Unfortunately, even setting this up can take more time than you would expect. So, here’s an example project doing it. Unfortunately, you can’t really pull the code down and compile it without access to the internal nuget repositories that it uses. But, it is some sample code that can be useful. Here are some highlights:

SimpleSecurityExample Repository

  • sc_CreateSimpleSecurityTables.sql

    This script will help setup the database Access table the AccessLog (audit log) tables. You will probably want to change UcsbNetId to Username, and drop UcsbCampusId altogether. This code assumes the usage of EntityFramework and Ninject.

  • Using a Shared Project

    Since AspNet.WebApi has been semi-deprecated for AspNetCore.Mvc, this project was written so that the logic is in a shared library. But, the framework specific implementation is in a separate project that targets that framework. (I’ll probably write another post about just his topic.)

    Basically, in Shared.LdapAuthenticationAttribute, the _AuthorizationAsync is the shared logic method that will implement  WebApi’s IAuthenticationFilter.AuthenticateAsync method. The interface is implemented in the framework specific AspNet.WebApi.LdapAuthenticationAttirbute code (in the lower level BasicAuthenticationAttribute, see below).

    To do this, the framework specific class HttpAuthenticationContext is replaced by the placeholder name HttpAuthenticationContextOrAuthorizationFilterContext. If you then need to make an AspNetCore.Mvc specific project, you can define the placeholder name to point to AuthorizationFilterContext in your using statements.
  • (external) BasicAuthenticationAttribute.cs

    This contains the ExtractUsernameAndPassword method that will pull the Base64-encoded username and password from the 'Authorization’ HTTP Header. It also implements IAuthenticationFilter.
  • AspNet.WebApi.HttpRequestMessageExtensions.cs

    When you have a method or functional call in the Shared code that is framework specific, you should pull that code into an extension method on the framework specific class. This allows for for the technology specific implementations to be housed in separate projects while keeping the shared code readable.

    This class also does parsing on the IP Address and X-Forwarded-For header in the incoming request. Often the X-Forwarded-For header is ignored when creating audit logs. This is becoming more important as cloud based proxies, load balancers, and high availability services come into use.

Anyways. I hope this example code can help someone shave a few hours off of implementing what theoretically should be a “very simple security” implementation.

    Denial of Service on an LDAP Server

    on Monday, April 9, 2018

    This came about accidently and was caused by three mistakes turning into a larger problem. This is similar to the way flights are forced to land. It’s never one thing that requires a plane to land mid-flight. Statistically, air planes that fail while flying have 7 small things go wrong with them that combined cause a plane to no longer function as a whole. So, when two or three small things go wrong while the plane is in the air, they generally land the flight and fix them.

    The three mistakes were:

    • On each login attempt to a website, an LDAP connection was established but never closed.
    • The number of open LDAP connections to the Identity server was limited to around 100.
    • A database lock caused slow page response times after login, causing the users to think they weren’t logged in and trying again. Causing a spike in login attempts.

    So, the root issue was that on each login attempt to a website, an LDAP connection was established but never closed. This had gone undetected for years because the connection would timeout after 2 minutes and the number of open connections would stay below any ones radar. Recently, we had found out that something was leaving open connections, but we didn’t see the offending line of code until this issue got to the Denial of Service level. The fix was straight forward: Close the connection properly after each usage. Ya’ know. How it’s supposed to be done.

    The number of open LDAP connections to the Identity server was limited to around 100. There was a recent update to the Identity server that provided LDAP services. One change during the update, which was unknown, was that the number of simultaneous open connections was lowered to around 100 at the same time. If connections had been closed properly there wouldn’t had been an issue. But, at this lower level even a small number of open connections easily pushed the server towards its limit.

    The big change that aggravated the situation was that the website that users were logging into changed the amount of data loaded after login. Previously all data was lazy loaded as needed, but a recent update changed the data to load after login (I might write more about this another day). The data load revealed that there was some database locking/contention with other websites/services that were also using the same database tables. This contention wasn’t found in the Test environment during load tests as not enough systems we’re involved to replicate the real world production data demands on the database (if we ever figure out how to do that there will definitely be a blog post). This new table lock changed an initial Login page response time from 2~3 seconds into 70+ seconds. The users, after about 10~15 seconds would feel like something had gone wrong with their login attempt and would try again. This continued for hours until enough people were trying over and over again that 100 simultaneous open LDAP connections were used and the LDAP server was effectively having it’s service denied.

    Ultimately, a single fix relieved enough pressure on the system to make things work. We changed the offending stored procedures in the database to no longer lock the table (allowing dirty reads for a while). The Login page response times returned to 2~3 seconds immediately, and the number of login attempts fell back to a normal rate.

    This wasn’t a permanent fix, but a temporary solution so we could have time to work on the three points above for a stable long term solution.

    Approve/Revoke API Keys in Apigee Through PS

    on Monday, April 2, 2018

    The Apigee API Gateway grants access using API Keys (in this example). And, those keys are provisioned for each application that will use an API. This can be a bit confusing, because when you first sign up to use an API, you think that you’re going to get an API Key. But, that’s not the case. It’s your application that gets the key. This grants finer grained control of what applications have access to what, and allows for malicious activity to have a narrower impact. It does make the REST API Management backend a little confusing, as you always need to specify a developers email address when updating an application. The application’s are owned by a developer, so you have to know the developer before updating the application.

    There’s a little more confusion because Applications don’t have direct access to APIs. Applications are given approval to use API Products. And, those API Products grant access to different API Proxies. This layer of abstraction is helpful when you want to make a very custom made API Product for an individual customer. And that can happen more often than you might expect.

    image

    So, in order to approve or revoke an API key on an Application you will actually need:

    • The developers email address
    • The application name
    • The API product name
    • And, either to approve or revoke the status

    This script will use the developer email address and application name to look up the status of the application. If no specific API product name is given then the status of Approve or Revoke will be applied to the Application and all associated API Products. If a specific API product name is given, then only that API Products' status will be updated.

    $global:ApigeeModule = @{}
    $global:ApigeeModule.ApiUrl = "https://api.enterprise.apigee.com/v1/organizations/{org-name}"
    $global:ApigeeModule.AuthHeader = @{ Authorization = "Basic {base64-encoded-credentials}" }
    
    <#
    .SYNOPSIS
    	Makes a call to the Apigee Management API. It sets the approved/revoked status on
        an individual API Product for an Appliation. If not individual API Product is specified
        then the status will change for the Application and all associated API Products.
    
    .PARAMETER Email
    	The developer email address that owns the Application
    
    .PARAMETER AppName
        The application to updated
    
    .PARAMETER ApiProductName
        The individual API product to update (optional).
    
    .PARAMETER Status
        Either 'approved' or 'revoked'
    
    .EXAMPLE
    	$appInfo = Set-ApigeeDeveloperAppStatus -Email it@company.org -AppName someapp -ApiProductName calendard_api -Status approved
    #>
    Function Set-ApigeeDeveloperAppStatus {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string] $Email,
        [Parameter(Mandatory = $true)]
        [string] $AppName,
        [Parameter(Mandatory = $false)]
        [string] $ApiProductName = [String]::Empty,
        [Parameter(Mandatory = $true)]
        [ValidateSet("approved","revoked")]
        [string] $Status
    )
    
        $action = "approve"
        if($Status -eq "revoked") { $action = "revoke" }
    
    
        # check the app exists (maybe use error handling here?)
        $appPath = "/developers/$Email/apps/$AppName"
        $app = Invoke-ApigeeRest -ApiPath $appPath
    
    
        # api key/oauth key are stored in the first credentials
        # (all api products are stored within this credential)
        $creds = $app.credentials[0]
    
        $keysPath = "$appPath/keys"
        $keyPath = "$keysPath/$($creds.consumerKey)"
    
    
        # if no api product is selected, approve or deny the entire developer app
        # (the api products are going to be approved/revoked as well)
        if($ApiProductName -eq [String]::Empty) {
    
            # it's very rare that the App will have it's status changed, but just in case ...
            if($app.status -ne $Status) { 
                $path = $appPath + "?action=$action"
                Write-Verbose "Setting app '$AppName' ($Email) to status '$Status'"
                Invoke-ApigeeRest -ApiPath $path -Method Post -ContentType "application/octet-stream"
            }
        
    
            # if no api product is selected,
            # then approve or deny the entire set of api products
            foreach($apiProduct in $creds.apiProducts) {
                if($apiProduct.status -ne $Status) {
                    $path ="$keyPath/apiproducts/$($apiproduct.apiproduct)?action=$action"
                    Write-Verbose "Setting api product '$($apiproduct.apiproduct)' on app '$AppName' ($Email) to status '$Status'"
                    Invoke-ApigeeRest -ApiPath $path -Method Post -ContentType "application/octet-stream"
                }
            }
    
        } else {
        # if an api product name is given, then only update that product
    
            # check the api product exists
            $apiProduct = $creds.apiProducts |? apiproduct -eq $ApiProductName
            if(-not $apiProduct) {
    
                Write-Verbose "Could not find api product '$ApiProductName' for app '$AppName' ($Email)"
    
            } else {
                
                $path = "$keyPath/apiproducts/$($ApiProductName)?action=$action"
                Write-Verbose "Setting api product '$ApiProductName' on app '$AppName' ($Email) to status '$Status'"
                Invoke-ApigeeRest -ApiPath $path -Method Post -ContentType "application/octet-stream"
    
            }
        }
        
    
        $app = Invoke-ApigeeRest -ApiPath $appPath
        return $app
    }
    
    
    
    <#
    .SYNOPSIS
    	Makes a call to the Apigee Management API. It adds the authorization header and
    	uses the root url for our organizations management api endpoint ($global:ApigeeModule.ApiUrl).
    	This returns the body of the response as a string.
    
    .PARAMETER ApiPath
    	The sub path that will be added onto $global:ApigeeModule.ApiUrl.
    
    .EXAMPLE
    	$developers = Invoke-ApigeeMethod -ApiPath "/developers"
    #>
    Function Invoke-ApigeeRest {
    [CmdletBinding()]
    Param (
    	[Parameter(Mandatory = $true)]
    	[string] $ApiPath,
    	[ValidateSet("Default","Delete","Get","Head","Merge","Options","Patch","Post","Put","Trace")]
    	[string] $Method = "Default",
    	[object] $Body = $null,
    	[string] $ContentType = "application/json",
        [string] $OutFile = $null
    )
    
    	if($ApiPath.StartsWith("/") -eq $false) {
    		$ApiPath = "/$ApiPath"
    	}
    	$Uri = $global:ApigeeModule.ApiUrl + $ApiPath
    
    	if($Body -eq $null) {
    		$result = Invoke-RestMethod `
                        -Uri $Uri `
                        -Method $Method `
                        -Headers $global:ApigeeModule.AuthHeader `
                        -ContentType $ContentType `
                        -OutFile $OutFile
    	} else {
    		$result = Invoke-RestMethod `
                        -Uri $Uri `
                        -Method $Method `
                        -Headers $global:ApigeeModule.AuthHeader `
                        -Body $Body `
                        -ContentType $ContentType `
                        -OutFile $OutFile
    	}
    	
    	return $result
    }
    


    Creative Commons License
    This site uses Alex Gorbatchev's SyntaxHighlighter, and hosted by herdingcode.com's Jon Galloway.