Authenticate with OAuth 2.0


The ledger supports the OAuth 2.0 Client Credentials Grant (RFC 6749 §4.4) as an alternative to key-pair JWT authentication. This is designed for machine-to-machine integrations where a client application exchanges a clientId and clientSecret for a short-lived access token, without managing asymmetric keys directly. The returned token is an RS256-signed JWT issued by a designated provider signer on your ledger.

The identity provider (idP) is the entity that issues OAuth2 access tokens: this could either be the ledger itself or an external idP like Auth0.

Prerequisites

  • A ledger with an active authentication policy (configured below)
  • The @minka/ledger-sdk package

Setting up OAuth2 in Ledger

There are one-time setup steps before a client application can exchange credentials for a token.

Option A: Ledger as the id provider

Step 1: Create the identity provider's signer and factor

Create the provider's signer:

import { createRsaKeyPair } from '@minka/crypto'
import { LedgerSdk } from '@minka/ledger-sdk'
import { AccessAction } from '@minka/types'

// Generate an RSA-2048 key pair for the provider signer.
const { secret: rsaPrivateKeyPem, public: rsaPublicKeyDerBase64 } =
      await createRsaKeyPair('der')

// Create the provider signer record on the ledger.
const { keyPair: providerKeyPair } = await sdk.signer
  .init()
  .data({ handle: 'my-oauth-provider' })
  .hash()
  .sign([{ keyPair: adminKeyPair }])
  .send()

Create the provider's signer factor:

// Attach an RSA key-pair factor so the provider can sign JWTs.
// The factor stores the RSA public key (DER base64) and the encrypted private key (PEM).
// The factor handle becomes the `kid` header in every JWT this provider issues,
// so receivers can identify which public key to use for verification.
await sdk.signer
  .with('my-oauth-provider')
  .factor.init()
  .data({
    handle: 'my-oauth-provider-key',
    signer: 'my-oauth-provider',
    schema: 'key-pair',
    format: 'rsa-der',
    public: rsaPublicKeyDerBase64,
    secret: '{{ secret.private }}',
    access: [{ action: AccessAction.Any }],
  })
  .meta({
    proofs: [],
    secret: { private: rsaPrivateKeyPem },
  })
  .hash()
  .sign([{ keyPair: providerKeyPair }])
  .send()

Step 2: Create an authentication policy

An authentication policy designates which signers are allowed to issue OAuth tokens on your ledger. The values array contains at least one entry with schema: 'oauth2'.

import { AccessRecord, AccessAction, PolicyValue } from '@minka/types'

const oauthPolicyValues: PolicyValue[] = [
  {
    schema: 'oauth2',                         // required
    signer: { handle: 'my-oauth-provider' },  // required: references the signer of the provider created in the previous step. 
    config: { "jwt.ttl": 3600 },              // optional: token time-to-live in seconds. Defaults to 3600
    target: { schema: 'oauth-application' },  // optional: scope this provider to only authenticate signers with this specific schema.
  } as PolicyValue,
]

You may also have multiple providers (either ledger-based or external idPs) within the authentication policy. Note: Each additional provider must also be created in ledger as in the previous step.

For example:

const oauthPolicyValues: PolicyValue[] = [
  {
    schema: 'oauth2',
    signer: { handle: 'my-oauth-provider' },
    target: { schema: 'oauth-application' },
  } as PolicyValue,
  {
    schema: 'oauth2',
    signer: { handle: 'another-provider' },
    target: { schema: 'another-application' },
  } as PolicyValue,
]

Proceed to create the authentication policy:

await sdk.policy
  .init()
  .data({
    handle: 'my-oauth-policy',
    schema: 'authentication',
    record: AccessRecord.Any,
    access: [{ action: AccessAction.Any }],
    values: oauthPolicyValues,
  })
  .hash()
  .sign([{ keyPair: adminKeyPair }])
  .send()

Option B: An external idP is the provider (e.g. Auth0)

When an external IdP issues tokens, the ledger does not mint JWTs itself — it only validates them.

Step 1: Create the identity provider's signer and factor

Create a signer referencing the external provider:

// Create the provider signer. Its handle can be any valid identifier.
const { keyPair: providerKeyPair } = await sdk.signer
  .init()
  .data({ handle: 'auth0-idp' }) 
  .hash()
  .sign([{ keyPair: adminKeyPair }])
  .send()

The ledger identifies the verification key from the kid header in the incoming JWT. It looks up a key-pair factor whose handle equals the kid, then uses that factor's public key for RS256 signature verification.

Create the provider's signer factor:

// Fetch the kid and RSA public key (SPKI DER, base64-encoded) from the IdP's JWKS.
// For Auth0, the JWKS endpoint is: https://<your-domain>/.well-known/jwks.json
const idpKid = '<kid-from-jwks>'
const idpPublicKeyDerBase64 = '<spki-der-base64>'

// Attach a key-pair factor whose handle equals the kid from the IdP's JWKS.
// The ledger uses this factor's public key to verify incoming Bearer JWTs.
// No private key is stored — the ledger only verifies, never signs.
await sdk.signer
  .with('auth0-idp')
  .factor.init()
  .data({
    handle: idpKid, // the handle must equal the kid from the IdP's JWKS.
    signer: 'auth0-idp',
    schema: 'key-pair',
    format: 'rsa-der',
    public: idpPublicKeyDerBase64,
    access: [{ action: AccessAction.Any }],
  })
  .meta({ proofs: [] })
  .hash()
  .sign([{ keyPair: providerKeyPair }])
  .send()

Client flow: clients use their clientId and clientSecret with the external IdP's token endpoint (e.g., Auth0's /oauth/token) to obtain a signed JWT. They then use that JWT as the Bearer token for ledger API calls — no call to the ledger's /oauth/token is needed.

Step 2: Create the authentication policy

For example:

const oauthPolicyValues: PolicyValue[] = [
  {
    schema: 'oauth2',                 // required
    signer: { handle: 'auth0-idp' },  // required: references the signer of the provider created in the previous step
  } as PolicyValue,
]

Proceed to create the authentication policy:

await sdk.policy
  .init()
  .data({
    handle: 'my-oauth-policy',
    schema: 'authentication',
    record: AccessRecord.Any,
    access: [{ action: AccessAction.Any }],
    values: oauthPolicyValues,
  })
  .hash()
  .sign([{ keyPair: adminKeyPair }])
  .send()

Specifying a target with an external idP will require all signers authenticating via this provider to exist in ledger AND match the target fields. For example: For an Auth0 JWT with sub: 'Auth0|12345', a signer with handle: Auth0|12345 must exist with the schema matching to the policy's target.

Onboarding a user

The following onboarding is required for all signers when the identity provider is the ledger itself, or when a target is assigned to an external identity provider.

Create the application signer and OAuth credentials factor

Create the applcation signer: the entity authenticating via OAuth. It appears as the sub (subject) claim in the JWT.

import { LedgerOAuthFactor } from '@minka/types'

// Create the application signer.
await sdk.signer
  .init()
  .data({ handle: 'my-app-signer' })
  .hash()
  .sign([{ keyPair: adminKeyPair }])
  .send()

Create an oauth-client-credentials factor: the credentials used by the application signer to fetch the access token. The ledger auto-generates the clientId and clientSecret — retrieve the clientSecret immediately after creation using include: ['meta.secret'].

// Attach an OAuth credentials factor.
const { factor, meta } = await sdk.signer
  .with('my-app-signer')
  .factor.init()
  .data({
    handle: 'my-app-oauth-credentials',
    signer: 'my-app-signer',
    schema: 'oauth-client-credentials',
    access: [{ action: AccessAction.Any }],
  })
  .meta({ proofs: [] })
  .hash()
  .sign([{ keyPair: appKeyPair }])
  .send({
    query: { include: ['meta.secret'] },
  })

const clientId = factor.clientId
const clientSecret = meta.secret.clientSecret

The factor credentials can also be fetched after creation using:

const { factor, meta } = await sdk.signer
  .with('my-app-signer')
  .factor.read<LedgerOAuthFactor>('my-app-oauth-credentials', {
    query: { include: ['meta.secret'] },
  })

Exchanging credentials for a token

For ledger-based identity providers, you may exchange the clientId and clientSecret of your application for an access token using the SDK:

const { accessToken, tokenType, expiresIn } =
  await sdk.oauth.exchangeToken(clientId, clientSecret)

// accessToken: RS256-signed JWT
// tokenType:   'Bearer'
// expiresIn:   seconds until the token expires (default: 3600)

The SDK handles the POST /v2/oauth/token request, the Authorization: Basic header encoding, and the application/x-www-form-urlencoded body format automatically.

Using the token for API calls

exchangeToken returns the token but does not configure the SDK automatically. Pass the token to sdk.setAuthParams to authenticate all subsequent calls on that SDK instance.

sdk.setAuthParams({ overrideToken: accessToken })

// All subsequent calls on this SDK instance use the OAuth token.
const { wallet } = await sdk.wallet.read('my-wallet')

Alternatively, construct the SDK with a pre-obtained token from the start:

const sdk = new LedgerSdk({
  server: '<your ledger URL>',
  secure: { overrideToken: accessToken },
})

The token is valid for expiresIn seconds. Call exchangeToken again before expiry and update the SDK:

const { accessToken: refreshed } = await sdk.oauth.exchangeToken(clientId, clientSecret)
sdk.setAuthParams({ overrideToken: refreshed })

On this page