Shine
Shine

Reputation: 588

Is bilateral private_key_jwt assertion necessary?

What is the added value to applying the private_key_jwt authentication approach bilaterally?

Currently I'm dealing with consuming from an API documented here. Authentication works by applying a private_key_jwt client_assertion to establish the client side identity.

However, the specification also denotes that the server side returns their own signed token inside the response body as json. I do not understand the added value of returning an encoded private_key_jwt or client_assertion in the response body. Is there benefit w.r.t. security?

I would intuitively expect the response to be a straightforward json response. Not a payload encoded within a json web token.

Disclaimer; I am not a security expert so logical mistakes on my side are expected.

Example request per documentation (using an already retrieved access token):

> Authorization: Bearer IIeDIrdnYo2ngwDQYJKoZIhvcNAQELBQAwSDEZMBcGA1UEAwwQaVNIQ

GET /parties?
    eori=EU.EORI.NL000000004&
    certificate_subject_name=C=NL, SERIALNUMBER=EU.EORI.NL000000004, CN=iSHARE Test Authorization Registry&
    active_only=true

Then the example response of the API that I can retrieve using an access_token.

{
  "parties_token": "eyJ4NWMiOlsiTUlJRWl..."
}

Which decodes (jwt.decode) into a payload;

{
  "iss": "EU.EORI.NL000000000",
  "sub": "EU.EORI.NL000000000",
  "jti": "0868904d8ed94c01a0a4d6dd5c65ce9e",
  "iat": 1591965905,
  "exp": 1591965935,
  "aud": "EU.EORI.NL000000001",
  "parties_info": {
    "count": 1,
    "data": [
      {
        "party_id": "EU.EORI.NL000000004",
        "party_name": "AskMeAnything Authorization Registry",
        "adherence": {
          "status": "Active",
          "start_date": "2018-04-26T14:59:14Z",
          "end_date": "2020-07-25T14:59:14Z"
        },
        "certifications": [
          {
            "role": "AuthorisationRegistry",
            "start_date": "2018-01-04T00:00:00Z",
            "end_date": "2020-02-02T00:00:00Z",
            "loa": 3
          }
        ]
      }
    ]
  }
}

Hence, they've encoded the content into the payload of the token. I do not understand the benefit of this approach as I've already used the access_token to authenticate. What's the point of encoding this information again in the body?

Upvotes: 0

Views: 104

Answers (1)

Gary Archer
Gary Archer

Reputation: 29291

OAUTH TRUST AND KEY MANAGEMENT

The most mainstream use of JWTs is as API message credentials. These JWT access tokens are issued by the authorization server (which owns the private key) to clients, who then send the access token to APIs. When an API receives an access token it verifies the JWT signature on every request, using the JWKS URI of the authorization server, which provides access to the token signing public key.

Here is some example Node.js code to validate the JWT. More complete code would check required scopes and implement error handling. The result is a Claims Principal containing secure values, that the API uses to implement its authorization business logic.

import {createRemoteJWKSet, jwtVerify, JWTPayload} from 'jose';

const remoteJWKSet = createRemoteJWKSet(new URL('https://login.example.com/jwks'));

async function validateAccessToken(accessToken: string): Promise<JWTPayload> {

    const options = {
        algorithms: [expectedAlgorithm],
        issuer: expectedIssuer,
        audience: expectedAudience,
    };

    const result = await jwtVerify(accessToken, remoteJWKSet, options);
    return result.payload;
}

The more subtle point about the code is the trust and key management. By default, the API only trusts the authorization server, as indicated by the JWKS URI configured. The authorization server externalizes all key management from clients and APIs. The authorization server (and the JWT library) also provide a solution that automatically deals with token signing key renewal.

CLIENT ASSERTIONS AND KEY MANAGEMENT

When interacting with third party APIs, it is common for their authorization server to require strong client authentication, since a string client secret might leak and be abused for a long time by a malicious party. The authorization server therefore requires one of the options recommended in Financial Grade APIs, for either mutual TLS or client assertions.

In the latter case, the client application uses its own private key and a library - to issue JWTs - as in this example code of mine to issue a JWT, and sets fields as defined in RFC7523 or OIDC Core. A request to authenticate using a client assertion (JWT) looks like this:

curl --request POST "https://login.example.com/oauth/token"
  -H "content-type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_assertion=my_assertion \
  -d client_assertion_type='urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
  -d scope=parties

The corresponding public key can either be configured in the authorization server, or a JWKS URI can be configured there, with a value such as https://client.partner.com/jwks. The latter option provides best options for managing key renewal for clients. The authorization server will then perform similar JWT validation to check the client's JWT before returning tokens to the client.

APIs AND KEY MANAGEMENT

JWTs are a general format for managing secure values in a way that remains digitally verifiable, and cannot be tampered with. For example, as a normal API request payload is sent through an API gateway, then to one or more upstream APIs, the TLS connection is decrypted. This introduces risks that request or response data might be altered by a malicious user or client. For this reason secure values (such as user IDs, tenant IDs, roles etc) should be encapsulated in JWT access tokens.

In some cases new tokens may be issued using token exchange, or some responses or fields received by clients can use a JWT format, eg signed metadata. In some cases the client can control response formats by sending an accept header. Most commonly, JWTs are returned from the authorization server and not in API responses.

API designers could choose to return some payloads as signed responses, encoded as JWTs. Doing so requires the API to manage and renew its own private and public keypair, while also requiring more work from clients, to first decode a JWT and then transform that to a business payload:

import {createRemoteJWKSet, jwtVerify, JWTPayload} from 'jose';

const remoteJWKSet = createRemoteJWKSet(new URL('https://api.example.com/jwks'));

async function processApiResponse(accessToken: string): Promise<JWTPayload> {

    const options = {
        algorithms: [expectedAlgorithm],
        issuer: expectedIssuer,
        audience: expectedAudience,
    };

    const result = await jwtVerify(accessToken, remoteJWKSet, options);
    return result.payload;
}

SUMMARY

By default in OAuth, all key management is done by the authorization server. It is possible for clients to use their own private keys to strengthen authentication. API owners could choose to use private keys to integrity protect some responses. This seems to be the design used for the Scheme Owner Endpoints in the link you referred to.

Security is always driven by the API owner, who provides the authorization server and sets the rules. The API owner should also provide good documentation and code examples to facilitate end-to-end integration. There is a trade off between security and usability also, since more key usage requires more technical overhead from clients.

Upvotes: 1

Related Questions