Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
jhrv committed May 22, 2024
1 parent bbe6bcd commit a08560a
Show file tree
Hide file tree
Showing 8 changed files with 387 additions and 345 deletions.
2 changes: 1 addition & 1 deletion docs/auth/maskinporten/how-to/consume.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Maskinporten will respond with a JSON object that contains the access token.
}
```

???+ note "Cache your tokens!"
???+ note "Cache your tokens"

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

Expand Down
6 changes: 6 additions & 0 deletions docs/auth/tokenx/.pages
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
title: TokenX
nav:
- README.md
- 🎯 How-To: how-to
- 📚 Reference: reference
- ...
43 changes: 43 additions & 0 deletions docs/auth/tokenx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
tags: [auth, services]
---

# TokenX

TokenX is NAIS' own implementation of OAuth 2.0 Token Exchange to be able to propagate citizen identities originating from ID-porten.

This allows applications to act on behalf of a citizen, while maintaining the [zero-trust](../../workloads/explanations/zero-trust.md) security model between applications throughout the request chain.

NAIS provides support for declarative registration and configuration of TokenX resources.
These cover two distinct use cases:

## Consume an API

To consume an API secured with TokenX on behalf of a citizen, you'll need to exchange the inbound [token](../explanations/README.md#tokens) for a new token only valid for the API you want to access. This new token will then contain the citizens identity context.

```mermaid
graph LR
Consumer -->|1. citizen token| A
A["your app"] -->|2. exchange token| TokenX
TokenX -->|3. new token for other app| A
A -->|4. use token| O[other app]
```

By exchanging the tokens between each application, each token is restricted to a given target application while propagating the citizen identity.

:dart: Learn how to [consume a API using TokenX](how-to/consume.md)

## Secure your API

To secure your API with TokenX, you'll first need to grant access to your consumers.

```mermaid
graph LR
Provider["Application"] -->|grant access| TokenX
```

Once configured, your consumers can [exchange a token from TokenX](#consume-an-api) that targets your application.
Your application must then validate inbound requests from the consumer.

:dart: Learn how to [secure your API using TokenX](how-to/secure.md)

138 changes: 138 additions & 0 deletions docs/auth/tokenx/how-to/consume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
tags: [tokenx, how-to]
---

# Consume API using TokenX


This how-to guides you through the steps required to consume an API secured with [TokenX](../README.md):

1. [Authenticate with TokenX](#authenticate-with-tokenx)
1. [Exchange token](#exchange-token)
1. [Consume the API using the token](#consume-api)

## Prerequisites

- outbound access policy mot api
- app du konsumerer må ha ap for deg
- du må ha konsumenter som bruker tokenx eller idporten

## Authenticate with TokenX

To perform a token exchange, your application must authenticate itself.
To do so, create a [client assertion](../../auth/explanations/README.md#client-assertion).

The client assertion is signed with your applications [private key](../../auth/explanations/README.md#private-keys) contained within [`TOKEN_X_PRIVATE_JWK`](tokenx.md#variables-for-exchanging-tokens).

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 [`TOKEN_X_CLIENT_ID`](tokenx.md#variables-for-exchanging-tokens). |
| **`iss`** | `dev-gcp:my-team:app-a` | The _issuer_ of the token. Set to [`TOKEN_X_CLIENT_ID`](tokenx.md#variables-for-exchanging-tokens). |
| **`aud`** | `https://tokenx.dev-gcp.nav.cloud.nais.io/token` | The _audience_ of the token. Set to [`TOKEN_X_TOKEN_ENDPOINT`](tokenx.md#variables-for-exchanging-tokens). |
| **`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`](tokenx.md#variables-for-exchanging-tokens). |
| **`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 "Example Client Assertion Values"

**Header**

```json
{
"kid": "93ad09a5-70bc-4858-bd26-5ff4a0c5f73f",
"typ": "JWT",
"alg": "RS256"
}
```

**Payload**

```json
{
"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"
}
```

## Exchanging a token

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

### Create exchange request

Create a POST request with the following required parameters:

| Parameter | Value | Comment |
|:------------------------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `grant_type` | `urn:ietf:params:oauth:grant-type:token-exchange` | |
| `client_assertion_type` | `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` | |
| `client_assertion` | A serialized JWT identifying the calling app | The [client assertion](#authenticate-with-tokenx) | |
| `subject_token_type` | `urn:ietf:params:oauth:token-type:jwt` | |
| `subject_token` | A serialized JWT, the token that should be exchanged | Inbound citizen token, either from ID-porten or TokenX |
| `audience` | The identifier of the app you wish to use the token for | The target application using the naming scheme `<cluster>:<namespace>:<appname>` e.g. `prod-gcp:namespace1:app1` |

Send the request to the `token_endpoint`, i.e. [`TOKEN_X_TOKEN_ENDPOINT`](tokenx.md#variables-for-exchanging-tokens).

???+ example
```http
POST /token HTTP/1.1
Host: tokenx.prod-gcp.nav.cloud.nais.io
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
```

### Exchange response

The response is a JSON object

```json title="Exchange Response"
{
"access_token" : "eyJraWQiOi..............",
"issued_token_type" : "urn:ietf:params:oauth:token-type:access_token",
"token_type" : "Bearer",
"expires_in" : 899
}
```

???+ note "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 is `key = sha256($subject_token + $audience)`.

### Exchange error response

If the exchange request is invalid, you will receive a structured error, as specified in
[RFC 8693, Section 2.2.2](https://www.rfc-editor.org/rfc/rfc8693.html#name-error-response):

```json title="Error response"
{
"error_description" : "token exchange audience <some-audience> is invalid",
"error" : "invalid_request"
}
```

128 changes: 128 additions & 0 deletions docs/auth/tokenx/how-to/secure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
tags: [tokenx, how-to]
---

# Secure your application with TokenX

### Getting Started

=== "nais.yaml"
```yaml
spec:
tokenx:
enabled: true
accessPolicy:
inbound:
rules:
- application: app-2
- application: app-3
namespace: team-a
- application: app-4
namespace: team-b
cluster: prod-gcp
```

### Access Policies

In order for other applications to acquire a token targeting your application, you **must explicitly** specify inbound access policies that authorizes these other applications.

Thus, the access policies defines _authorization_ on the application layer, and is enforced by Tokendings on token exchange operations.

For example:

```yaml
spec:
tokenx:
enabled: true
accessPolicy:
inbound:
rules:
- application: app-1
- application: app-2
namespace: team-a
- application: app-3
namespace: team-b
cluster: prod-gcp
```
The above configuration authorizes the following applications:
* application `app-1` running in the **same namespace** and **same cluster** as your application
* application `app-2` running in the namespace `team-a` in the **same cluster**
* application `app-3` running in the namespace `team-b` in the cluster `prod-gcp`

### Token Validation

If your app is a [resource server / API](../../auth/explanations/README.md#resource-server) and receives a token from another application, it is **your responsibility** to [validate the token](../../auth/explanations/README.md#token-validation) intended for your application.

Configure your app with the OAuth 2.0 Authorization Server Metadata found at the well-known endpoint, [`TOKEN_X_WELL_KNOWN_URL`](tokenx.md#variables-for-validating-tokens).
Alternatively, use the resolved values from said endpoint for convenience:

- [`TOKEN_X_ISSUER`](tokenx.md#variables-for-validating-tokens)
- [`TOKEN_X_JWKS_URI`](tokenx.md#variables-for-validating-tokens)

#### Signature Verification

* The token should be signed with the `RS256` algorithm (defined in JWT header). Tokens not matching this algorithm should be rejected.
* Verify that the signature is correct.
* The issuer's signing keys can be retrieved from the JWK Set (JWKS) at the `jwks_uri`, i.e. [`TOKEN_X_JWKS_URI`](tokenx.md#variables-for-validating-tokens).
* The `kid` attribute in the token header is thus a reference to a key contained within the JWK Set.
* The token signature should be verified against the public key in the matching JWK.

#### Claims

The following claims are by default provided in the issued token and should explicitly be validated:

* `iss` \(**issuer**\): The issuer of the token **must match exactly** with the Tokendings issuer URI ([`TOKEN_X_ISSUER`](tokenx.md#variables-for-validating-tokens)).
* `aud` \(**audience**\): The intended audience for the token, **must match** your application's `client_id` ([`TOKEN_X_CLIENT_ID`](tokenx.md#variables-for-validating-tokens)).
* `exp` \(**expiration time**\): Expiration time, i.e. tokens received after this date **must be rejected**.
* `nbf` \(**not before time**\): The token cannot be used before this time, i.e. if the token is issued in the "future" (outside "reasonable" clock skew) it **must be rejected**.
* `iat` \(**issued at time**\): The time at which the token has been issued. **Must be before `exp`**.
* `sub` \(**subject**\): If applicable, used in user centric access control. This represents a unique identifier for the user.

Other non-standard claims (with some exceptions, see the [claim mappings](#claim-mappings) section) in the token are copied verbatim from the original token issued by `idp` (the original issuer of the subject token).
For example, the claim used for the personal identifier (_personidentifikator_) for tokens issued by ID-porten is `pid`.

#### Claim Mappings

Some claims are mapped to a different value for legacy/compatibility reasons, depending on the original issuer (`idp`).

The table below shows the claim mappings:

| Claim | Original Value | Mapped Value |
|:------|:---------------------------|:--------------|
| `acr` | `idporten-loa-substantial` | `Level3` |
| `acr` | `idporten-loa-high` | `Level4` |

This currently only affects tokens from ID-porten, i.e. `idp=https://test.idporten.no` or `idp=https://idporten.no`.

The mappings will be removed at some point in the future.
If you're using the `acr` claim in any way, check for both the original and mapped values.

#### Example Token (exchanged from ID-porten)

The following example shows the claims of a token issued by Tokendings, where the exchanged subject token is issued by [ID-porten](idporten.md):

???+ example
```json
{
"at_hash": "x6lQGCdbMX62p1VHeDsFBA",
"sub": "HmjqfL7....",
"amr": [
"BankID"
],
"iss": "https://tokenx.prod-gcp.nav.cloud.nais.io",
"pid": "12345678910",
"locale": "nb",
"client_id": "prod-gcp:team-a:app-a",
"sid": "DASgLATSjYTp__ylaVbskHy66zWiplQrGDAYahvwk1k",
"aud": "prod-fss:team-b:app-b",
"acr": "Level4",
"nbf": 1597783152,
"idp": "https://idporten.no",
"auth_time": 1611926877,
"exp": 1597783452,
"iat": 1597783152,
"jti": "97f580a6-b479-426d-876b-267aa9848e2e"
}
```
71 changes: 71 additions & 0 deletions docs/auth/tokenx/reference/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
tags: [tokenx, reference]
---

# TokenX reference

## Spec

See the [NAIS manifest](../../workloads/application/reference/application-spec.md#tokenxenabled).


## Runtime Variables & Credentials

Your application will automatically be injected with environment variable.

### Variables used when acquiring tokens

These variables are used for [client authentication](tokenx.md#client-authentication) and [exchanging tokens](tokenx.md#exchanging-a-token):

| Name | Description |
|:-------------------------|:---------------------------------------------------------------------------------------------------------|
| `TOKEN_X_CLIENT_ID` | [Client ID](../../auth/explanations/README.md#client-id) that uniquely identifies the application in TokenX. |
| `TOKEN_X_PRIVATE_JWK` | [Private JWK](../../auth/explanations/README.md#private-keys) containing an RSA key belonging to client. |
| `TOKEN_X_TOKEN_ENDPOINT` | `token_endpoint` from the [metadata discovery document](../../auth/explanations/README.md#token-endpoint). |

### Variables for when validating tokens

These variables are used for [token validation](tokenx.md#token-validation):

| Name | Description |
|:-------------------------|:-----------------------------------------------------------------------------------------------------|
| `TOKEN_X_CLIENT_ID` | [Client ID](../../auth/explanations/README.md#client-id) that uniquely identifies the application in TokenX. |
| `TOKEN_X_WELL_KNOWN_URL` | The URL for Tokendings' [metadata discovery document](../../auth/explanations/README.md#well-known-url-metadata-document). |
| `TOKEN_X_ISSUER` | `issuer` from the [metadata discovery document](../../auth/explanations/README.md#issuer). |
| `TOKEN_X_JWKS_URI` | `jwks_uri` from the [metadata discovery document](../../auth/explanations/README.md#jwks-endpoint-public-keys). |

## Local Development

See also the [development overview](development.md) page.

### Token Generator

In many cases, you want to locally develop and test against a secured API in the development environments.
To do so, you need a [token](../../auth/explanations/README.md#bearer-token) to access said API.

Use <https://tokenx-token-generator.intern.dev.nav.no> to generate tokens in the development environments.

#### Prerequisites

1. The API application must be configured with [TokenX enabled](#configuration).
2. Pre-authorize the token generator service by adding it to the API application's [access policy](#access-policies):
```yaml
spec:
accessPolicy:
inbound:
rules:
- application: tokenx-token-generator
namespace: aura
cluster: dev-gcp
```
#### Getting a token
1. Visit <https://tokenx-token-generator.intern.dev.nav.no/api/obo?aud=&lt;audience&gt;> in your browser.
- Replace `<audience>` with the intended _audience_ of the token, in this case the API application.
- The audience value must be on the form of `<cluster>:<namespace>:<application>`
- For example: `dev-gcp:aura:my-app`
2. You will be redirected to log in at ID-porten (if not already logged in).
3. After logging in, you should be redirected back to the token generator and presented with a JSON response containing an `access_token`.
4. Use the `access_token` as a [Bearer token](../../auth/explanations/README.md#bearer-token) for calls to your API application.
5. Success!
Loading

0 comments on commit a08560a

Please sign in to comment.