| <!-- | |
| ~ Licensed to the Apache Software Foundation (ASF) under one | |
| ~ or more contributor license agreements. See the NOTICE file | |
| ~ distributed with this work for additional information | |
| ~ regarding copyright ownership. The ASF licenses this file | |
| ~ to you under the Apache License, Version 2.0 (the | |
| ~ "License"); you may not use this file except in compliance | |
| ~ with the License. You may obtain a copy of the License at | |
| ~ | |
| ~ http://www.apache.org/licenses/LICENSE-2.0 | |
| ~ | |
| ~ Unless required by applicable law or agreed to in writing, | |
| ~ software distributed under the License is distributed on an | |
| ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| ~ KIND, either express or implied. See the License for the | |
| ~ specific language governing permissions and limitations | |
| ~ under the License. | |
| ~ | |
| --> | |
| # OIDC Integration | |
| OIDC integration lets users authenticate against an external identity provider (Keycloak by | |
| default) instead of, or in addition to, the built-in CouchDB-backed login. | |
| Admin-api validates the OIDC access token, maps the token claims to an OpenServerless | |
| namespace and returns the flat `AUTH`/`NAMESPACE` payload consumed by SSO clients | |
| such as `ops`. The existing `/system/api/v1/auth` password endpoint retains its | |
| metadata response and authentication behavior. | |
| On `0.9.0`, auto-provisioning creates `WhiskUser` resources with API version | |
| `openserverless.org/v1` in namespace `openserverless`, using the existing | |
| Kubernetes client and operator. Configuration tasks must target | |
| `openserverless-system-api` in that namespace. SSO requires an external IdP and | |
| the matching `ops config sso` tasks; the CLI's SSO code is already in `0.9.0`. | |
| Run `task utest-sso` for the OIDC and legacy authentication unit tests. These | |
| tests use fake HTTP, CouchDB and Kubernetes clients and do not deploy resources. | |
| Three login styles are supported: | |
| | style | description | | |
| |:------------------|:----------------------------------------------------------------------------------------------| | |
| | direct token | The caller already holds an OIDC access token (e.g. from a browser/PKCE flow) and posts it directly. | | |
| | device flow | admin-api drives an OAuth2 Device Authorization flow on behalf of a CLI/headless client; the OIDC tokens never reach the client. | | |
| | password grant | admin-api exchanges a username/password directly with the OIDC provider (Resource Owner Password Credentials grant); useful for trusted CLI/automation flows. | | |
| In all three cases, the access token is verified locally (RS256 signature against the | |
| provider's JWKS, issuer, audience, expiry and optional group membership) before any namespace | |
| mapping happens — see [openserverless/common/oidc_validator.py](../openserverless/common/oidc_validator.py). | |
| ## Namespace mapping | |
| The `preferred_username` claim (or whatever `OIDC_USERNAME_CLAIM` points to) identifies the | |
| external identity. It is turned into an OpenServerless namespace by | |
| [openserverless/common/sso_namespace.py](../openserverless/common/sso_namespace.py) using this | |
| precedence: | |
| 1. If `OIDC_NAMESPACE_CLAIM` is set and the claim value is a valid namespace, use it as-is. | |
| 2. If `SSO_NAMESPACE_PRESERVE_VALID` is enabled (default) and the external username is already | |
| a valid namespace, use it as-is. | |
| 3. Otherwise, normalize the external username (lowercase, strip invalid characters, pad if too | |
| short) and append a stable hash suffix derived from `iss` + `sub` + username, so the mapping | |
| collision-resistant and repeatable across logins. | |
| If a namespace cannot be provisioned automatically, an existing `WhiskUser` can be bound to an | |
| SSO identity manually by an admin, or auto-provisioning can be enabled (see below). | |
| Namespace mapping does not grant ownership. Before returning credentials, admin-api requires | |
| the `WhiskUser` annotations `openserverless.apache.org/sso-issuer` and | |
| `openserverless.apache.org/sso-sub` to match the validated token's nonempty `iss` and `sub` | |
| exactly. An unbound legacy namespace, a different subject, or a disabled binding receives | |
| `403`, even when the external username matches the namespace. Password login is unaffected. | |
| To bind an existing namespace, a cluster administrator must verify its owner and obtain the | |
| stable subject identifier from the trusted identity provider, then explicitly set: | |
| ```sh | |
| kubectl -n openserverless annotate whisksusers/<namespace> \ | |
| openserverless.apache.org/sso-issuer='<exact configured issuer>' \ | |
| openserverless.apache.org/sso-sub='<verified owner subject>' --overwrite | |
| ``` | |
| This operation must not be delegated to an unauthenticated caller or derived from a matching | |
| username alone. If the binding was deliberately disabled, re-enabling it requires a separate | |
| administrator decision to remove or change `openserverless.apache.org/sso-disabled`. | |
| Auto-provisioning records the binding on a new namespace and never takes over an unbound | |
| existing namespace or one belonging to another subject. Binding is checked again after | |
| provisioning. Orphan metadata and a `WhiskUser` replaced during the login are rejected. | |
| ## Auto-provisioning | |
| By default, an OIDC login only succeeds if a `WhiskUser` for the resolved namespace already | |
| exists. Setting `SSO_AUTOPROVISION_ON_LOGIN=true` allows admin-api to create the `WhiskUser` | |
| custom resource on first login (requires an `email` claim), then poll CouchDB until the | |
| namespace metadata becomes available. | |
| A namespace can be locked out of SSO login (e.g. to force a manual password reset) by setting | |
| the `openserverless.apache.org/sso-disabled` annotation to `true` on its `WhiskUser`. | |
| ## Configuration | |
| All configuration is read from environment variables of the admin-api pod. | |
| | variable | required | default | description | | |
| |:-------------------------------------|:---------|:------------------------|:------------| | |
| | `OIDC_ISSUER_URL` | yes | — | Expected `iss` claim / base URL of the realm, also used to derive the device-authorization and token endpoints. | | |
| | `OIDC_JWKS_URL` | yes | — | JWKS endpoint used to fetch the provider's signing keys. | | |
| | `OIDC_AUDIENCE` | yes | — | Expected `aud` claim of the access token. | | |
| | `OIDC_CLIENT_ID` | no | value of `OIDC_AUDIENCE` | Client id used for the device and password grant flows. | | |
| | `OIDC_CLIENT_SECRET` | no | — | Client secret, if the OIDC client is confidential. | | |
| | `OIDC_USERNAME_CLAIM` | no | `preferred_username` | Claim used as the external username. | | |
| | `OIDC_NAMESPACE_CLAIM` | no | — | Claim to use directly as the namespace, when present and valid. | | |
| | `OIDC_GROUPS_CLAIM` | no | `groups` | Claim holding the user's group memberships. | | |
| | `OIDC_REQUIRED_GROUP` | no | — | If set, tokens without this group in `OIDC_GROUPS_CLAIM` are rejected with `403`. | | |
| | `OIDC_CLOCK_LEEWAY_SECONDS` | no | `30` | Clock skew tolerance applied to `exp`/`nbf` validation. | | |
| | `OIDC_PROVIDER` | no | `keycloak` | Recorded on auto-provisioned `WhiskUser`s for traceability. | | |
| | `OIDC_DEVICE_AUTHORIZATION_URL` | no | `${OIDC_ISSUER_URL}/protocol/openid-connect/auth/device` | Device Authorization endpoint. | | |
| | `OIDC_TOKEN_URL` | no | `${OIDC_ISSUER_URL}/protocol/openid-connect/token` | Token endpoint used by the device and password flows. | | |
| | `OIDC_DEVICE_SCOPE` | no | `openid email profile` | Scope requested when starting a device flow. | | |
| | `OIDC_PASSWORD_SCOPE` | no | `openid email profile` | Scope requested for the password grant. | | |
| | `SSO_NAMESPACE_PRESERVE_VALID` | no | `true` | Keep the external username as namespace when it is already a valid namespace. | | |
| | `SSO_NAMESPACE_HASH_LENGTH` | no | `8` (clamped 6-16) | Length of the collision-avoidance hash suffix appended to normalized namespaces. | | |
| | `SSO_NAMESPACE_MAX_LENGTH` | no | `61` (clamped 13-61) | Maximum length of a generated namespace. | | |
| | `SSO_AUTOPROVISION_ON_LOGIN` | no | `false` | Create a `WhiskUser` automatically on first successful OIDC login. | | |
| | `SSO_AUTOPROVISION_DEFAULT_SERVICES` | no | `all` | When set to `all`, enables redis/mongodb/postgres/object-storage/milvus on the provisioned `WhiskUser`. | | |
| | `SSO_AUTOPROVISION_STORAGE_QUOTA` | no | `auto` | Object-storage quota assigned to auto-provisioned users. | | |
| | `SSO_AUTOPROVISION_TIMEOUT_SECONDS` | no | `120` | How long admin-api waits for the provisioned namespace metadata to appear. | | |
| | `SSO_AUTOPROVISION_POLL_SECONDS` | no | `2` | Poll interval while waiting for provisioning to complete. | | |
| *NOTE*: `OIDC_ISSUER_URL`, `OIDC_JWKS_URL` and `OIDC_AUDIENCE` are always required; the device | |
| and password grant flows additionally rely on `OIDC_ISSUER_URL` to derive their endpoints | |
| unless `OIDC_DEVICE_AUTHORIZATION_URL` / `OIDC_TOKEN_URL` are overridden explicitly. | |
| A namespace can also opt out of SSO entirely by setting the | |
| `openserverless.apache.org/sso-disabled: "true"` annotation on its `WhiskUser`. | |
| ## Endpoints | |
| `POST /system/api/v1/auth/oidc` - Authenticate with an OIDC access token, passed either as a | |
| `Bearer` `Authorization` header or as `access_token` in the JSON body. | |
| `POST /system/api/v1/auth/oidc/device/start` - Start a backend-managed OAuth2 Device | |
| Authorization flow. Returns an opaque `flow_id` plus the `user_code`/`verification_uri` to show | |
| to the user; no OIDC token is exposed to the caller. | |
| `POST /system/api/v1/auth/oidc/device/poll` - Poll a device flow started above, using the | |
| `flow_id`. Returns `202` while the user hasn't completed the login at the identity provider yet, | |
| and the OpenServerless login payload once it succeeds. | |
| `POST /system/api/v1/auth/oidc/password` - Authenticate with a username/password using the OIDC | |
| Resource Owner Password Credentials grant. The password and any OIDC tokens are never returned | |
| to the caller. | |
| Unlike `/system/api/v1/auth`, none of the OIDC endpoints require a pre-existing `wsk` token: | |
| the OIDC token (or credentials) themselves are the proof of identity. | |
| ## Examples | |
| ### Direct token login | |
| ```json | |
| POST /system/api/v1/auth/oidc | |
| Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9... | |
| ``` | |
| ### Device flow | |
| ```json | |
| POST /system/api/v1/auth/oidc/device/start | |
| { | |
| "namespace": "myuser" | |
| } | |
| ``` | |
| ```json | |
| 200 OK | |
| { | |
| "flow_id": "u9F1...opaque...", | |
| "user_code": "ABCD-EFGH", | |
| "verification_uri": "https://keycloak.example.test/device", | |
| "verification_uri_complete": "https://keycloak.example.test/device?user_code=ABCD-EFGH", | |
| "expires_in": 600, | |
| "interval": 5 | |
| } | |
| ``` | |
| Show `verification_uri_complete` (or `user_code` + `verification_uri`) to the user, then poll: | |
| ```json | |
| POST /system/api/v1/auth/oidc/device/poll | |
| { | |
| "flow_id": "u9F1...opaque..." | |
| } | |
| ``` | |
| ### Password grant login | |
| ```json | |
| POST /system/api/v1/auth/oidc/password | |
| { | |
| "username": "myuser", | |
| "password": "secret", | |
| "namespace": "myuser" | |
| } | |
| ``` | |
| The optional `namespace` field in the device/password flows lets the caller assert which | |
| namespace it expects to log into; if the token resolves to a different namespace, admin-api | |
| returns `403` rather than logging into the wrong workspace. | |
| # Useful Links | |
| - https://www.keycloak.org/docs/latest/securing_apps/#_device_authorization_grant | |
| - https://datatracker.ietf.org/doc/html/rfc8628 | |
| - https://openid.net/specs/openid-connect-core-1_0.html |