From 8e3ebba18816a5a819c40737b231f1c40e33c736 Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 7 Jul 2026 17:54:03 +0200 Subject: [PATCH] Document 0.11.0: device grant, predefined providers, tenant context, roles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - oauth/granttypes.md: Device Code grant (RFC 8628) — flow, request/poll tables, curl - oauth/endpoints.md: /oauth/device_authorization and /activate endpoints; note that /authorize does not require client_secret (secret is authenticated at /token) - oauth/jwt_decoding.md: optional roles array claim - providers/providers.md: predefined providers (uitrusting/v1) object syntax - providers/userloginprovider.md + uservalidationprovider.md: tenant context (credentials.tenant {name, namespace}) and optional roles getter - configuration/tenant_client_config.md: device_code grant, device_grant_config, predefined provider entries --- content/configuration/tenant_client_config.md | 58 +++++++++- content/oauth/endpoints.md | 65 +++++++++++ content/oauth/granttypes.md | 103 ++++++++++++++++++ content/oauth/jwt_decoding.md | 40 +++++++ content/providers/providers.md | 50 +++++++++ content/providers/userloginprovider.md | 55 +++++++++- content/providers/uservalidationprovider.md | 42 ++++++- 7 files changed, 404 insertions(+), 9 deletions(-) diff --git a/content/configuration/tenant_client_config.md b/content/configuration/tenant_client_config.md index ec7b18a..089ac83 100644 --- a/content/configuration/tenant_client_config.md +++ b/content/configuration/tenant_client_config.md @@ -196,6 +196,33 @@ spec: | silent_login | no | `true` | `false` | When this option is enabled and a client has a valid auth cookie shared with the login page, its login information will be used to authenticate the user without asking for a username or password. | | jwt_algorithm | no | `HS256` | `RS256` or `HS256` | JWT signing algorithm for this tenant. Defaults to `HS256` if not specified. Use `RS256` for production (asymmetric, better security, supports key rotation every 90 days), or `HS256` for development/legacy systems (symmetric, requires JWT_SECRET). See [JWT Algorithms](/configuration/jwt_algorithms) for detailed comparison. | +### Predefined Providers + +Each entry in the `providers` list can be either a raw JavaScript script (as in the examples above) or a +**predefined-provider object** that references a ready-made provider by type. This lets you reuse a shared provider +implementation without pasting the full script into every tenant. + +```yaml +spec: + hosts: + - bnbc.example + providers: + # A raw JavaScript provider ... + - "class UserValidationProvider { [...] }" + # ... and a predefined provider referenced by type: + - type: uitrusting/v1 + url: users.srv.cluster.local + token: shared-provider-token # optional +``` + +| Property | Mandatory | Default | Example | Discussion | +|----------|-----------|---------|----------------------------|-----------------------------------------------------------------------| +| type | yes | - | `uitrusting/v1` | The identifier of the predefined provider to load. | +| url | yes | - | `users.srv.cluster.local` | Host of the backend the predefined provider connects to. | +| token | no | - | `shared-provider-token` | Optional authentication token passed to the predefined provider. | + +See [Providers](/providers/providers) for the full list of predefined providers and how they work. + ### JWT Algorithm Configuration Each tenant can use its own JWT signing algorithm, independent of other tenants in the same instance. This allows gradual migration from HS256 to RS256, or mixing tenants with different security requirements. @@ -376,7 +403,8 @@ spec: | name | yes | - | `bnbc-ios-app` | Give the client a unique and specific name. Clients should reflect the device classes that you need to target with specific rights and to get individual statistics from. | | tenantname | yes | - | `bnbc-tenant` | The name of the tenant for which this client is for. On kubernetes this must contain the tenants namespace: `[tennant namespace]/bnbc-tenant` | | redirect_urls | yes | - | `["https://www.bnbc.(example|example.com)/bnbc-club/*"]` | A client sends a redirect url to which the response will be redirected to. Specify the allowed urls for security reasons, otherwise it will be possible to hijack the token in the response. See information below. | -| grant_types | no | ["authorization_code", "refresh_token"] | `["password"]` | A list of allowed grant types. If not set, a default set will be applied: `authorization_code`, `refresh_token`. If you need to support the “password" grant, you must specify it explicitly! | +| grant_types | no | ["authorization_code", "refresh_token"] | `["password"]` | A list of allowed grant types. If not set, a default set will be applied: `authorization_code`, `refresh_token`. If you need to support the “password" grant, you must specify it explicitly! Add `device_code` to enable the OAuth 2.0 Device Authorization Grant (RFC 8628). | +| device_grant_config | no | - | _see below_ | Optional fine-tuning for the `device_code` grant. Only used when `device_code` is listed in `grant_types`. All keys are optional and override the built-in defaults. See below. | | scopes | no | [] | `["recipes:read", "recipes:write", "timeline:post"]` | A list of allowed scopes for this client. If a client requests scopes, these will be filtered by the ones that are allowed. This controls scopes requested by the OAuth client during authorization. | | allowedProviderScopes | no | [] | `["user:*", "can:*", "org:read"]` | A list of allowed scopes that JavaScript providers can add to user profiles. Supports wildcard patterns (e.g., `user:*` matches `user:list`, `user:add`). Provider-supplied scopes are filtered against this list before being merged with client-requested scopes. Defaults to empty (no provider scopes allowed), providing secure-by-default behavior. | | referrers | no | [] | `[https://www.bnbc.example/bnbc-club/login]` | If set, only clients that come from these referers are allowed. | @@ -388,6 +416,7 @@ spec: - authorization_code - refresh_token - password +- device_code If you allow a `authorization_code`, you should also allow `refresh_token`, because to refresh a token you need to get one via the `authorization_code` request. @@ -396,6 +425,33 @@ The response from a `password` request does not return a refresh token! Try to avoid the `password` grant in production! It is insecure and should be replaced by a pkce code request. Only if you have to support older clients you may need to turn this option on. +Add `device_code` for input-constrained devices (smart TVs, CLIs, IoT) that cannot present a browser. This enables the +OAuth 2.0 Device Authorization Grant (RFC 8628), where the device shows a short `user_code` that the user enters on a +second device to authorize the login. + +**Device Grant Configuration** + +The Device Authorization Grant is enabled purely by listing `device_code` in `grant_types`. The optional +`device_grant_config` block only overrides the built-in defaults - when it is omitted entirely, the defaults apply. + +```yaml +spec: + grant_types: + - authorization_code + - refresh_token + - device_code + device_grant_config: + expires_in: 1800 # optional, lifetime in seconds of the device_code/user_code (default 1800) + interval: 5 # optional, minimum polling interval in seconds (default 5) + # verification_uri: # optional, override the URL shown to the user; auto-detected as https:///activate when omitted +``` + +| Property | Mandatory | Default | Example | Discussion | +|------------------|-----------|---------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------| +| expires_in | no | `1800` | `1800` | Lifetime in seconds of the issued `device_code` and `user_code`. After this window the user has to start the flow again. | +| interval | no | `5` | `5` | Minimum polling interval in seconds the device must wait between `token` requests while the user completes the login. | +| verification_uri | no | `https:///activate` | `https://login.bnbc.example/activate` | Override the URL shown to the user to enter the `user_code`. Auto-detected as `https:///activate` when omitted. | + **Redirect Urls** If the requested `redirect_url` of an `AuthRequest` does not match any of these url patterns, the whole authorization request will be denied. diff --git a/content/oauth/endpoints.md b/content/oauth/endpoints.md index f910c41..f54ca7f 100644 --- a/content/oauth/endpoints.md +++ b/content/oauth/endpoints.md @@ -46,6 +46,13 @@ You can find an example setup in our [quick start guide](/general/quickstart#cre > > Use [PKCE](/oauth/pkce) to request an authorization code. +> **Note about `client_secret`**: +> +> The authorization endpoint (`/authorize`) does not require or validate the `client_secret`. Per RFC 6749 +> §4.1.1/§3.2.1 the client is only identified here by its `client_id`. Confidential clients authenticate with their +> secret on the back-channel **token** request (`/token`) instead. This prevents the secret from leaking through the +> browser front-channel. + **Example**: Request an authorization code with PKCE SHA265 code ```text @@ -171,6 +178,53 @@ used in certain grant types. The refresh_token is used to obtain a new access to without having to prompt the user for their login credentials again. That is strictly forbidden with the password grant type. +### /oauth/device_authorization + +The `/oauth/device_authorization` endpoint is the device authorization request endpoint of the +[OAuth 2.0 Device Authorization Grant (RFC 8628)](https://datatracker.ietf.org/doc/html/rfc8628). It is used by +input-constrained devices (command line tools, smart TVs, IoT devices) that authenticate the user on a secondary device +with a browser. + +The device POSTs its `client_id` (and optionally a `scope`) to this endpoint and obtains a `device_code` together with a +`user_code`. The `device_code` stays on the device and is used to poll the [/token](#token) endpoint, while the +`user_code` is shown to the user, who enters it in a browser at the [/activate](#activate) verification page. + +**Example**: Request a device and user code + +```shell +curl --request POST \ + --url https://id.example.com/oauth/device_authorization \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data 'client_id=9095A4F2-35B2-48B1-A325-309CA324B97E' \ + --data 'scope=openid' +``` + +The authorization server responds with the device authorization response: + +```json +{ + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "WDJB-MJHT", + "verification_uri": "https://id.example.com/activate", + "expires_in": 1800, + "interval": 5 +} +``` + +The device then polls the [/token](#token) endpoint using the `device_code` and the +`urn:ietf:params:oauth:grant-type:device_code` grant type. See the +[device code grant type](/oauth/granttypes#device-code) for the full flow and polling responses. + +### /activate + +The `/activate` endpoint is the user-facing verification page of the device authorization grant. The user opens this URL +(the `verification_uri` returned by [/oauth/device_authorization](#oauthdevice_authorization)) in a browser, enters the +`user_code` shown on the device and authenticates. After a successful login the user approves the pending device request, +which allows the device to obtain its tokens from the [/token](#token) endpoint. + +`GET /activate` renders the verification form (optionally pre-filled with the `user_code` when it is passed as a query +parameter), and `POST /activate` submits the entered `user_code` to approve the device. + ### /revoke The `/revoke` endpoint allows clients to notify Uitsmijter that a previously obtained token (access token or refresh token) is no longer needed and should be invalidated. This endpoint implements [RFC 7009: OAuth 2.0 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009). @@ -309,6 +363,17 @@ This returns a JSON document with the OpenID Provider Metadata: } ``` +When a client of the tenant supports the `device_code` grant, the discovery document additionally advertises a +`device_authorization_endpoint` pointing at [/oauth/device_authorization](#oauthdevice_authorization) and lists +`urn:ietf:params:oauth:grant-type:device_code` in `grant_types_supported`: + +```json +{ + "device_authorization_endpoint": "https://id.example.com/oauth/device_authorization", + "grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"] +} +``` + **Multi-tenant discovery** Each tenant in Uitsmijter has its own discovery endpoint with tenant-specific configuration: diff --git a/content/oauth/granttypes.md b/content/oauth/granttypes.md index c278546..5b1f739 100644 --- a/content/oauth/granttypes.md +++ b/content/oauth/granttypes.md @@ -17,6 +17,7 @@ Grant types can be set at `Client` level. - authorization_code - refresh_token - password + - device_code ``` If none of any grant type is specified, that `authorization_code` and `refresh_token` are enabled by default. @@ -229,6 +230,108 @@ curl -v \ "https://api.example.com/resource" ``` +## Device Code + +The device code grant type implements the +[OAuth 2.0 Device Authorization Grant (RFC 8628)](https://datatracker.ietf.org/doc/html/rfc8628). It is designed for +input-constrained devices that either lack a browser or are difficult to type on, such as command line tools, smart TVs, +media consoles and other IoT devices. Instead of typing their credentials on the device, the user authenticates on a +secondary device (typically a phone or laptop) that has a full browser. + +The flow consists of three steps: + +1. **Device authorization request**: The device POSTs to `/oauth/device_authorization` and receives a `device_code` + (kept on the device), a `user_code` (shown to the user) and a `verification_uri` that the user should open. +2. **User authorization**: The user opens the `verification_uri` in a browser on another device, enters the `user_code` + and authenticates at `/activate`. After a successful login the user approves the pending device request. +3. **Token polling**: While the user authorizes on the secondary device, the device polls `POST /token` with its + `device_code`. Once the user has approved the request, the token endpoint returns an access token (and a refresh + token). + +Clients explicitly have to turn on the `device_code` grant type to support it by listing `device_code` in their +`grant_types`. The behaviour of the flow (for example the polling `interval` or the code lifetime) can optionally be +tuned per client via `device_grant_config`. Read more on the +[tenant and client config](/configuration/tenant_client_config) page. + +The following values must be set in the request for an access token: + +| Property | Value | Description | +|---------------|----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| grant_type | urn:ietf:params:oauth:grant-type:device_code | This tells the server we’re using the device code grant type. The bare `device_code` is also accepted as an alias. | +| client_id | _UUID of the client_ | The public identifier of the application that the developer obtained during registration | +| client_secret | (optional) | Must be set if the client request an secret. Reed more on [tenant and client config](/configuration/tenant_client_config) page | +| device_code | _device code_ | The `device_code` that was returned by the device authorization response. | + +### Polling responses + +While the user has not yet approved the request, the device keeps polling the `/token` endpoint. The token endpoint +responds with one of the following: + +| Status | Body | Meaning | +|--------|-----------------------------------------|-------------------------------------------------------------------------------| +| `200` | access token (and refresh token) | The user approved the request. Stop polling and use the token. | +| `400` | `{"error":"authorization_pending"}` | The user has not yet approved the request. Keep polling at `interval`. | +| `400` | `{"error":"slow_down"}` | The device polls too fast. Increase the polling `interval` (by 5 seconds). | +| `400` | `{"error":"access_denied"}` | The user denied the request. Stop polling. | +| `400` | `{"error":"invalid_grant"}` | The `device_code` is unknown or expired. Stop polling and start a new flow. | + +These errors follow the standard RFC 6749/8628 error shape, so standard OAuth2 client libraries interoperate with the +device flow without any Uitsmijter-specific handling. + +### Example + +First, the device requests a `device_code` and a `user_code` from the device authorization endpoint: + +```shell +curl -v \ + -X POST \ + -d 'client_id=D742D5BF-0402-4C04-9FF8-94C1D2DA5BE2&scope=openid' \ + "https://login.example.com/oauth/device_authorization" +``` + +The server replies with the device authorization response: + +```json +{ + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "WDJB-MJHT", + "verification_uri": "https://login.example.com/activate", + "expires_in": 1800, + "interval": 5 +} +``` + +The device now shows the `user_code` and the `verification_uri` to the user and starts polling the token endpoint with +the `device_code`: + +```shell +curl -v \ + -X POST \ + -d 'grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=D742D5BF-0402-4C04-9FF8-94C1D2DA5BE2&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS' \ + "https://login.example.com/token" +``` + +As long as the user has not approved the request, the server replies with `authorization_pending`: + +```json +{ + "error": "authorization_pending" +} +``` + +Once the user has entered the `user_code` at the `verification_uri` and approved the request, the server replies with an +access token and a refresh token: + +```json +{ + "access_token": "aoth5bie8eiy2iPhaeghai6aijahvaeshungae8phieva6tiebeequ6tushei3ei", + "refresh_token": "DOO5AHD6SAi9PA1OOKIAZoOSHOHgO1TO", + "token_type": "bearer", + "expires_in": 7200, + "scope": "openid" +} +``` + ## Further readings - Available [Endpoints](/oauth/endpoints) diff --git a/content/oauth/jwt_decoding.md b/content/oauth/jwt_decoding.md index 7116dc2..bd7d87c 100644 --- a/content/oauth/jwt_decoding.md +++ b/content/oauth/jwt_decoding.md @@ -40,6 +40,46 @@ claims using the dot notation, like this: console.log(decoded.name); // "John Doe" ``` +## Token Claims + +A decoded token contains the standard claims (`iss`, `sub`, `aud`, `exp`, `iat`) alongside Uitsmijter specific claims +such as `tenant`, `profile`, `scope` and `role`: + +```json +{ + "iss": "https://auth.example.com", + "sub": "user@example.com", + "aud": "550e8400-e29b-41d4-a716-446655440000", + "exp": 1736649600, + "iat": 1736563200, + "tenant": "example-tenant", + "role": "admin", + "roles": [ + "admin", + "editor", + "user" + ], + "scope": [ + "openid", + "email", + "profile" + ], + "profile": { + "email": "user@example.com", + "name": "John Doe" + } +} +``` + +| Claim | Type | Discussion | +|---------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| role | string | The primary role of the user, as returned by the provider's `role` getter. | +| roles | array of string | Optional. Present when the provider exposes multiple roles. Contains all roles of the user; the single `role` claim mirrors the primary (first) entry of this list. | +| scope | array of string | The final list of scopes granted to the token after filtering and merging. See [Scopes](/oauth/scopes). | + +The `roles` claim is **omitted for single-role providers**, so tokens issued by existing providers remain unchanged. +When it is present, the single `role` claim always mirrors the primary (first) role for backward compatibility. + ## Further readings - [Authorization Code Flow with Proof Key for Code Exchange](/oauth/pkce) diff --git a/content/providers/providers.md b/content/providers/providers.md index 5d7b908..22eb66b 100644 --- a/content/providers/providers.md +++ b/content/providers/providers.md @@ -124,6 +124,56 @@ Provider execution time is limited. The advanced setting `SCRIPT_TIMEOUT` can mo The default timeout is **30 seconds**, which is recommended unless you need a shorter timeout. The provider must complete all tasks within this time limit, including performing all necessary requests and returning the result. +## Predefined Providers + +A tenant's `providers` list entry can be **either** a raw JavaScript script string (as shown above) **or** a +predefined-provider object that Uitsmijter expands into a generated provider internally. Both styles can be mixed in the +same list: + +```yaml +providers: + - | + class UserLoginProvider { /* ... raw script ... */ } + - type: uitrusting/v1 + url: api.uitrusting.svc + token: "" +``` + +A predefined-provider object supports the following keys: + +| Key | Description | +|---------|---------------------------------------------------------------------------------------------------------------------------------| +| `type` | The predefined provider type. Currently the only supported value is `uitrusting/v1`. | +| `url` | Base URL/host of the external service. If no scheme is given, `http://` is added; the path `/verify` is appended automatically. | +| `token` | **Optional** shared secret sent as the `X-Internal-Token` header. Omit it for open/unauthenticated mode. | + +### uitrusting/v1 + +For `type: uitrusting/v1`, Uitsmijter generates **both** a `UserLoginProvider` and a `UserValidationProvider` that call +`POST {url}/verify`: + +- **Login** sends JSON `{ tenant, namespace, username, password_hash }`, where `password_hash` is the **SHA256 hex** of + the password. The plain password is never transmitted. +- **Refresh re-validation** sends `{ tenant, namespace, username }` (no hash). + +The service always answers with HTTP status `200` and the following JSON body: + +```json +{ + "known": true, + "valid": true, + "subject": "1734034", + "roles": ["admin", "editor"], + "scopes": ["user:read"], + "profile": { "name": "Lorene Ibsen" } +} +``` + +The `valid` field decides success — the decision is made on `valid`, **not** on the HTTP status code. The `roles`, +`scopes` and `profile` fields are only populated when `valid` is `true`. + +Predefined providers are additive: raw-string providers are unchanged. + ## Further readings - [User Login Provider](/providers/userloginprovider) diff --git a/content/providers/userloginprovider.md b/content/providers/userloginprovider.md index 50ed7d2..f05ef99 100644 --- a/content/providers/userloginprovider.md +++ b/content/providers/userloginprovider.md @@ -24,9 +24,9 @@ anything that accepts a http request and sends a proper response with valid stat ## Parameters -| Parameter | Description | -|---------------------------|-------------------------------------------------------------------------------------------| -| constructor(:credentials) | A Object with two properties: `username` and `password` is passed into the init function. | +| Parameter | Description | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| constructor(:credentials) | A Object with the properties `username`, `password` and `tenant` is passed into the init function. `tenant` carries `name` and `namespace`. | ## Methods @@ -34,10 +34,11 @@ Those methods/getters must be implemented: | Method | Description | |---------------------------|------------------------------------------------------------------------------------------------------------| -| constructor(:credentials) | Initialisation method that gets the `username` and the `password` for the user in question. | +| constructor(:credentials) | Initialisation method that gets the `username`, the `password` and the `tenant` (`name`, `namespace`) for the user in question. | | canLogin | Getter that should indicate if the current user in context can be logged in (has valid credentials) or not | | userProfile | Getter that should return the users profile. | | role | Getter that should return the users role. | +| roles | **Optional** getter that returns an array of role strings. When present, the JWT gets an optional `roles` array claim, and the single `role` claim mirrors the primary (first) role. Omitted for single-role providers. | | scopes | **Optional** getter that returns an array of scopes to add to the user's JWT token based on user context (roles, groups, permissions). | After the constructor called `commit(:obj)` the two getters `canLogin` and `userProfile` must have the correct values. @@ -173,6 +174,39 @@ commit({message: "A good login"}, {"subject": "lorene.ibsen@example.com"}, {erro commit(response.status, {"subject": "lorene.ibsen@example.com"}, {error: false}) ``` +## Tenant Context + +In addition to `username` and `password`, the `credentials` object passed to the constructor carries the `tenant` the +login is performed for. This lets a provider scope its request to the correct tenant, which is useful for multi-tenant +user services that share a single provider script. + +The `tenant` object has the following shape: + +``` +tenant: { name: "", namespace: "" } +``` + +Read it via `credentials.tenant.name` and `credentials.tenant.namespace`: + +```javascript +constructor(credentials) { + fetch(`http://users.example.com/validate-login`, { + method: "post", + body: { + tenant: credentials.tenant.name, + namespace: credentials.tenant.namespace, + username: credentials.username, + passwordHash: sha256(credentials.password) + } + }).then((result) => { + // ... + commit(result.code); + }); +} +``` + +This is additive and backward compatible — existing scripts that ignore `tenant` keep working. + ## Dynamic Scope Assignment The `scopes` getter is an **optional** method that allows JavaScript providers to dynamically assign OAuth2 scopes to users based on their authentication context, such as roles, group memberships, permissions, or any other user attributes. @@ -278,6 +312,19 @@ Dynamic scope assignment is useful for: For more information, see [Client Configuration](/configuration/tenant_client_config) and [Managing Clients](/working-with-uitsmijter/clients). +## Roles + +Besides the required single `role` getter, a provider may expose an **optional `roles` getter** that returns an array of +role strings. When present, the JWT gets an optional [`roles`](/oauth/jwt_decoding) array claim, while the single `role` +claim mirrors the primary (first) role. For single-role providers the getter can be omitted and no `roles` claim is +added. + +```javascript +get roles() { + return ["admin", "editor"]; +} +``` + ## Examples **Simple Example** diff --git a/content/providers/uservalidationprovider.md b/content/providers/uservalidationprovider.md index 36591d1..9430186 100644 --- a/content/providers/uservalidationprovider.md +++ b/content/providers/uservalidationprovider.md @@ -26,9 +26,9 @@ be anything that accepts a http request and sends a proper response with valid s ## Parameters -| Parameter | Description | -|--------------------|--------------------------------------------------------------------------| -| constructor(:args) | A Object with one property: `username` is passed into the init function. | +| Parameter | Description | +|--------------------|------------------------------------------------------------------------------------------------------------------------| +| constructor(:args) | A Object with the properties `username` and `tenant` is passed into the init function. `tenant` carries `name` and `namespace`. | ## Methods @@ -36,7 +36,7 @@ Those methods/getters must be implemented: | Method | Description | |--------------------|----------------------------------------------------------------------------------| -| constructor(:args) | Initialisation method that gets the `username` for the user in question. | +| constructor(:args) | Initialisation method that gets the `username` and the `tenant` (`name`, `namespace`) for the user in question. | | isValid | Getter that should indicate if the current user in context can sill be logged in | After the constructor called `commit(:args)` the getter `isValid` must have the correct values. @@ -82,3 +82,37 @@ class UserValidationProvider { } } ``` + +## Tenant Context + +In addition to `username`, the constructor argument carries the `tenant` the validation is performed for. This lets a +provider scope its request to the correct tenant, which is useful for multi-tenant user services that share a single +provider script. + +The `tenant` object has the following shape: + +``` +tenant: { name: "", namespace: "" } +``` + +Read it via `args.tenant.name` and `args.tenant.namespace`: + +```javascript +constructor(args) { + fetch(`http://users.example.com/validate-user`, { + method: "post", + body: { + tenant: args.tenant.name, + namespace: args.tenant.namespace, + username: args.username + } + }).then((result) => { + if (result.code == 200) { + this.isValid = true; + } + commit(this.isValid); + }); +} +``` + +This is additive and backward compatible — existing scripts that ignore `tenant` keep working.