Secure DAML Infrastructure - Part 2 - JWT, JWKS and Auth0
In Part 1 of this blog, we described how to set up a PKI infrastructure and configure the DAML Ledger Server to use secure TLS connections and mutual authentication. This protects data in transit and only authorised clients can connect.
An application will need to issue DAML commands over the secure connection and retrieve the subset of contract data that it is authorised to see. To enable this, the Ledger Server uses HTTP security headers (specifically “Authorization” Bearer tokens) to receive an authorization token from the application that describes what it is authorised to do.
The user or application is expected to authenticate against an Identity Provider and in return receive an authorization token for the Ledger. This is presented on every API call.
What are JWT & JWKS?
Java Web Tokens (JWT) is an industry standard way to transmit data between two parties. Full details on JWT can be found at the JWT: Introduction and the associated JWT Handbook. Here we will provide a summary of the specification and how the Ledger Server uses custom claims to define allowed actions of an application.
JWTs are a JSON formatted structure with a HEADER, a PAYLOAD and a SIGNATURE. The Header defines the algorithm used to process the payload, in particular the algorithm used to sign (or encrypt) the payload. The payload contains the details of the authorization given to the application and the signature is over the structure to ensure it has not been tampered in transit. Each section is then base64 encoded with a dot separator between the sections. This is placed in the Authorization HTTP header to pass as part of each HTTP request.
An end-user or application will obtain the token by first authenticating to an Identity Provider and being issued an access token. oAuth protocol defines several means to achieve this including web users (3-way handshake that also ask for consent from human end-user) and applications (2-step Client Credentials Flow, which uses a client_id and client_secret for machine accounts). The Identity Provider will validate the provided credentials and issue a signed token for the request service or API.
So how does Ledger Server get the public key of the signer so it can validate the signature and trust the token. This is where JSONWeb Key Sets (JWKS) comes in. Each Identity Provider publishes a well known URL and we configure the Ledger Server to query this to retrieve the JKWS structure. This contains the public key of the signer and some additional metadata.
In the previous blog post you may have noticed a parameter to the Ledger Server as follows:
This tells Ledger Server to trust tokens generated from, in this case, a specific Auth0 tenant and to use the URL to get the Auth0 JWKS. It also enforces a key signing using RSA keys - RSA keys with a SHA-256 hash function.
JWT can also use other algorithms including Elliptic Curve (ES256 - EC P-256 cipher with SHA-256) and shared secret (HS256). We do not recommend using HS256 for anything more than development / testing as it is open to bruteforce attack of the shared password.
In-depth JWT Example
To give some more detail, an authenticated application submits an Ledger API command over HTTPS (GRPC or JSON) and provides a security header
The token string is of the format:
If we separate out the sections of the JWT token you would get
This is the header, payload and signature encoded as base64 with each section separated by a dot. This is normally a single string.
After decoding (using the provided script ./decode-jwt.sh <filename>) or via the JWT Debugger (https://jwt.io/), the JWT becomes the Header and Payload portions as follows.
These represent the header and payload sections. What does this tell us?
The header shows that the JWT was signed using RS256 and used a specific key (kid value). An identity provider may have multiple signing keys described in the JKWS and this selects which one to use to verify the JWT.
The payload contains many standard attributes (the three letter combinations) and one custom claim ("https://daml.com/ledger-api"). The standard attributes include:
|alg||Algorithm used for signing, here RS256 [checked by API]|
|aud||Audience for token|
|exp||Expiry in Epoch seconds [checked by API]|
|iat||Issued at in Epoch seconds|
|sub||Subject (i.e account name)|
The custom claim ("https://daml.com/ledger-api") details a variety of capabilities for this application:
|admin||Is the application allowed to call to administrative API functions (true/false)|
|actAs||An array of Ledger Party IDs that the application is allowed to submit DAML Commands as|
|readAs||An array of Ledger Party IDs that the application is allowed to read contracts for|
|ApplicationId||A unique ID for the application. If set then Ledger Server will validate that the submitted commands also have this AppID set|
|LedgerId||The Ledger ID of the Ledger that the application is trying to connect.|
The authorizing Identity Management provider is expected to set these to appropriate values for the application that is requesting access.
Full details of the API and exposed service endpoints is available in the DAML Documentation. Details of the API and associated permissions is summarised in the Core Concepts section of the sample repo:
Public services are available to any application connecting to a ledger (Mutual TLS may restrict this but a valid token, with minimally not admin and no parties, is still required). Administrative services are expected to be used by specific applications or operational tooling. The remaining Contracts, Command and Transaction services are restricted to the set of parties the application is authorised for.
JWKS (JSON Web Key Sets)
The final piece of the puzzle is JSON Web Key Sets (JWKS) which an identity provider exposes to distribute its public key to allow signature verification.
An example JWKS format is:
The details for each key, the algorithm being used (RS256), the key type (RSA), use (signatures), the key ID (kid value for which Auth0 uses the key fingerprint x5t), the public key (x5c) and some RSA key parameters (n and e fields. Other fields will be seen for EC keys). JWKS supports the distribution of private keys with additional fields for the private key but this is not used here.
A receiving service (in this case the Ledger Server API) will use this to validate the signed JWT to validate that it was issued by the trusted provider and is unaltered.
Using an example Identity Provider - Auth0
So now we have described JWT and JWKS, how do we use these standards? The following builds on the previous post Easy authentication for your distributed app with DAML and Auth0 that focused on end-user authentication and authorisation. You may want to read this first.
In the reference sample, we provide two options:
- Authenticating using Auth0 for end-users and service accounts
- Authenticating services via local JWT provider for CI/CD automation
The full detailed steps and scripts are described in the reference documentation. To use Auth0 you will need to do the following:
- Create an Auth0 tenant - a free trial tenant is usable for this sample
- Create an Auth0 API to represent the Ledger Server API. This is the target for user and services to access Ledger information via the API
- Create New API
- Provide a name (ex-secure-daml-infra)
- Provide an Identifier (https://daml.com/ledger-api)
- Select Signing Algorithm of RS256
- Create an Auth0 “web application” for end-user authentication and access. This uses a single page application (SPA) with React, to create a sample authenticated page that displays the logged in user details and accesses the current contract set via the API.
- Create new Application
- Select Single Page Application
- Select React
- In App Settings:
- Set Allowed CallBack URLS, Allowed Logout URLS, Allowed Web Origins:
- http://localhost:3000, https://web.acme.com
- Set Allowed CallBack URLS, Allowed Logout URLS, Allowed Web Origins:
- Configure two Auth0 “rule”s - this is a programmatic way in Auth0 to add custom claims to token generated for user authentication requests
Rule: "Onboard user to ledger"
Rule: "Add Ledger API Claims"
- Set up and configure end-users and define login credentials and metadata about their Ledger ID. One of the provided Rules allows metadata to be configured on first access of the user. The DAML Sandbox auto-registers new Parties on first use but production ledgers, particularly on DLT platforms, may require more complex provisioning flows.
- Create a New User
- Enter Email and your preferred (strong) passphrase
- If using local Username / Password database, set connection to Username-Password-Authentication.
- In the app_metadata section of the User, add the following template. You will need to adjust per user so that partyIdentifier matches that name of the user in the Ledger, i.e. "Alice", "Bob", “George”
- Define services (machine-to-machine “applications” in Auth0 terminology) and some associated metadata for each service - which parties they can act or read on behalf of. These are linked to the above API. Each m2m application defines client-Id and client_secret credentials for each service
- Configure an Auth0 “hook” - this is a equivalent to the end user case above but configures a way to define custom claims for services
Once this is in place, you can then update the following:
- add the Auth0 tenant details and each of the service account credential pairs. In a production setting you would use some form of credential vault like Hashicorp Vault, AWS or GCP KMS services, etc to store and pass these to the respective services.
- Update the ./ui/src/auth_config.json to point to the correct Auth0 tenant
The Auth0 environment is now ready for use.
Local JWT Provider
Since depending on third party services is complicated for automated testing environments, we implemented a sample JWT provider that uses code signing certificates issued from the local PKI.
In particular, you can set an option in the env.sh script to require the environment to use a local signer. In this model the following is used:
- A Signing Certificate is issued from the Intermediate CA
- A small Python program then issues JWT tokens for each service with respective custom claims for access. The are placed in <pwd>/certs/jwt directory
- A simple Python web server is provided that exposes a local JKWS endpoint with the code signing certificate reformatted to JWKS format. It also acts as a simple authentication provider for the Python boy which uses oAuth 2-step Client Credential flow to obtain a token.
The steps are in the following scripts:
The tokens are issued with an expiry of one day.
Summary and Next Steps
In this post we reviewed the JWT and JWKS Standards to allow an application to request an authorization token from an Identity Provider and submit with Commands to a Ledger. We showed how to use a sample identity provider (in this case Auth0) to allow end-user and service account authentication and get appropriate authorization tokens.
Next step is to run the sample environment and execute some tests against the environment. This is the topic for the final part of this series.If you want to see the first part on “PKI and certificates” please check here:
Read the first part on PKI and certificates