AuthN / authZ

OAuth 2.0 authorization

Available since

  • HAProxy 2.5
  • HAProxy Enterprise 2.5r1
  • HAProxy ALOHA 14.0

With the OAuth 2.0 authorization protocol, you can use JSON Web Tokens (JWTs) to convey a client’s level of access for a service without requiring a password.

JWTs, which contain a client’s permissions but not their identity, serve as a client’s proof of membership and encapsulate the fine-grained permissions they have. A JWT is a payload of base64-encoded JSON data, cryptographically signed by the party that authenticated the client. You can require that each HTTP request come with a JWT to verify whether the requested action should be allowed.

The load balancer provides configuration directives that cover all of the functionality needed to support OAuth 2.0, including checking that a token:

  • has not expired.
  • was issued and signed by a trusted authentication service.
  • is meant for your service and not someone else’s.
  • contains any necessary claims to grant a client a specific type of access (for example, read or write access).

The load balancer, which sits in front of your service, verifies that the token is genuine and checks it to see which permissions the client should have. You can either have the proxy relay those permissions to your application via HTTP headers or make a decision within the load balancer itself to deny the request immediately. For example, if the client requests to update data with a PUT request, but they do not have the write permission, you can deny the request.

Get a JWT with Auth0 Jump to heading

The scenario we’ll describe here is a client-side application using OAuth to access a server-side API.

The client-side application will need a JWT to get access to the API. To get the JWT, they must make a request to a third-party authentication service, passing their client ID and client secret to get a JWT in return. Once they have the JWT, they will attach it to their requests that pass through the load balancer in order to gain access.

The load balancer does not generate tokens, so you need to subscribe to an authentication service. Below, we use Auth0 as the authentication service.

To get a key and token using Auth0, follow these steps:

  1. Create an account with Auth0.

  2. Log into your Auth0 account and go to Applications > APIs to create your API. This represents the API for which you’ll require a JWT. When creating your API, note that the Identifier field will be the audience in the token (more on that later) and is typically a URL like https://api.mywebsite.com. The Signing Algorithm can be RS256 or HS256. The former uses an X.509 key pair to verify the signature, while the latter uses a shared secret.

  3. In Auth0, go to Applications > Applications to create a Machine to Machine Applications that will be calling your API. When creating the application, choose the API it should have access to and its permissions.

  4. Go to Applications > Applications > [Your App] > Quick Start to see how to make a call to get an access token.

    For example:

    nix
    curl --request POST \
    --url https://myaccount.auth0.com/oauth/token \
    --header 'content-type: application/json' \
    --data '{"client_id":"abcd12345….","client_secret":"ABCD12345…","audience":"https://api.mywebsite.com","grant_type":"client_credentials"}'
    nix
    curl --request POST \
    --url https://myaccount.auth0.com/oauth/token \
    --header 'content-type: application/json' \
    --data '{"client_id":"abcd12345….","client_secret":"ABCD12345…","audience":"https://api.mywebsite.com","grant_type":"client_credentials"}'
    output
    json
    {
    "access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...",
    "scope":"read:myapp write:myapp",
    "expires_in":86400,
    "token_type":"Bearer"
    }
    output
    json
    {
    "access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...",
    "scope":"read:myapp write:myapp",
    "expires_in":86400,
    "token_type":"Bearer"
    }

    This response contains a JWT access token as the access_token field inside a JSON response, which expires after 24 hours. The client should attach this to its HTTP requests via an Authentication header.

    What's inside a JWT?

    To see what’s inside the token, check out jwt.io, which can display the fields inside the token.

    For example:

    json
    {
    "alg": "RS256",
    "typ": "JWT"
    }
    {
    "iss": "https://myaccount.auth0.com/",
    "aud": "https://api.mywebsite.com",
    "exp": 1662753594,
    "scope": "read write",
    "gty": "client-credentials"
    }
    {
    // RSASHA256 signature
    }
    json
    {
    "alg": "RS256",
    "typ": "JWT"
    }
    {
    "iss": "https://myaccount.auth0.com/",
    "aud": "https://api.mywebsite.com",
    "exp": 1662753594,
    "scope": "read write",
    "gty": "client-credentials"
    }
    {
    // RSASHA256 signature
    }

    The token contains three parts:

    • a header
    • a payload
    • a cryptographic signature

    The header indicates which algorithm was used to sign the token. The payload contains the name of the issuer, the intended audience, the expiration date, and any permissions (also known as scopes).

Configure the load balancer with RS256 Jump to heading

To enable the load balancer to validate requests with attached JWTs using the RS256 signing algorithm:

  1. Log into your Auth0 account. Go to Applications > Applications > [Your App] > Settings > Advanced Settings > Certificates and download the certificate in the PEM format. Extract the public key from the downloaded PEM file using the openssl x509 command:

    nix
    openssl x509 -pubkey -noout -in ./myaccount.pem > pubkey.pem
    nix
    openssl x509 -pubkey -noout -in ./myaccount.pem > pubkey.pem

    Store the public key file on your load balancer server.

  2. Update your load balancer configuration as shown here:

    haproxy
    frontend myapi
    bind :80
    bind :443 ssl crt /etc/hapee-2.8/certs/foo.com/cert.crt alpn h2
    http-request redirect scheme https unless { ssl_fc }
    http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }
    # get header part of the JWT
    http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
    # get payload part of the JWT
    http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
    http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
    http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
    http-request set-var(txn.scope) http_auth_bearer,jwt_payload_query('$.scope')
    # Validate the JWT
    http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
    http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://myaccount.auth0.com/ }
    http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.mywebsite.com }
    http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/pubkey.pem") -m int 1 }
    http-request set-var(txn.now) date()
    http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }
    # OPTIONAL: Deny requests that lack sufficient permissions
    http-request deny if { path_beg /api/ } { method GET } ! { var(txn.scope) -m sub read }
    http-request deny if { path_beg /api/ } { method DELETE POST PUT } ! { var(txn.scope) -m sub write }
    default_backend servers
    backend servers
    balance roundrobin
    server web1 192.168.56.31:3000 check maxconn 30
    haproxy
    frontend myapi
    bind :80
    bind :443 ssl crt /etc/hapee-2.8/certs/foo.com/cert.crt alpn h2
    http-request redirect scheme https unless { ssl_fc }
    http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }
    # get header part of the JWT
    http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
    # get payload part of the JWT
    http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
    http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
    http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
    http-request set-var(txn.scope) http_auth_bearer,jwt_payload_query('$.scope')
    # Validate the JWT
    http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
    http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://myaccount.auth0.com/ }
    http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.mywebsite.com }
    http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/pubkey.pem") -m int 1 }
    http-request set-var(txn.now) date()
    http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }
    # OPTIONAL: Deny requests that lack sufficient permissions
    http-request deny if { path_beg /api/ } { method GET } ! { var(txn.scope) -m sub read }
    http-request deny if { path_beg /api/ } { method DELETE POST PUT } ! { var(txn.scope) -m sub write }
    default_backend servers
    backend servers
    balance roundrobin
    server web1 192.168.56.31:3000 check maxconn 30

    In this example:

    • We get the token from the Authentication HTTP header by using the http_auth_bearer fetch method. The jwt_header_query converter then extracts fields from the token’s header, and the jwt_payload_query converter extracts fields from the payload.
    • We store the fields in variables by using the http-request set-var directive.
    • We use the http-request deny directive to deny requests that fail validation in some way. This directive allows you to set a string parameter, which lets you return an error message to the user.
    • We validate that the JWT’s fields and signature are valid for the expected issuer, audience, and signing algorithm.
    • We verify the token’s signature using the jwt_verify converter.
    • We relay valid requests to the backend named servers.

Different authentication services may use different names for the fields in the token, such as the Identifier and scopes fields cited above. Inspect the token or consult the authentication service’s documentation to determine the proper field names.

Test it by calling your load balanced API at the address where the load balancer is listening. In your request, the token should be set in the Authorization header:

nix
curl --request GET \
-k \
--url https://192.168.56.20/ \
--header 'authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...'
nix
curl --request GET \
-k \
--url https://192.168.56.20/ \
--header 'authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...'

Do you have any suggestions on how we can improve the content of this page?