Authentication
OAuth 2.0 authorization
This page applies to:
- HAProxy 2.5 and newer
- HAProxy Enterprise 2.5r1 and newer
- HAProxy ALOHA 14.0 and newer
With the OAuth 2.0 authorization protocol, you can protect your applications from unauthorized access by requiring that clients send a JSON Web Token with their HTTP requests, proving that they have the necessary set of permissions.
For example, let’s suppose you’ve developed an online service named My Recipes where users can save cooking recipes. Separately, a team of developers creates a client app named Ingredients that helps find low prices on cooking ingredients. You want to allow the ingredients app to access your recipes service to make it easy for users to find recipes that match their ingredients shopping list. But you want to ensure that users consent to the ingredients app accessing their recipes first.
The OAuth 2.0 protocol focuses on allowing third-party applications to access user data or resources without exposing credentials. It isn’t meant for single sign-on or for the user to sign in to an app. It’s meant for granting access between apps and services on behalf of a user. There are several ways to get a user’s consent, including prompting the user. However, for our purposes, we’ll stick with the most straight-forward workflow, which is to give the asking application (the ingredients app) a unique client ID and a secret phrase. We’ll associate the client ID with the permissions we want to grant, so that that part will be set upfront, rather than prompting the user to choose the level of access they want to give. This is called the client credentials flow. It’s ideal for scenarios where you don’t need to involve the user in choosing which permissions to grant and it’s permissible to set permissions beforehand.
It works like this:
-
As the developer of the My Recipes service, you register the client application (Ingredients app) with your OAuth 2.0 authorization server and assign to it a new client ID and secret. You’ll associate that client with the permissions you want to give it.
-
The Ingredients app developers configure their client application to store their client ID and secret within their app.
-
Prior to making a request to the target service (recipes service), the client app will use its client ID and secret to make a request to the OAuth 2.0 authorization server.
-
The OAuth 2.0 authorization server will respond with a JSON Web Token that contains the client’s permissions.
-
The client application will make a request with the JSON Web Token to the target service.
-
HAProxy will validate the token and, if it’s valid and contains the necessary permissions, will allow the request to proceed to recipe server in the backend.
Configure an authorization server Jump to heading
A JSON Web Token conveys a client application’s level of access. Its payload is base64-encoded JSON data that’s cryptographically signed by the OAuth 2.0 authorization server during authentication. In this section, we cover how to register a client application with an OAuth 2.0 authorization server, get a client ID and secret according to the client credentials flow, and set permissions for the client app.
Auth0 Jump to heading
You can sign up for an Auth0 account to authorize client applications before they can access your API. The scenario we’ll describe here is an app using OAuth 2.0 to access an API.
To register your API service and client app with Auth0:
-
Create an account with Auth0 and then log in to your account.
-
Go to Applications > APIs and click Create API. In Auth0, an API is the entity that receives the tokens and grants access, which in our case is HAProxy. But you can create multiple APIs with different names, depending on the number of backend applications you have, and route them all through HAProxy. Think of it as the public face of your application, with HAProxy serving as a proxy.
When creating the API, set:
- Name to a name for your API, such as
My Recipes API. - Identifier to the HTTPS address you’d like to use for accessing your application. You’ll need to configure your DNS server to send this traffic to HAProxy. For example, use
https://recipes.mywebsite.com. This address will get included in the token as theaudclaim, which we’ll check in our HAProxy configuration to make sure that the token we receive is meant for this API. - JSON Web Token (JWT) Signing Algorithm to either
RS256orHS256. TheRS256option uses an X.509 certificate to verify the signature, whileHS256uses a shared secret.
- Name to a name for your API, such as
-
Click Create to create the API.
-
Optional: After creating the API, you can edit it and then create permissions to control the access given to client applications. By default, all client applications get unrestricted access to the API if they present a valid token. With permissions, however, you can define more specific levels of access and assign them to clients selectively. For example, you could create permissions named
read:recipesandwrite:recipes. -
To register the client app you want to give access to, go to Applications > Applications to click Create Application.
When creating the application, set:
- Name to a name for the client app, such as
Ingredients App. - Application Type to Machine to Machine Application.
- Select the API you created,
My Recipes API, as the authorized API. - Optional: If you’ve defined permissions on your API, you can select them here to grant them to the client app.
- Name to a name for the client app, such as
-
Click Authorize to give access to the client application.
-
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 x509command:nixopenssl x509 -pubkey -noout -in ./myaccount.pem > pubkey.pemnixopenssl x509 -pubkey -noout -in ./myaccount.pem > pubkey.pemStore the public key file on your load balancer server.
Use jwt_verify_cert in newer versions
Beginning in HAProxy 3.3, you no longer need to extract the public key from the certificate. By using the
jwt_verify_certconverter in your configuration instead of the olderjwt_verifyconverter, the key will be extracted automatically. -
Go to Applications > Applications > [Your App] > Credentials to copy the client ID and client secret, which you can then copy to the client app’s settings.
-
To see it working, go to Applications > Applications > [Your App] > Quick Start for an example
curlcall that gets a token. For example:nixcurl --request POST \--url https://myaccount.auth0.com/oauth/token \--header 'content-type: application/json' \--data '{"client_id":"abcd12345...","client_secret":"ABCD12345...","audience":"https://recipes.mywebsite.com","grant_type":"client_credentials"}'nixcurl --request POST \--url https://myaccount.auth0.com/oauth/token \--header 'content-type: application/json' \--data '{"client_id":"abcd12345...","client_secret":"ABCD12345...","audience":"https://recipes.mywebsite.com","grant_type":"client_credentials"}'outputjson{"access_token":"eyJhbGciOiJSUzI1NiCUfGiCxRykRKvWnP4Nr...","scope":"read:recipes","expires_in":86400,"token_type":"Bearer"}outputjson{"access_token":"eyJhbGciOiJSUzI1NiCUfGiCxRykRKvWnP4Nr...","scope":"read:recipes","expires_in":86400,"token_type":"Bearer"}This response contains a JSON Web Token as
access_token, any assigned permissions asscope, the time in seconds until expiration, and the token type. The client application should attach the token to its HTTP requests via anAuthenticationheader.What's inside a JSON Web Token?
To see what’s inside the token, use the debugger at the JSON Web Token Debugger website, which can display the JWT fields inside the
access_tokenvalue.For example:
json{"alg": "RS256","typ": "JWT"}{"iss": "https://myaccount.auth0.com/","aud": "https://api.mywebsite.com","exp": 1662753594,"scope": "read:myapp write:myapp","gty": "client-credentials"}{// RSASHA256 signature}json{"alg": "RS256","typ": "JWT"}{"iss": "https://myaccount.auth0.com/","aud": "https://api.mywebsite.com","exp": 1662753594,"scope": "read:myapp write:myapp","gty": "client-credentials"}{// RSASHA256 signature}A JSON Web 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). The signature proves the authenticity and integrity of the token.
Keycloak Jump to heading
You can use Keycloak to authorize client applications before they can access your API. The scenario we’ll describe here is an app using OAuth 2.0 to access an API.
To register your API service and client app with Keycloak:
-
Log in to the Keycloak Admin Console, which listens on port 8080.
-
Select a realm or create a new realm. For example, create a realm named
recipes-api. A realm contains the settings for your services and the clients you want to authorize.Realm name
Don’t create a realm with a name that has spaces in it. This can cause errors later on.
-
Go to Configure > Realm settings and select the Tokens tab.
Set Default Signature Algorithm to one of the values that HAProxy supports:
ES256,ES384,ES512,HS256,HS384,HS512,PS256,PS384,PS512RS256,RS384, orRS512. The algorithms that begin withES,RS, andPSuse an X.509 certificate to verify the signature, whileHSalgorithms use a shared secret. -
Optional: You can create permissions to control the access given to client applications. By default, all client applications get unrestricted access to the API if they present a valid token. With permissions, however, you can define more specific levels of access and assign them to clients selectively. For example, you could create permissions named
read:recipesandwrite:recipes.- Go to Manage > Client scopes and click Create client scope.
- Set Name to the name of the permission, such as
read:recipes. - Set Include in token scope to
on, which adds this permission to thescopeclaim in tokens.
Click Save.
-
To register the client app you want to give access to, go to Manage > Clients and click Create client.
When creating the client, set:
- Client type to
OpenID Connect, which is a protocol built upon OAuth. - Client ID to a unique identifier for the client application. For example, use
ingredients-appor use a GUID. - Name to a display-friendly name, such as
Ingredients App. - Description to a short description, such as
An application for finding deals on ingredients.
Click Next.
- Client type to
-
On the next screen, which is Capability config, set:
- Client authentication to
on. This enables confidential access. - Service account roles as an enabled authentication flow. This enables the Client credentials flow.
Click Next.
- Client authentication to
-
On the next screen, which is Login settings, click Save.
-
Optional: To assign a permission to the client:
- Go to Manage > Clients and choose the client from the list (e.g.
ingredients-app). - Select the Client scopes tab.
- Click Add client scope.
- Select the permission (e.g.
read:recipes). - Click Add > Default.
- Go to Manage > Clients and choose the client from the list (e.g.
-
To set the
audclaim in access tokens, which we’ll check in our HAProxy configuration to make sure that the tokens we receive are meant for this API:- Go to Manage > Clients and choose the client from the list (e.g.
ingredients-app). - Select the Client scopes tab.
- Select the
<client-id>-dedicatedscope (e.g.ingredients-app-dedicated). - The Mappers screen displays. Click Configure a new mapper and select
Audience. - The Add Mapper screen displays.
- Set Name to
audience-mapper. - Set Included Custom Audience to the HTTPS address you’d like to use for accessing your API. You’ll need to configure your DNS server to send this traffic to HAProxy. For example, use
https://recipes.mywebsite.com.
Click Save.
- Go to Manage > Clients and choose the client from the list (e.g.
-
Download the public key that you can use to verify the tokens that Keycloak signs.
- Go to Configure > Realm settings and select the Keys tab.
- Download the public key depending on the signature algorithm you configured. For example, save the public key for the
RS256algorithm aspubkey.pem. - Store the public key file on your load balancer server.
-
Go to Manage > Clients and choose the client from the list (e.g.
ingredients-app).- Select the Credentials tab.
- Copy the client secret, which you can then copy to the client app’s settings, along with the client ID.
-
To see it working, try using
curlto make a request to Keycloak for a new token. Send the client ID and client secret.nixcurl --request POST \--url 'http://localhost:8080/realms/recipes-api/protocol/openid-connect/token' \--data 'client_id=ingredients-app' \--data 'client_secret=t1eAKDBTz4DR9CRsjmHcJBqAXVNSwHBt' \--data 'grant_type=client_credentials'nixcurl --request POST \--url 'http://localhost:8080/realms/recipes-api/protocol/openid-connect/token' \--data 'client_id=ingredients-app' \--data 'client_secret=t1eAKDBTz4DR9CRsjmHcJBqAXVNSwHBt' \--data 'grant_type=client_credentials'outputjson{"access_token":"eyJhbGciOiJSUzI1NiCUfGiCxRykRKvWnP4Nr...","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"read:recipes profile email"}outputjson{"access_token":"eyJhbGciOiJSUzI1NiCUfGiCxRykRKvWnP4Nr...","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"read:recipes profile email"}This response contains a JSON Web Token as
access_token, any assigned permissions asscope, the time in seconds until expiration, and the token type. The client application should attach the token to its HTTP requests via anAuthenticationheader.
Microsoft Entra ID Jump to heading
You can use Microsoft Entra ID to authorize client applications before they can access your API. The scenario we’ll describe here is an app using OAuth 2.0 to access an API. Microsoft Entra ID uses RS256 as the token signing algorithm and you cannot change this.
To register your API service and client app with Microsoft Entra ID:
-
Sign in to the Microsoft Azure portal.
-
Search for and select Microsoft Entra ID. If you manage multiple tenants, choose Manage tenants, select a tenant, and then click Switch.
-
Register the API that you want to protect with OAuth. From the Microsoft Entra ID Overview page, go to Manage > App registrations in the left-hand menu and choose New registration. On the Register an application screen, set:
- a name for the application for which you’re enabling OAuth-based authorization, such as
My Recipes API. - supported account types to
Single tenant only.
Click Register.
- a name for the application for which you’re enabling OAuth-based authorization, such as
-
To expose this API for OAuth, go to Manage > App registrations and select your API (e.g.
My Recipes API).- Click Expose an API.
- Next to Application ID URI, click Add. The Application ID URI has a value like
api://e42b4c0c-f14b-476a-9995-96adb78fbb92. This will be the value of theaudclaim in the access token. Save this value to use later. - Click Save.
-
Register the client application that will access the API. Go to Manage > App registrations in the left-hand menu and choose New registration. On the Register an application screen, set:
- a name for the client application, such as
Ingredients App. - supported account types to
Single tenant only.
Click Register.
- a name for the client application, such as
-
From the overview screen, add a client secret to the client application:
- Copy the Application (client) ID to use later. It has a value like
e202df99-99ef-48db-a41d-b42c5f1b8bf1. - Click Add a certificate or secret.
- Add a client secret and give it a description that indicates the client application it belongs to (e.g.
Ingredients App). - Click Add.
- Copy the client secret value to use later. It has a value like
1z-8Q~iarOwhIFkugwuu4D5inK.2bF1V007bBbDf.
- Copy the Application (client) ID to use later. It has a value like
-
From the app registrations overview screen, select Endpoints and copy the endpoint URL named OAuth 2.0 token endpoint (v2). It has a value like
https://login.microsoftonline.com/<TENANT-ID>/oauth2/v2.0/token. This is the endpoint from which client applications will request access tokens. -
Optional: You can create permissions to control the access given to client applications. By default, all client applications get unrestricted access to the API if they present a valid token. With permissions, however, you can define more specific levels of access and assign them to clients selectively. For example, you could create permissions named
read:recipesandwrite:recipes.- Go to Manage > App registrations and select your API (e.g.
My Recipes API). - Go to App roles and click Create app role.
- Set the app role’s display name, such as
Read recipes. - Set Allowed member types to
Both. - Set the value, such as
read:recipes. - Set the description, such as
Allow reading recipes. - Click Apply.
- Go to Manage > App registrations and select your API (e.g.
-
Optional: Assign a permission to the client application.
- Go to Manage > App registrations and select the client application (e.g.
Ingredients App). - Go to API permissions and click Add a permission.
- Under APIs my organization uses, select your API (e.g.
My Recipes API). - Select the permission to assign, such as
Read recipes. - Click Add permission.
- To immediately grant this permission without needing consent during the authorization workflow, click Grant admin consent.
Permissions are in the roles claim
Microsoft Entra ID puts permissions into the access token as a claim named
roles, unlike other OAuth 2.0 providers that put them into a claim namedscope. - Go to Manage > App registrations and select the client application (e.g.
-
To see it working, try using
curlto make a request to Microsoft Entra ID for a new token. Send the client ID and client secret.nixcurl --request POST \--url 'https://login.microsoftonline.com/<YOUR-TENANT-ID>/oauth2/v2.0/token' \--data 'scope=<APPLICATION-ID-URI>/.default' \--data 'client_id=<CLIENT-ID>' \--data 'client_secret=<CLIENT-SECRET>' \--data 'grant_type=client_credentials'nixcurl --request POST \--url 'https://login.microsoftonline.com/<YOUR-TENANT-ID>/oauth2/v2.0/token' \--data 'scope=<APPLICATION-ID-URI>/.default' \--data 'client_id=<CLIENT-ID>' \--data 'client_secret=<CLIENT-SECRET>' \--data 'grant_type=client_credentials'For example:
nixcurl --request POST \--url 'https://login.microsoftonline.com/abcd1234-defg-1234-abcd-efgi53456789/oauth2/v2.0/token' \--data 'scope=api://e42b4c0c-f14b-476a-9995-96adb78fbb92/.default' \--data 'client_id=e202df99-99ef-48db-a41d-b42c5f1b8bf1' \--data 'client_secret=1z-8Q~iarOwhIFkugwuu4D5inK.2bF1V007bBbDf' \--data 'grant_type=client_credentials'nixcurl --request POST \--url 'https://login.microsoftonline.com/abcd1234-defg-1234-abcd-efgi53456789/oauth2/v2.0/token' \--data 'scope=api://e42b4c0c-f14b-476a-9995-96adb78fbb92/.default' \--data 'client_id=e202df99-99ef-48db-a41d-b42c5f1b8bf1' \--data 'client_secret=1z-8Q~iarOwhIFkugwuu4D5inK.2bF1V007bBbDf' \--data 'grant_type=client_credentials'outputjson{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"yJhbGciOiJSUzI1NiCUfGiCxRykRKvWnP4Nr..."}outputjson{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"yJhbGciOiJSUzI1NiCUfGiCxRykRKvWnP4Nr..."}This response contains a JSON Web Token as
access_token, the time in seconds until expiration, and the token type. The client application should attach the token to its HTTP requests via anAuthenticationheader.
Configure HAProxy Jump to heading
HAProxy provides configuration directives that cover all of the functionality needed to validate JSON Web Tokens, including checking that a token:
- hasn’t expired.
- was cryptographically signed by a trusted authorization server.
- is meant for your application and not someone else’s.
- contains any necessary permissions (typically defined in the token as
scope), such as read and write permissions.
HAProxy proxies traffic to your application, handling the validation of JSON Web Tokens and denying unauthorized requests. For example, if the client sends an HTTP PUT request, indicating that they want to update data, but they don’t have the write permission, you can deny the request. Beyond validation and granting access, you can also relay the token’s claims to your backend application via HTTP headers for additional processing.
Validate tokens Jump to heading
To configure HAProxy for token validation, make these changes to your frontend configuration section:
-
Add a
binddirective that enables HTTPS via thesslandcrtarguments. This ensures that tokens will be sent over a secure connection. For details on how to configure HTTPS, see Basics - Enable TLS.haproxyfrontend myfrontendbind :80bind :443 ssl crt /etc/hapee-3.3/certs/mywebsite.com/ssl.pem...haproxyfrontend myfrontendbind :80bind :443 ssl crt /etc/hapee-3.3/certs/mywebsite.com/ssl.pem... -
Add directives that validate tokens.
haproxy# Deny requests missing an Authorization headerhttp-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }# extract header part of the tokenhttp-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')# extract payload part of the tokenhttp-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 tokenhttp-request deny content-type 'text/html' string 'Unsupported token signing algorithm' unless { var(txn.alg) -m str RS256 }http-request deny content-type 'text/html' string 'Invalid token issuer' unless { var(txn.iss) -m str https://myaccount.auth0.com/ }http-request deny content-type 'text/html' string 'Invalid token audience' unless { var(txn.aud) -m str https://recipes.mywebsite.com }http-request deny content-type 'text/html' string 'Invalid token signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/path/to/pubkey.pem") -m int 1 }http-request set-var(txn.now) date()http-request deny content-type 'text/html' string 'Token has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }haproxy# Deny requests missing an Authorization headerhttp-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }# extract header part of the tokenhttp-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')# extract payload part of the tokenhttp-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 tokenhttp-request deny content-type 'text/html' string 'Unsupported token signing algorithm' unless { var(txn.alg) -m str RS256 }http-request deny content-type 'text/html' string 'Invalid token issuer' unless { var(txn.iss) -m str https://myaccount.auth0.com/ }http-request deny content-type 'text/html' string 'Invalid token audience' unless { var(txn.aud) -m str https://recipes.mywebsite.com }http-request deny content-type 'text/html' string 'Invalid token signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/path/to/pubkey.pem") -m int 1 }http-request set-var(txn.now) date()http-request deny content-type 'text/html' string 'Token has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }In this example:
- We deny requests that are missing an
AuthorizationHTTP header. - We extract the token from the
AuthorizationHTTP header by using thehttp_auth_bearerfetch method. - The
jwt_header_queryconverter extracts fields from the token’s header. The header contains thealgfield, which indicates the algorithm used to sign the token, eitherRS256orHS256. - The
jwt_payload_queryconverter extracts fields from the payload. The payload contains theissfield (issuer, which is the authorization server’s URL),aud(audience, your application’s URL),exp(token’s expiration date), andscope(optional permissions). For Microsoft Entra ID, change changescopetoroles. Different OAuth 2.0 services may use different names for the fields in the token. Inspect the token or consult the service’s documentation to determine the proper field names. - We store the header and payload fields in variables by using the
http-request set-vardirective. - We validate that the JWT’s fields and signature are valid for the expected issuer, audience, and signing algorithm. The
http-request denydirectives deny requests that fail validation in some way and return an error message to the client application. Change these values for your setup. - To verify the token’s signature, we use the
jwt_verifyconverter. Because we’ve set the expectedalgtoRS256, we pass in the public key that we got from the authorization server. When usingHS256, pass in the shared secret instead of the public key. Change these values for your setup. Some authorization servers, such as Microsoft Entra ID, let you get the public key from a discovery URL endpoint (e.g.https://login.microsoftonline.com/<YOUR-TENANT-ID>/discovery/keys). Match up thekid(key ID) from the decrypted access token’s header with the keys listed in the discovery endpoint’s response to find the right key.
- We deny requests that are missing an
-
Optional: Deny requests based on specific permissions with
http-request denydirectives.haproxyhttp-request deny if { method GET } ! { var(txn.scope) -m sub read }http-request deny if { method DELETE POST PUT } ! { var(txn.scope) -m sub write }haproxyhttp-request deny if { method GET } ! { var(txn.scope) -m sub read }http-request deny if { method DELETE POST PUT } ! { var(txn.scope) -m sub write }In this example:
- Deny
GETrequests if missing thereadscope. - Deny
DELETE,POST, andPUTrequests if missing thewritescope. - For Microsoft Entra ID, use
rolesinstead ofscope.
- Deny
-
Test it by calling your load balanced API at its listening address. In your request, set the token in the
Authorizationheader.nixcurl --request GET \--insecure \--url https://recipes.mywebsite.com \--header 'authorization: Bearer eyJhbGciOiJSUzI1NiCUfGiCxRykRKvWnP4Nr...'nixcurl --request GET \--insecure \--url https://recipes.mywebsite.com \--header 'authorization: Bearer eyJhbGciOiJSUzI1NiCUfGiCxRykRKvWnP4Nr...'
See also Jump to heading
For complete information on the directives related to OAuth authorization, see the HAProxy Configuration Manual:
- Fetch the authorization token: http_auth_bearer
- Deny an HTTP request: http-request deny
- Set a variable: http-request set-var
- Query a JSON Web Token header: jwt_header_query
- Query a JSON Web Token payload: jwt_payload_query