Protecting APIs and Users with OAuth and Asymmetric Authentication

Introduction

OAuth2 dropped per-call cryptographic singing of requests in favor of easier to use, but arguably less secure access tokens. Users whose access tokens are intercepted can be impersonated as long as the token is valid. This would be much harder if every request had to be signed using asymmetric cryptography allowing resource servers to authenticate each API call before granting authorization.

While this may be cumbersome for some types of applications, it is trivial for applications that utilize blockchain networks as the ability to create and verify digital signatures using public key cryptography is innate to those applications.

This article demonstrated how adding asymmetric authentication to your OAuth2 flow improves both user experience and security.

Authentication/Authorization Flows

First, let us review a conventional OAuth2/OpenID Connect flow without asymmetric authentication:

OAuth2 Flow.png

OAuth2 with OpenID Connect

1 User identity is unknown when the web application is first accessed. The web application redirects to the web front-end of the identity provider via a sign-in button.

2 The user leaves the web application and is presented with the login screen of the identity provider.

3 The user id and password along with user claim and scope grants are sent to the identity provider’s backend server and validated. If validation succeeds, the user is redirected back to the web application along with either a token that will allow the web application to request an id and access token from the identity provider or it receives those tokens directly with the redirect.

4 Back in the web application, the user identity is now known via information encoded in the id token. Authorization for API calls is obtained by including the access token with every request to the web API. The web API will have to access the identity server at least once in order to validate the access token presented by the web client, but it will not have to do that for every request. Authorization happens in the web API, i.e. the web API decides what resources are available based on information contained in the access token. In addition to user id, the access token can contain user claims such as age, country of residence etc.

Key

Key1.png

The advantage of OAuth2 with OpenID Connect is that user credentials do not flow through the web application but are only known to the identity provider which is completely separate from the web application and associated API. Additionally, any number of web applications can utilize the same identity provider for authentication and users can manager access grants for all those applications centrally in one place.

However, the only time the user is actually authenticated is when the user id and password is validated by the identity server. All subsequent requests to the web API are not authenticated and authorization is granted purely based on the presence of the JWT access token. If the access token is leaked or intercepted, then the user can be impersonated simply by including the token with forged requests.

Adding asymmetric authentication to the flow eliminates these shortcomings:

Asymmetric Flow.png

Asymmetric Authentication with Authorization Token

1 With asymmetric authentication, there is no need to redirect to a login screen because users are authenticated from the very first interaction with the web application.

2 Id and access tokens are requested automatically without user interaction from the identity server using a cryptographically signed message that proves the user’s identity.

3 User id and access token are obtained by the web application and used for claims-based authorization only (user claims are encoded in the tokens). When making an API request, a cryptographic signature is included along with the access token. The signature is unique for each request and therefore the web API is able to authenticate every request. Once authenticated, access to API resources is authorized based on the provided access token.

Key

Key2.png

There are several advantages of this flow over the previous OAuth2/OpenId Connect flow.

The user does not have to leave the web application in order to sign in - in fact there is no need to log in at all. When opening the web application, the user is authenticated automatically, and the full application and user data become available immediately.

Authorization works as before, but it is no longer possible to use leaked or intercepted access tokens for impersonation. Every call to the web API is authenticated. The access token alone is insufficient to gain access to API resources and should be more appropriately called "authorization token".

Signature validation is a compute-bound operation that does not create additional network traffic.

In short, adding Asymmetric Authentication to the OAuth flow improves both user experience and security without reducing scalability.

The flow can be further simplified if the id and authorization token (aka access token) are not needed for authorization, i.e. if authorization does not rely on user claims encoded in the tokens but is purely determined by the user's identity:

Simplified Asymmetric Flow.png

Asymmetric Authentication without Authorization Token

1 If the web API does not depend on user claims included in the JWT access token then there is no need to obtain one from the identity server. The cryptographically signed request must include the public key in order for the web API to validate the signature. Therefore, the authenticated user's identity (public key) is provided to the web API with every request. In many scenarios this will be enough for managing authorization, for example if the user profile has to be loaded anyway in order to determine the user's registration status.

This flow is also very well suited for direct API access from command line clients:

API Asymmetric Flow.png

Sample Implementation for .NET Web API and VueJS Web Client

In your ASP.NET Core Web API, import the ProDerivatives.AsymmetricAuthentication NuGet package. As we will be using Ethereum signatures, also import the ProDerivatives.Ethereum NuGet package.

Add below code to you Startup class:

Enable asymmetric authentication in ConfigureService method:

services.AddAuthentication(AsymmetricAuthenticationDefaults.AuthenticationScheme)
    .AddAsymmetricAuthentication(options =>
    {
        options.SignatureTokenRetriever = TokenRetrieval.FromAuthenticationHeader();
        options.SignatureValidator = VerifySignature();
    });

Verify signature, in this case Ethereum signatures using ProDerivatives.Ethereum NuGet package:

private static Func<AuthenticationToken, string, bool> VerifySignature()
{
    return (token, message) =>
    {
        var verifyFunction = ProDerivatives.Ethereum.Signer.VerifySignature();
        return verifyFunction(token.Signature, token.PublicKey, message);
    };
}

Optionally, also add bearer token validation with IdentityServer4. Simply specify authority and API name (aka audience):

services.AddAuthentication(AsymmetricAuthenticationDefaults.AuthenticationScheme)
    .AddAsymmetricAuthentication(options =>
    {
        options.SignatureTokenRetriever = TokenRetrieval.FromAuthenticationHeader();
        options.SignatureValidator = VerifySignature();
    })
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = Configuration["Settings:Authority"];

        if (Configuration["ASPNETCORE_ENVIRONMENT"] == "Development")
            options.RequireHttpsMetadata = false;

        options.ApiName = "ProDerivatives";
    });

If bearer tokens are used, then subject must match public key of signature.

The asymmetric authentication handler expects an authentication token in the header with following properties:

public class AuthenticationToken
{
    public string Signature { get; set; }
    public string PublicKey { get; set; }
    public long Nonce { get; set; }
    public DateTime Timestamp { get; set; }
}

In your web client, sign all requests to the web API by adding an authentication token to the request header. One simple way to create Ethereum signatures is to use the MetaMask browser plugin. Simply retrieve the connected account (i.e. Ethereum public key) and signatures from the injected ethereum object:

import { ethers } from "ethers"

class EthereumAuthentication {
    constructor() {
        if (typeof ethereum !== 'undefined') {
            this._ethereum = window.ethereum
            var provider = new ethers.providers.Web3Provider(this._ethereum)
            this._signer = provider.getSigner()
        }
    }

    getTicks() {
        const d = new Date()
        const n = d.getTime()
        return n
    }

    async getAuthenticationHeader(action, url) {
        const nonce = this.getTicks()
        let message = `${nonce}|${action}|/${url}|`
        const signerAddress = (await this._ethereum.request({ method: 'eth_accounts' }))[0]
        const signature = await this._signer.signMessage(message)
        const token = {
            PublicKey: signerAddress,
            Signature: signature,
            Timestamp: new Date().toISOString(),
            Nonce: nonce
        }
        const headers = {
            AsymmetricAuthentication: JSON.stringify(token)
        }
        return headers
    }
}


export default {
    EthereumAuthentication
}

Finally, call API endpoints like so:

var header = await auth.getAuthenticationHeader("GET", path)
var result = (await this.$http.get(`${appBasePath}/${path}`, { headers: header })).body.value

If you would like to use authorization tokens in addition to signatures, then also add the bearer token obtained from IdenityServer to each request. Interceptors are a convenient way to set request headers:

Vue.http.interceptors.push((request, next) => {
  const token = localStorage.getItem('user-token')
  if (token) {
    request.headers.set('Authorization', `Bearer ${token}`)
  }
  next()
})

Now all calls to the web API are authenticated and authorized using cryptographic signatures and bearer tokens, respectively.

Finally, in the VerifySignature method of the API server, it is highly recommended to also check that the nonce of the authentication token has been incremented in order to prevent replay attacks.