Skip to content

Consume internal API on behalf of a citizenΒΆ

This how-to guides you through the steps required to consume an API secured with TokenX:

PrerequisitesΒΆ

  • Your application receives requests with a citizen subject token in the Authorization header
    • The subject token can either be from ID-porten or from TokenX itself
  • The API you're consuming has granted access to your application

Configure your applicationΒΆ

Enable TokenX in your application:

app.yaml
spec:
  tokenx:
    enabled: true

Depending on how you communicate with the API you're consuming, configure the appropriate outbound access policies.

Exchange tokenΒΆ

To exchange a token, you can either:

Exchange tokens with TexasΒΆ

Texas is not enabled by default

See the Texas documentation for more information.

Send a HTTP POST request to the endpoint found in the NAIS_TOKEN_EXCHANGE_ENDPOINT environment variable. The request must have a Content-Type header set to either:

  • application/json or
  • application/x-www-form-urlencoded

The body of the request should contain the following parameters:

Parameter Example Value Description
identity_provider tokenx Always tokenx.
target <cluster>:<namespace>:<other-app-name> The intended audience (target API or recipient) of the new token.
user_token eyJra... The user's access token from the inbound request. Token that should be exchanged.
Token request
POST ${NAIS_TOKEN_EXCHANGE_ENDPOINT} HTTP/1.1
Content-Type: application/json

{
    "identity_provider": "tokenx",
    "target": "<cluster>:<namespace>:<other-app-name>",
    "user_token": "eyJra..."
}
Token request
POST ${NAIS_TOKEN_EXCHANGE_ENDPOINT} HTTP/1.1
Content-Type: application/x-www-form-urlencoded

identity_provider=tokenx&
target=<cluster>:<namespace>:<other-app-name>&
user_token=eyJra...
Successful response
{
    "access_token": "eyJra...",
    "expires_in": 3599,
    "token_type": "Bearer"
}

Your application does not need to validate this token.

Tokens are cached by default with regards to the expires_in field. To forcibly fetch a new token, set the skip_cache=true parameter in the request.

Exchange tokens manuallyΒΆ

Create client assertionΒΆ

To perform a token exchange, your application must authenticate itself. To do so, create a client assertion.

Sign the client assertion with your applications private key contained within the TOKEN_X_PRIVATE_JWK environment variable.

The assertion must contain the following claims:

Claim Example Value Description
sub dev-gcp:my-team:app-a The subject of the token. Set to the TOKEN_X_CLIENT_ID environment variable.
iss dev-gcp:my-team:app-a The issuer of the token. Set to the TOKEN_X_CLIENT_ID environment variable.
aud https://tokenx.dev-gcp.nav.cloud.nais.io/token The audience of the token. Set to the TOKEN_X_TOKEN_ENDPOINT environment variable.
jti 83c580a6-b479-426d-876b-267aa9848e2f The JWT ID of the token. Used to uniquely identify a token. Set this to a UUID or similar.
nbf 1597783152 nbf stands for not before. Set to now.
iat 1597783152 iat stands for issued at. Set to now.
exp 1597783182 exp is the expiration time of the token. Between 1 and 120 seconds after now. Typically 30 seconds is fine.

Additionally, the headers of the assertion must contain the following parameters:

Parameter Value Description
kid 93ad09a5-70bc-4858-bd26-5ff4a0c5f73f The key identifier of the key used to sign the assertion. This identifier is available in the JWK found in TOKEN_X_PRIVATE_JWK.
typ JWT Represents the type of this JWT. Set this to JWT.
alg RS256 Represents the cryptographic algorithm used to secure the JWT. Set this to RS256.
Example Client Assertion Values
Header
{
  "kid": "93ad09a5-70bc-4858-bd26-5ff4a0c5f73f",
  "typ": "JWT",
  "alg": "RS256"
}
Payload
{
  "sub": "prod-gcp:namespace-gcp:gcp-app",
  "aud": "https://tokenx.dev-gcp.nav.cloud.nais.io/token",
  "nbf": 1592508050,
  "iss": "prod-gcp:namespace-gcp:gcp-app",
  "exp": 1592508171,
  "iat": 1592508050,
  "jti": "fd9717d3-6889-4b22-89b8-2626332abf14"
}

Create and perform exchange requestΒΆ

Now that you have a client assertion, we can use this to exchange the inbound token you received from your consumer.

The token request is an HTTP POST request. It must have the Content-Type header set to application/x-www-form-urlencoded

The body of the request should contain the following parameters:

Parameter Value Description
grant_type urn:ietf:params:oauth:grant-type:token-exchange Always urn:ietf:params:oauth:grant-type:token-exchange.
client_assertion_type urn:ietf:params:oauth:client-assertion-type:jwt-bearer Always urn:ietf:params:oauth:client-assertion-type:jwt-bearer.
client_assertion eyJraWQ... The client assertion. Token that authenticates your application. It should be unique and only used once.
subject_token_type urn:ietf:params:oauth:token-type:jwt Always urn:ietf:params:oauth:token-type:jwt.
subject_token eyJraWQ... The citizen's subject token from the inbound request, either from ID-porten or TokenX. Token that should be exchanged.
audience <cluster>:<namespace>:<appname> The identifier for the target application. Intended audience or recipient of the new token.

Send the request to the token_endpoint, i.e. the URL found in the TOKEN_X_TOKEN_ENDPOINT environment variable.

Example request
POST ${TOKEN_X_TOKEN_ENDPOINT} HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange&
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
client_assertion=eY...............&
subject_token_type=urn:ietf:params:oauth:token-type:jwt&
subject_token=eY...............&
audience=prod-gcp:namespace1:app1
Success responseΒΆ

TokenX responds with a JSON object that contains the new access token:

Success response body
{
  "access_token" : "eyJraWQiOi..............",
  "expires_in" : 899,
  ...
}

Your application does not need to validate this token.

Cache your tokens

The expires_in field in the response indicates the lifetime of the token in seconds.

Use this field to cache and reuse the token to minimize network latency impact.

A safe cache key for this flow is key = sha256($subject_token + $audience).

Error responseΒΆ

If the exchange request is invalid, you will receive a structured error as specified in RFC 8693, Section 2.2.2:

Error response body
{
  "error_description" : "token exchange audience <some-audience> is invalid",
  "error" : "invalid_request"
}

Consume APIΒΆ

Once you have acquired a new token, you can finally consume the target API by using the token as a Bearer token:

GET /resource HTTP/1.1

Host: api.example.com
Authorization: Bearer eyJraWQ...