From cdbd5cd146047f4be1bec42445fac1e3eb066fc8 Mon Sep 17 00:00:00 2001 From: Kris Simon Date: Sat, 25 Oct 2025 22:19:36 +0200 Subject: [PATCH 1/4] Document OpenID Connect Discovery endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive documentation for the /.well-known/openid-configuration endpoint including: - Overview of OpenID Connect Discovery - Benefits (automatic configuration, multi-tenant support, version compatibility) - Example requests and responses - Multi-tenant usage patterns - Integration with OAuth clients like oidc-client-ts πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- content/oauth/endpoints.md | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/content/oauth/endpoints.md b/content/oauth/endpoints.md index 289173c..2bd7d3f 100644 --- a/content/oauth/endpoints.md +++ b/content/oauth/endpoints.md @@ -171,6 +171,93 @@ 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. +## Discovery endpoints + +Discovery endpoints allow OAuth/OpenID Connect clients to automatically discover the configuration and capabilities of Uitsmijter without manual configuration. This is especially useful for dynamic client registration, multi-tenant deployments, and maintaining compatibility across different versions. + +### /.well-known/openid-configuration + +The `/.well-known/openid-configuration` endpoint provides OpenID Connect Discovery metadata as specified in [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html). This endpoint returns a JSON document containing all the information that OAuth/OIDC clients need to interact with Uitsmijter, including: + +- **Endpoint URLs**: Authorization, token, userinfo, and JWKS endpoints +- **Supported features**: Grant types, response types, scopes, and authentication methods +- **Cryptographic capabilities**: Signing algorithms and PKCE methods +- **Multi-tenant configuration**: Each tenant has its own discovery document with tenant-specific settings + +**Why use OpenID Connect Discovery?** + +Instead of manually configuring every OAuth client with endpoint URLs and supported features, clients can automatically fetch this information from the discovery endpoint. This provides several benefits: + +1. **Automatic configuration**: Modern OAuth libraries (like [oidc-client-ts](https://github.com/authts/oidc-client-ts)) can automatically configure themselves by reading the discovery document +2. **Multi-tenant support**: Different tenants can advertise different capabilities (scopes, grant types, policies) +3. **Version compatibility**: When Uitsmijter is upgraded with new features, clients automatically discover the new capabilities +4. **Reduced configuration errors**: No need to manually maintain endpoint URLs in multiple client configurations + +**Example**: Fetching discovery metadata + +```shell +curl --request GET \ + --url https://id.example.com/.well-known/openid-configuration \ + --header 'Accept: application/json' +``` + +This returns a JSON document with the OpenID Provider Metadata: + +```json +{ + "issuer": "https://id.example.com", + "authorization_endpoint": "https://id.example.com/authorize", + "token_endpoint": "https://id.example.com/token", + "userinfo_endpoint": "https://id.example.com/userinfo", + "jwks_uri": "https://id.example.com/.well-known/jwks.json", + "scopes_supported": ["openid", "profile", "email", "read", "write"], + "response_types_supported": ["code"], + "grant_types_supported": ["authorization_code", "refresh_token"], + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["RS256"], + "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic", "none"], + "code_challenge_methods_supported": ["S256", "plain"], + "claims_supported": ["sub", "iss", "aud", "exp", "iat", "name", "email", "tenant"] +} +``` + +**Multi-tenant discovery** + +Each tenant in Uitsmijter has its own discovery endpoint with tenant-specific configuration: + +```shell +# Tenant A discovery +curl https://tenant-a.example.com/.well-known/openid-configuration + +# Tenant B discovery +curl https://tenant-b.example.com/.well-known/openid-configuration +``` + +The discovery document automatically reflects: +- Tenant-specific issuer URLs +- Aggregated scopes from all clients in the tenant +- Aggregated grant types from all clients in the tenant +- Tenant privacy policy URLs (if configured) + +**Using discovery with OAuth clients** + +Most modern OAuth/OIDC libraries support automatic configuration via discovery. For example, with `oidc-client-ts`: + +```typescript +import { UserManager } from 'oidc-client-ts'; + +const userManager = new UserManager({ + authority: 'https://id.example.com', // Base URL - library fetches /.well-known/openid-configuration + client_id: '9095A4F2-35B2-48B1-A325-309CA324B97E', + redirect_uri: 'https://myapp.example.com/callback', + // All endpoint URLs and capabilities are automatically discovered! +}); +``` + +The library will automatically fetch the discovery document and configure itself with the correct endpoints, supported scopes, and authentication methods. + +> **Note**: The discovery endpoint is publicly accessible and does not require authentication. This is by design, as clients need to discover the configuration before they can authenticate. + ## Profile endpoints Even the `/token/info` endpoint is not a standard endpoint in OAuth, it is widely used to provide information about From 8ca2c0b62c96f410c513b19f0f062191a14dddf1 Mon Sep 17 00:00:00 2001 From: Kris Simon Date: Sun, 26 Oct 2025 10:22:36 +0100 Subject: [PATCH 2/4] UIT-576 | ks |improvements, fixes, well-known --- content/configuration/tenant_client_config.md | 12 +- content/general/about.md | 26 +-- content/general/quickstart.md | 179 +++++++++--------- content/general/terminology.md | 2 +- content/interceptor/interceptor.md | 39 ++-- content/oauth/endpoints.md | 20 +- content/oauth/granttypes.md | 3 +- content/oauth/pkce.md | 6 +- content/providers/providers.md | 16 +- 9 files changed, 149 insertions(+), 154 deletions(-) diff --git a/content/configuration/tenant_client_config.md b/content/configuration/tenant_client_config.md index 3f1e47f..edc454b 100644 --- a/content/configuration/tenant_client_config.md +++ b/content/configuration/tenant_client_config.md @@ -71,7 +71,7 @@ config: method: "post", body: { user: credentials.username, password: md5(credentials.password) } }).then((result) => { - console.log("User Login", credentials.username, r.code); + console.log("User Login", credentials.username, result.status); if(result.status === 200){ this.isLoggedIn = true; this.profile = result.body; @@ -98,7 +98,7 @@ config: method: "post", body: { user: args.username } }).then((result) => { - console.log("User Validation", args.username, r.code); + console.log("User Validation", args.username, result.status); if(result.status === 200){ this.isValid = true; return commit(this.isValid); @@ -175,7 +175,7 @@ spec: | Property | Mandatory | Default | Example | Discussion | |-------------------|-----------|---------|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| name | yes | - | `bbnc` | The name of the tenant depends to your architectural discussions. Consider creating tenants for different brands or companies or teams inside your company. Remember: tenants are seperated spaces inside one instance. | +| name | yes | - | `bbnc` | The name of the tenant depends on your architectural decisions. Consider creating tenants for different brands, companies, or teams within your organization. Remember: tenants are separated spaces within a single instance. | | hosts | yes | - | `["bnbc.example", "us.bnbc.example"]` | A concrete list of hosts for which the server serves the tenant. Overlapping hosts in different tenants are not allowed, they have to be unique. Be sure that the hosts are configured as ingress hosts too. | | interceptor | no | | | _see the full example above_ | | interceptor.enabled | yes | | | Can be set to `false` if the tenant should not support the [Interceptor-Mode](/interceptor/interceptor). | @@ -233,7 +233,7 @@ config: secret: aejochiecaishee4ootooSh3ph ``` -> Remember. For Kubernetes warp name into `metadata` and rename `config` to `spec`: +> Remember: For Kubernetes, wrap the name in `metadata` and rename `config` to `spec`: ```yaml metadata: @@ -278,7 +278,7 @@ spec: | Property | Mandatory | Default | Example | Discussion | |---------------|-----------|-----------------------------------------|---------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ident | yes | A random UUID | `58392627-0121-4721-9DAC-D358BDD86CA6` | The ident is the internal primary key for that client. | -| name | yes | - | `bnbc-ios-app` | Give the client a unique and specific name. Client should be reelect the device classes that you do need to target with specific rights and get individual statistics from. | +| 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! | @@ -316,4 +316,4 @@ applications that run on a server controlled by developers and whose source code applications are considered public clients. Since anyone running a Javascript application can easily see the source code of the application, a secret would be easily visible there. -Set a secret for server-side applications where the user does not have access to the source code. +Set a secret for server-side applications where users cannot access the source code. diff --git a/content/general/about.md b/content/general/about.md index c4860fd..9c74193 100644 --- a/content/general/about.md +++ b/content/general/about.md @@ -7,20 +7,20 @@ weight: 1 Uitsmijter is a versatile OAuth2 authorization server and a Kubernetes Middleware for Traefik. -On one side it provides a flexible and powerful basis for new projects, on the other hand it has been built with the -focus to comfortably bring existing, mostly monolithic applications into the microservice, cloud- and multi-cloud world. +On one hand, it provides a flexible and powerful basis for new projects; on the other hand, it has been built with the +focus of comfortably bringing existing, mostly monolithic applications into the microservice, cloud, and multi-cloud world. -It offers multi-tenant single sign-on via secure, low-maintenance and easy-to-implement middleware, as well as +It offers multi-tenant single sign-on via secure, low-maintenance, and easy-to-implement middleware, as well as protocol-compliant OAuth 2.0 authorization workflows. Both processes work hand in hand and, after minimal and -easy-to-understand configuration in a short time after foolproof and fully automated (Infrastructure As Code) +easy-to-understand configuration, can be operational in a short time following foolproof and fully automated (Infrastructure as Code) installation. -A company-wide login can be put into operation within the shortest possible time in a vendor-neutral manner and without -data specifications on your user profiles, even without changing the user database. It is important that your data -contents and data structures as well as the data management can be determined by you at any time. +A company-wide login can be put into operation in the shortest possible time in a vendor-neutral manner without +data specifications for your user profiles, and even without changing the user database. It is important that your data +content, data structures, and data management can be determined by you at any time. -Uitsmijter does not bring its own user data storage, but offers interfaces to use your -existing databases and services in a simple, secure and elegant way. +Uitsmijter does not provide its own user data storage but offers interfaces to use your +existing databases and services in a simple, secure, and elegant way. Read more [about our motivation](/general/motivation) for Uitsmijter @@ -36,10 +36,10 @@ In addition to RFC 6749, there are several other RFCs that define specific aspec example, [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750.html) defines the Bearer Token usage, which specifies how to use access tokens in HTTP requests. -All information you need to install, configure, run the server, as well as configuring the client libraries are covered -in this documentation. Our goal is to present you everything you need in an understandable language. If you are missing -some aspects, please do not hesitate to [contact us](mailto:sales@uitsmijter.io). We are improving the documentation -constantly. Your feedback is welcome. +All information you need to install, configure, and run the server, as well as configure the client libraries, is covered +in this documentation. Our goal is to present everything you need in an understandable language. If you find +some aspects are missing, please do not hesitate to [contact us](mailto:sales@uitsmijter.io). We are continuously improving the documentation. +Your feedback is welcome. ## Further readings diff --git a/content/general/quickstart.md b/content/general/quickstart.md index 8f6c804..75196c3 100644 --- a/content/general/quickstart.md +++ b/content/general/quickstart.md @@ -5,23 +5,20 @@ weight: 5 # Quick Start Guide for Kubernetes -This guide covers all you need to get up and running with Uitsmijter. The documentation is based on a fictive Project -for better understanding when and why to set some configurations. +This guide covers everything you need to get up and running with Uitsmijter. The documentation is based on a fictional project +to provide better understanding of when and why to set specific configurations. ## Meet the requirements -This quick start guide assumes that the requirements are given. See [this list of requirements](/general/requirements) -that -cover -the following criteria: +This quick start guide assumes that the requirements are met. See [this list of requirements](/general/requirements) that covers the following criteria: - Kubernetes is up and running - Traefik is up and running - Your cluster is able to get valid certificates for ingresses, e.g. with cert-manager -## Needed privileges to deploy onto your cluster +## Required privileges to deploy onto your cluster -To deploy a working instance of Uitsmijter you need to have privileges on the kubernetes cluster that allow you to +To deploy a working instance of Uitsmijter, you need privileges on the Kubernetes cluster that allow you to deploy the following resource kinds: **A service account with a cluster role** is needed to allow Uitsmijter to read its `CustomResources` @@ -34,7 +31,7 @@ deploy the following resource kinds: - CustomResourceDefinition -**Kubernetes Resources will be installed** during the installation: +**Kubernetes resources will be installed** during the installation: - Namespace - ConfigMap @@ -45,12 +42,11 @@ deploy the following resource kinds: - Ingress - HorizontalPodAutoscaler -**The Interceptor-Mode is relying on Traefik Middlewares** that will be set up during the installation: +**The Interceptor-Mode relies on Traefik Middlewares** that will be set up during the installation: - Middleware -**CustomResources, declared by `CustomResourceDefinition` should be allowed to create, list and edit** by your account -in your namespaces: +**CustomResources declared by `CustomResourceDefinition` should allow your account to create, list, and edit** in your namespaces: - Client - Tenant @@ -60,8 +56,8 @@ your system administrator for help. ## Prepare the installation -Uitsmijter offers a [πŸ”— Helm](https://helm.sh) installation routine. Download the Values.yaml first and change the -values for your needs. The following example describes the sections on a fictive project. You have to change the values +Uitsmijter offers a [πŸ”— Helm](https://helm.sh) installation routine. Download the Values.yaml first and adjust the +values for your needs. The following example describes the sections for a fictional project. You will need to change the values accordingly. **The Project Setup**: @@ -69,10 +65,10 @@ We are planning a new customer portal for the domain `example.com`. The portal s send small notes to a selected group of recipients. However, we are planning to create different Microservices behind a Single-Page-Application (SPA). -The SPA shows general available content and offers a login button. Various functions are available only if a user is -logged in. Without a valid login the user sees marketing project information provided by a cms. After login the user has -access to its own profile, address book and incoming messages and also allowed to write a new message to all -participants of the address book. +The SPA shows generally available content and offers a login button. Various functions are available only if a user is +logged in. Without a valid login, the user sees marketing project information provided by a CMS. After login, the user has +access to their own profile, address book, and incoming messages and is also allowed to write a new message to all +participants in the address book. The business requirements say that certain users with the `partner` role should have an extra functionality that is available as a link to a portal that is made by another team. If the user is logged in to example.com then the user @@ -88,17 +84,17 @@ So far so good. The architecture of the new project is set and looks like this: - Inbox backend (inbox.srv.example.com) - Send messages backend (send.srv.example.com) -> As you can see we do make the services public available! We will secure them later on with a JWT. To make it -> accessible from within the SPA it should be publicly available, otherwise we would need +> As you can see, we do make the services publicly available! We will secure them later with a JWT. To make them +> accessible from within the SPA, they should be publicly available; otherwise, we would need > a [πŸ”— BFF](https://blog.bitsrc.io/bff-pattern-backend-for-frontend-an-introduction-e4fa965128bf). **Create a User Backend**: -> Somewhere user data must be stored. **Uitsmijter does not store any account data, profiles or passwords**. To create a -> store for the users credentials either a service must be created or selected from the existing once. In our example -> the `Profile backend` would fit, but this we want to make public available and the user store should only be +> User data must be stored somewhere. **Uitsmijter does not store any account data, profiles, or passwords**. To create a +> store for user credentials, either a service must be created or selected from the existing ones. In our example, +> the `Profile backend` would fit, but we want to make this publicly available, and the user store should only be > accessible -> within the cluster. So we could do an extra route that is only available from a private service but for the sake of -> security and the luck of a new project we create a service that is just there to store user credentials. +> within the cluster. We could create an extra route that is only available from a private service, but for the sake of +> security and the benefit of a new project, we will create a service dedicated to storing user credentials. This new `Credentials service` got one route named: "POST: /validate-login" and fires a query against a database: @@ -111,9 +107,9 @@ WHERE `username` = ? _In our example passwords are stored as a sha256-Hash. You can choose between sha256, md5 and plain text._ -Some other applications will fill in the users after registration. This is out of scope for now. Important is that -the `/validate-login` takes two parameters: `username` and `passwordHash` and returns a status 200 with a user profile -object or some unauthorised error if the credentials do not match. +Other applications will populate the users after registration. This is out of scope for now. The important thing is that +`/validate-login` takes two parameters: `username` and `passwordHash`, and returns a status 200 with a user profile +object or an unauthorized error if the credentials do not match. In case the credentials match, return the user profile object: @@ -147,9 +143,9 @@ spec: ``` **It's time to install Uitsmijter!**: -At this point in time, we need some service that handles the authorisation for our project. We do not want to log in -multiple times to different portals, and we do not want to authenticate the user in all backends. Backends should be -denied the access if a user request with an invalid token, and access data on the users behalf if the token is correct. +At this point, we need a service that handles the authorization for our project. We do not want to log in +multiple times to different portals, and we do not want to authenticate the user in all backends. Backends should +deny access if a user requests with an invalid token, and access data on the user's behalf if the token is correct. That implies that we expect some criteria: @@ -159,7 +155,7 @@ That implies that we expect some criteria: - To allow other Portals (like partner.example.com) to join the SSO, authorisation must be outside the main portal **Edit the Uitsmijter Values.yaml**: -In this section we go through all the available settings and describe them in detail with recommended settings for the +In this section, we will go through all available settings and describe them in detail with recommended settings for the demo project described above. ### Namespace @@ -168,11 +164,11 @@ demo project described above. namespaceOverride: "" ``` -This value specifies the namespace in which Uitsmijter should be installed. We recommend to install into the default -namespace: `uitsmijter`. If you are planning installation into another namespace, you have to adjust Middleware paths -later on. That is very easy if you know what you are doing, but can be confusing if you are new to Kubernetes or +This value specifies the namespace in which Uitsmijter should be installed. We recommend installing into the default +namespace: `uitsmijter`. If you are planning to install into another namespace, you will need to adjust Middleware paths +later. This is straightforward if you know what you are doing, but can be confusing if you are new to Kubernetes or [πŸ”— Ingress middleware with Traefik](https://doc.traefik.io/traefik/middlewares/overview/). If you want to start without -hassle and without debugging it is highly recommended to install Uitsmijter in the desired namespace first. +hassle and debugging, it is highly recommended to install Uitsmijter in the default namespace first. ### Repository, Images and Tags @@ -183,19 +179,19 @@ image: tag: "" ``` -If you downloaded the newest version from the public repository the settings are just fine and work out of the box. -Only if you host Docker images at a private repository you need to change the `image.repository` path to locate to your +If you downloaded the newest version from the public repository, the settings are fine and work out of the box. +Only if you host Docker images in a private repository do you need to change the `image.repository` path to point to your private copy of the image. For example: `docker.example.com/sso/uitsmijter`. -> We **do not recommend** to host a single private copy of Uitsmijter in your own repository, because we are updating -> the images to fix bugs and improve features frequently. To get informed about updates and pull from the latest version -> you may want to clone a mirror of the whole repository instead. If you do not know how to do this, +> We **do not recommend** hosting a single private copy of Uitsmijter in your own repository because we update +> the images to fix bugs and improve features frequently. To stay informed about updates and pull from the latest version, +> you may want to clone a mirror of the entire repository instead. If you do not know how to do this, > please [ask for assistance](mailto:sales@uitsmijter.io). -The Version `tag` is set automatically according to the Application version of the Helm chart. Please be sure that you +The version `tag` is set automatically according to the application version of the Helm chart. Please ensure that you have downloaded the latest version. -Only if you are doing an upgrade, you have to set the version by hand. For example upgrading from version `1.0.0` to -version `1.0.1` you have to set the tag: +Only if you are performing an upgrade do you need to set the version manually. For example, when upgrading from version `1.0.0` to +version `1.0.1`, you need to set the tag: ```yaml tag: "1.0.1" @@ -207,8 +203,8 @@ version `1.0.1` you have to set the tag: imagePullSecrets: ``` -Default is blank, because Uitsmijter is public available. But if you are cloning the repository into your private one, -it may be secured by a imagePullSecret. You can define the name of the secret here. +Default is blank because Uitsmijter is publicly available. However, if you are cloning the repository to your private one, +it may be secured by an imagePullSecret. You can define the name of the secret here. > Beware that the secret must be present in the namespace of Uitsmijter! @@ -231,9 +227,9 @@ installSA: true You **have to** change the values of the passwords in `jwtSecret` and `redisPassword`! -The `jwtSecret` is a global passphrase with which all JWTs are signed. Applications dealing with the JWT must know -this shared secret. The `jwtSecret` should be set while installation and kept on the server only. We highly -recommend to use [πŸ”— config-syncer](https://github.com/kubeops/config-syncer) to share the secret into other namespaces. +The `jwtSecret` is a global passphrase with which all JWTs are signed. Applications dealing with JWTs must know +this shared secret. The `jwtSecret` should be set during installation and kept on the server only. We highly +recommend using [πŸ”— config-syncer](https://github.com/kubeops/config-syncer) to share the secret to other namespaces. From the example above we decided that the `Profile backend`, `Address book backend`, `Inbox backend` and `Send messages backend` will get their own namespaces to collect the backend and the databases, as well as services @@ -245,11 +241,10 @@ and ingresses all together in the domain of the service: - sender The `jwtSecret` will be created as a secret in the `uitsmijter` namespace (_if not changed with `namespaceOverride`_). -All the backends need to know about the secret to validate the incoming JWT. Rather than creating handwritten -secrets in all the four namespaces that can run out of sync can run out of sync while rolling the secret (_that you -should do from time to -time_), we recommend to **sync** the secret from the `uitsmijter` namespace into the `profile`, `address`, `inbox` -and `sender`namespace. +All backends need to know the secret to validate incoming JWTs. Rather than creating manual +secrets in all four namespaces that can fall out of sync when rotating the secret (_which you +should do from time to time_), we recommend **syncing** the secret from the `uitsmijter` namespace to the `profile`, `address`, `inbox`, +and `sender` namespaces. To sync the secret into namespaces add a label to the namespace the secret has to sync in: @@ -268,12 +263,12 @@ please take a look at the [πŸ”— config-syncer documentation](https://appscode.com/products/kubed/v0.12.0/guides/config-syncer/intra-cluster/). The Uitsmijter installation will set up a [πŸ”— Redis database](https://redis.io) to store refresh tokens. -The `redisPassword` will only be used inside the `uitsmijter` namespace, and you **have to** replace the value while -installing. +The `redisPassword` will only be used inside the `uitsmijter` namespace, and you **must** replace the value during +installation. -> Attention: after changing the redis password you have to roll out redis again and restart the services. We recommend -> to generate a random password at the first installation and keep it secret for the implementation. To roll the -> secret you may want to come back later and [πŸ”— read this article](https://redis.io/docs/management/security/acl/). +> Attention: after changing the Redis password, you must roll out Redis again and restart the services. We recommend +> generating a random password at the first installation and keeping it secret during implementation. To rotate the +> secret, you may want to return later and [πŸ”— read this article](https://redis.io/docs/management/security/acl/). The `storageClassName` highly depends on your Kubernetes installation. You can list all available storage classes with kubectl: @@ -301,35 +296,35 @@ config: ``` **logFormat**: -The log format can be switched between `console` and `ndjson`. console will print out each log entry on a single line -with the level and the server time: +The log format can be switched between `console` and `ndjson`. Console will print each log entry on a single line +with the level and server time: ```text [NOTICE] Wed, 21 Dec 2022 10:48:24 GMT: Server starting on http://127.0.0.1:8080 ``` -If you are using a log aggregator it is more familiar to log in [πŸ”— ndjson](http://ndjson.org): +If you are using a log aggregator, it is more convenient to log in [πŸ”— ndjson](http://ndjson.org): ```text {"function":"start(address:)","level":"NOTICE","date":"2022-12-21T10:52:18Z","message":"Server starting on http:\/\/127.0.0.1:8080"} ``` **logLevel**: -The standard log level is `info` and provides a good overview of what Uitsmijter is doing. `info` also prints out -notices, errors and critical alerts as well. +The standard log level is `info` and provides a good overview of what Uitsmijter is doing. `info` also prints +notices, errors, and critical alerts. -In case you want to see more of the applications behavior you may want to switch on the development `trace` logs. And if -you just want to get alerts about things that do not go well, you can suppress most of the info and notices by setting +If you want to see more of the application's behavior, you may want to enable the development `trace` logs. If +you only want alerts about issues, you can suppress most info and notices by setting the log level to `error`. > Everything about logging is [described in this separate section](/configuration/logging) **cookieExpirationInDays**: -You can adjust the days a cookie is valid without refreshing its value. A valid cookie means that the user is logged in. -This is highly important for the Interceptor-Mode, because if you are deleting a user it can still use your service for -the period of the cookie time! A good value to start with is **1 day**. A deleted user is valid for the maximum of 24h -in [Interceptor-Mode](/interceptor/interceptor) and with maximum of `tokenExpirationInHours` for -each [OAuth-FLow](/oauth/flow). +You can adjust how many days a cookie is valid without refreshing its value. A valid cookie means the user is logged in. +This is highly important for Interceptor-Mode because if you delete a user, they can still use your service for +the duration of the cookie lifetime! A good starting value is **1 day**. A deleted user remains valid for a maximum of 24 hours +in [Interceptor-Mode](/interceptor/interceptor) and for a maximum of `tokenExpirationInHours` for +each [OAuth-Flow](/oauth/flow). > The cookie expiration time has to be always equal or greater than the token expiration. > _In the example project we assume that a user pays in a monthly subscription, and we do not have external resources @@ -337,30 +332,30 @@ each [OAuth-FLow](/oauth/flow). > will fit our needs later on, too._ **tokenExpirationInHours**: -In [OAuth-FLow](/oauth/flow) the user exchanges an authorization code (see [grant_types](/oauth/granttypes)) for an +In [OAuth-Flow](/oauth/flow), the user exchanges an authorization code (see [grant_types](/oauth/granttypes)) for an access and refresh token. -If the access token expires, a new valid one can obtained with the refresh token. +If the access token expires, a new valid one can be obtained with the refresh token. -As long as the access token is not expired, a user is logged in, even if the user has been deleted from the credentials +As long as the access token has not expired, a user is logged in, even if the user has been deleted from the credentials service. -In the example of `2 hours` the user can access our portal at least for a maximum of 2 hours before being kicked out. -This setting is regardless of the cookie lifetime. +With the example value of `2 hours`, the user can access our portal for a maximum of 2 hours before being kicked out. +This setting is independent of the cookie lifetime. > Special case **silent login**: If silent login is turned on, the login might happen automatically! > You should only rely on the token expiration time when silent login is turned off (enabled by default). > More information is provided in the [tenant and client configuration](/configuration/tenant_client_config) section. **tokenRefreshExpirationInHours**: -For every code exchange and every refresh the authorisation server generates a pair of an access token and a refresh -token. The access token is a Bearer encoded JWT with the user profile encoded. The refresh token is a random key that +For every code exchange and every refresh, the authorization server generates a pair of an access token and a refresh +token. The access token is a Bearer-encoded JWT with the user profile encoded. The refresh token is a random key that can be used to refresh the access token. -If an access token gets invalid, the user (mostly the library that is used) can get a new fresh valid access token with +If an access token becomes invalid, the user (or the library being used) can obtain a new valid access token with the refresh token (see [grant_types](/oauth/granttypes)). -Uitsmijter stores the refresh tokens for a defined amount of time. If a user has a valid and known refresh token, an +Uitsmijter stores refresh tokens for a defined amount of time. If a user has a valid and known refresh token, an access token can be requested. -Therefor the refresh expiration period **must be** longer than the access token. +Therefore, the refresh expiration period **must be** longer than the access token expiration. > Do you know those mobile Apps where you are always logged in after initial registration? Those apps know you because > they have a very long refresh token period (sometimes ~1 year). When opening the app the first thing is to exchange @@ -368,16 +363,16 @@ Therefor the refresh expiration period **must be** longer than the access token. > access token, regardless of the period, with the very long-lived refresh token. This is the way you are always signed > in. In our example after 30 days (720 hours) of inactivity the user must log in with credentials again. -Our recommendation for the first installation is set as defaults. You may want to adjust the settings later on to fit to -your business model. If you need any assistance please to not hesitate +Our recommendation for the first installation is to use the default settings. You may want to adjust the settings later to fit +your business model. If you need any assistance, please do not hesitate to [contact our consultants](mailto:sales@uitsmijzter.io) or ask the community. ### Domains -Uitsmijter should run at least on one domain. At least, because Uitsmijter is multi tenant and multi client aware and -one instance _can_ run for more than one domain. For large installations with multiple different brands it may be a good -idea to run one clustered Uitsmijter and provide the login functionality to different domains, so that a login does not -change the main domain to ensure the trust level for your customers. +Uitsmijter should run on at least one domain. We say "at least" because Uitsmijter is multi-tenant and multi-client aware, and +one instance _can_ run on more than one domain. For large installations with multiple different brands, it may be a good +idea to run one clustered Uitsmijter instance and provide login functionality to different domains so that a login does not +change the main domain, ensuring the trust level for your customers. ```yaml domains: @@ -427,16 +422,16 @@ Read more about the [helm charts](/configuration/helm) configuration. ## Create the first Tenant -In the project example we are setting up Uitsmijter for one domain and one company. Only one tenant is needed. Examples -for a multi-tenant setup is given in the [tenant and client configuration](/configuration/tenant_client_config) section. -Our one and only tenant is called `portal`. For the configuration of this tenant we first create a new namespace -to collect all overall settings there: +In the project example, we are setting up Uitsmijter for one domain and one company. Only one tenant is needed. Examples +for a multi-tenant setup are given in the [tenant and client configuration](/configuration/tenant_client_config) section. +Our single tenant is called `portal`. For the configuration of this tenant, we first create a new namespace +to collect all overall settings: ```shell kubectl create ns portal ``` -In that namespace we will add the tenant. Therefore, we have to define it first: +In that namespace, we will add the tenant. First, we need to define it: ```yaml --- diff --git a/content/general/terminology.md b/content/general/terminology.md index 7c72318..f207221 100644 --- a/content/general/terminology.md +++ b/content/general/terminology.md @@ -23,5 +23,5 @@ weight: 2 A Tenant can be a company or a product with unique user group. It has Clients which represent different applications. See also [Configuration/Tenant](/configuration/entities#Tenant). - `JWT` - A JSON Web Token is a method for representing claims securely between two parties as defined in RFC 7519. -- `Provider` - A provider are lightweight scripts that establish a seamless connection between Uitsmijter and a user +- `Provider` - Providers are lightweight scripts that establish a seamless connection between Uitsmijter and a user data store diff --git a/content/interceptor/interceptor.md b/content/interceptor/interceptor.md index fa9d968..b33820f 100644 --- a/content/interceptor/interceptor.md +++ b/content/interceptor/interceptor.md @@ -6,12 +6,12 @@ weight: 2 # Interceptor Mode Interceptor mode is used within Traefik2 as a middleware authorization controller. -When a resource is requested the middleware checks if the current user is logged in. If not, the request is -redirected to the login page. If the user making the request is logged in, then the middleware forwards the request +When a resource is requested, the middleware checks if the current user is logged in. If not, the request is +redirected to the login page. If the user making the request is logged in, the middleware forwards the request to the requested resource. -> For other ingress controllers support please feel free to [contact](mailto:sales@uitsmijter.io) our development and -> consulting team. We are constantly adding support for other controllers and document them if needed. +> For support of other ingress controllers, please feel free to [contact](mailto:sales@uitsmijter.io) our development and +> consulting team. We are constantly adding support for other controllers and documenting them as needed. ## Flow @@ -41,14 +41,14 @@ to the requested resource. 2. The AuthForward delegates the request to `Uitsmijter` first 3. If the user is not logged in, a login mask is provided 4. If the login fails, the AuthForward responds with an error code -5. If the login succeeded, or the user is already logged in, Uitsmijter adds the JWT to the header and the AuthForwarder +5. If the login succeeds, or the user is already logged in, Uitsmijter adds the JWT to the header and the AuthForwarder forwards the request to the resource server. ## Login status -The status whether a user is logged in or not is stored in a cookie that is strictly bound to the domain of the -middleware. The domain must be set at tenant level, shown in the [example](/interceptor/examples) section. -Inside the cookie there is an encoded JWT stored. This JWT will be added to the `Authorization` header for every +The status of whether a user is logged in is stored in a cookie that is strictly bound to the domain of the +middleware. The domain must be set at the tenant level, as shown in the [example](/interceptor/examples) section. +An encoded JWT is stored inside the cookie. This JWT will be added to the `Authorization` header for every request. > **In your application:** @@ -57,16 +57,15 @@ request. ## Refresh the token -The middleware will refresh the requests JWT automatically, when 3/4 of the lifetime was passed. +The middleware will refresh the request's JWT automatically when 3/4 of the lifetime has passed. -> This could potentially lead to a situation where two different valid JWTs arrive at the underlying application, in case -> the application fires parallel requests against themselve. Even both tokens are valid and encode the same -> information, some applications may not like this when storing the original token. The solution for this scenario is -> easy: validate the token as soon as possible and decode the payload first. Save the decoded payload for comparison, -> not the token. This "problem" is just an academic one, because if your application makes a request with a token which -> is -> already known you are in the Single-Page-Application landscape already. In this case please use a -> proper [OAuth-Flow](/oauth/flow) instead. In a server rendered application a parallel request with different tokens +> This could potentially lead to a situation where two different valid JWTs arrive at the underlying application if +> the application fires parallel requests against itself. Even though both tokens are valid and encode the same +> information, some applications may not handle this well when storing the original token. The solution for this scenario is +> simple: validate the token as soon as possible and decode the payload first. Save the decoded payload for comparison, +> not the token. This "problem" is primarily academic because if your application makes requests with a token that is +> already known, you are already in the Single-Page-Application landscape. In this case, please use a +> proper [OAuth-Flow](/oauth/flow) instead. In a server-rendered application, parallel requests with different tokens > will never be a problem if you decode the payload first. ## Configuration and Examples @@ -78,10 +77,10 @@ annotations: traefik.ingress.kubernetes.io/router.middlewares: uitsmijter-forward-auth@kubernetescrd ``` -If your setup works on the same top level domain, then that is everything needed. For example, Uitsmijter's main domain -is: `login.example.com` and the resource server to protect is located at `secured.example.com`. +If your setup operates on the same top-level domain, then that is all that is needed. For example, if Uitsmijter's main domain +is `login.example.com` and the resource server to protect is located at `secured.example.com`. -A bit more tricky is when projects are at different top level domains. For example the Uitsmijter installation is still +It is more challenging when projects are on different top-level domains. For example, if the Uitsmijter installation is still located at `login.example.com`, but the resource server to protect is located at `toast.example.com`. Because cookies must be from within the same domain, the trick is to proxy the service into the new domain via an [πŸ”— external service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) and then defining diff --git a/content/oauth/endpoints.md b/content/oauth/endpoints.md index 2bd7d3f..a50d5a5 100644 --- a/content/oauth/endpoints.md +++ b/content/oauth/endpoints.md @@ -5,7 +5,7 @@ weight: 3 # Available Endpoints -An endpoint is a specific location that is capable of accepting incoming requests, and is usually a specific URL +An endpoint is a specific location capable of accepting incoming requests and is usually a specific URL (Uniform Resource Locator) that is provided by an API (Application Programming Interface). An API is a set of programming instructions and standards for accessing a web-based software application or web tool. APIs allow different software systems to communicate with each other, and enable functionality such as requesting data from a server, or @@ -28,9 +28,9 @@ In OAuth two additional endpoints should be mentioned: Besides the authorisation endpoint and the token endpoint Uitsmijter do provide endpoints for monitoring and metrics as well. -This page describes the technical details of the available endpoints and shows some basic examples how to use them. -This information is importend if you are writing your own client library implementation, but you will not need to know all -the details when using an already existent client library +This page describes the technical details of the available endpoints and shows some basic examples of how to use them. +This information is important if you are writing your own client library implementation, but you will not need to know all +the details when using an existing client library like [πŸ”— oidc-client-ts](https://github.com/authts/oidc-client-ts). ## OAuth endpoints @@ -260,8 +260,14 @@ The library will automatically fetch the discovery document and configure itself ## Profile endpoints -Even the `/token/info` endpoint is not a standard endpoint in OAuth, it is widely used to provide information about -access tokens. +### /token/info (UserInfo Endpoint) + +The `/token/info` endpoint provides user profile information for the authenticated user. This endpoint serves as +Uitsmijter's implementation of the OpenID Connect UserInfo endpoint. + +> **Note:** While the OpenID Connect specification defines the standard endpoint as `/userinfo`, Uitsmijter uses +> `/token/info` as the UserInfo endpoint. The OpenID Connect Discovery document correctly advertises this endpoint +> as the `userinfo_endpoint`. To use this endpoint, you will need to make a GET request to the `/token/info` endpoint and include the access token in the request. For example, you can use the curl command to make a request like this: @@ -284,7 +290,7 @@ of the authorization that it represents. For example: > **Customise the profile**: > > The JSON object returned can be customised by the user backend provider. Everything that is returned from -> the `userProfile` getter is encoded in the JWT and will be decoded in the response of teh `/token/info` call. +> the `userProfile` getter is encoded in the JWT and will be decoded in the response of the `/token/info` call. ## Monitoring endpoints diff --git a/content/oauth/granttypes.md b/content/oauth/granttypes.md index 77e691a..c278546 100644 --- a/content/oauth/granttypes.md +++ b/content/oauth/granttypes.md @@ -158,8 +158,7 @@ curl -v \ Clients explicitly have to turn on the `password` grant type to support it! -The `password` grant type should be used **for testing purposes only**. In OAuth the `password` grant type is often -called `implicit grant flow`. The user directly sends the username and the **cleartext password** to +The `password` grant type should be used **for testing purposes only**. The user directly sends the username and the **cleartext password** to the `Authorization server` and receives a valid `access token` when the credentials match. The returned token contains only a valid `access token` without a `refresh token`. Users with this kind of token pair diff --git a/content/oauth/pkce.md b/content/oauth/pkce.md index 3dc7ef4..27cadf5 100644 --- a/content/oauth/pkce.md +++ b/content/oauth/pkce.md @@ -41,10 +41,8 @@ reason not to. For example if you are using a client library or framework that d relatively new extension to OAuth 2.0, so some client libraries or frameworks might not yet support it. In this case, you would not be able to use PKCE with these libraries or frameworks. Consider to update the client library. -Clients as describes in [tenant and client configuration](/configuration/tenant_client_config) can be set to except -PKCE-only -clients. This should be the default and could be the default if not set explicitly to `false` in the configuration of -the client. +Clients as described in [tenant and client configuration](/configuration/tenant_client_config) can be set to accept +only PKCE requests. This should be the default behavior unless explicitly set to `false` in the client configuration. ## Generating a code verifier diff --git a/content/providers/providers.md b/content/providers/providers.md index bbbfe30..5d7b908 100644 --- a/content/providers/providers.md +++ b/content/providers/providers.md @@ -5,8 +5,7 @@ weight: 1 # General provider information -Because Uitsmijter does not store any user data to authenticate a login, request providers are written to check if given -credentials are valid. Each `tenant` has a set of providers to do certain tasks. +Because Uitsmijter does not store user authentication data, providers are written to verify if given credentials are valid. Each `tenant` has a set of providers to do certain tasks. The [User Login Provider](/providers/userloginprovider) is responsible for the user backend which knows how to verify user credentials. The [User Validation Provider](/providers/uservalidationprovider) is responsible to check if a `username` still exists in the backend user store. @@ -54,9 +53,9 @@ Minimal example: } ``` -The providers are responsible for verifying the user and getting the profile of a user into the authorization server. -Providers are only glue code and normally should not implement any business logic at all. -Usually, providers are sending a request to some service and committing the result back. +The providers are responsible for verifying the user and retrieving the user profile for the authorization server. +Providers are only glue code and normally should not implement any business logic. +Typically, providers send a request to a service and commit the result back. Example: @@ -121,10 +120,9 @@ your operation is done. You also **have to** provide one getter: - isValid -The execution time of a provider is limited. The advanced setting `SCRIPT_TIMEOUT` can manipulate that behaviour in the -future, but the default (and this is what you should use, if not less) is set to **30 seconds**. The provider has to -complete all tasks within this time limit, this includes performing all necessary requests and reply with the -result. +Provider execution time is limited. The advanced setting `SCRIPT_TIMEOUT` can modify this behavior. +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. ## Further readings From 9d0edea14f7f8f0089d9120e6ee2300a6059fb99 Mon Sep 17 00:00:00 2001 From: Kris Simon Date: Mon, 27 Oct 2025 07:44:47 +0100 Subject: [PATCH 3/4] Add test filter documentation --- content/contribution/tooling.md | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/content/contribution/tooling.md b/content/contribution/tooling.md index 863af00..8b16a47 100644 --- a/content/contribution/tooling.md +++ b/content/contribution/tooling.md @@ -161,6 +161,54 @@ test suites. After the test a coverage reports is generated. ./tooling.sh test ``` +#### Filtering Unit Tests + +You can filter unit tests to run only specific test targets, suites, or individual tests by passing a filter argument directly after the `test` command: + +```shell +# Run all tests in a specific target +./tooling.sh test + +# Examples: +./tooling.sh test ServerTests # Run all ServerTests +./tooling.sh test LoggerTests # Run all LoggerTests +./tooling.sh test Uitsmijter-AuthServerTests # Run all Uitsmijter-AuthServerTests +``` + +**Dash vs Underscore Automatic Conversion:** + +Swift test automatically converts target names containing dashes (`-`) to underscores (`_`) in test identifiers. The tooling automatically handles this conversion, so you can use either format: + +```bash +# Both work (dashes are automatically converted to underscores) +./tooling.sh test Uitsmijter-AuthServerTests +./tooling.sh test Uitsmijter_AuthServerTests +``` + +**Advanced Filtering:** + +You can also filter by suite name or individual test: + +```bash +# Filter by suite +./tooling.sh test "ServerTests.AppTests" + +# Filter by specific test +./tooling.sh test "ServerTests.AppTests/testHelloWorld" +``` + +The test script shows debug output when a filter is applied: + +``` +Test filter: --filter Uitsmijter_AuthServerTests +``` + +Or when no filter is set: + +``` +No test filter set - running all tests +``` + ### e2e Besides the bespoken UnitTests, `e2e` runs end-to-end tests which can be found as shell scripts in `/Tests/e2e/`. @@ -192,6 +240,35 @@ option `--dirty` to the command. > Do never ever use a --dirty flag in a CI! The safety of a fresh (non cached) release is always more important than > saving CI-hours. Use `--dirty` in your own workflow only. +#### Filtering E2E Tests + +You can filter e2e tests to run only specific test scenarios using the `--filter` flag with a test name pattern. The e2e tests use [πŸ”— Playwright](https://playwright.dev/) and support filtering by test description: + +```shell +# Filter e2e tests by pattern +./tooling.sh e2e --filter "" + +# Examples: +./tooling.sh e2e --filter "should respond with error" +./tooling.sh e2e --filter "login" +./tooling.sh e2e --filter "OAuth" +``` + +You can combine the `--filter` flag with other options: + +```bash +# Run filtered tests with dirty build (faster development) +./tooling.sh e2e --dirty --filter "login" + +# Run filtered tests with fast mode (single browser) +./tooling.sh e2e --fast --filter "should respond with error" + +# Combine all options +./tooling.sh e2e --dirty --fast --filter "OAuth" +``` + +The filter pattern is passed to Playwright's `--grep` option, which matches against test descriptions using regular expressions. + ### run Start the Uitsmijter server localy for testing in a docker environment. It can be reached at http://localhost:8080. The From bf955755b008abf5be85897186087326e4f2c2f1 Mon Sep 17 00:00:00 2001 From: Kris Simon Date: Sat, 8 Nov 2025 12:05:52 +0100 Subject: [PATCH 4/4] Add comprehensive documentation for RFC 7517 (JWKS) and JWT algorithms This commit adds extensive documentation for the JWKS endpoint and JWT signing algorithm configuration: **Updates to oauth/endpoints.md:** - Added complete `/.well-known/jwks.json` endpoint documentation - Explained JWKS purpose, use cases, and benefits - Documented JWKS response structure and field meanings - Added caching behavior and recommendations - Provided JWT verification example with jwks-rsa library - Explained key rotation process and zero-downtime updates - Documented algorithm selection (HS256 vs RS256) - Added security recommendations for RS256 adoption **New file: configuration/jwt_algorithms.md:** - Comprehensive guide to JWT signing algorithms - Detailed comparison table: HS256 vs RS256 - When to use each algorithm (use cases and recommendations) - Zero-downtime migration strategy from HS256 to RS256 - 8-step migration process with code examples - Rollback strategy - Monitoring and verification steps - Key rotation best practices (RS256 only) - Troubleshooting guide for common migration issues - Environment variable reference (JWT_ALGORITHM, JWT_SECRET, etc.) - Links to RFCs and related documentation The documentation provides clear guidance for: - Production deployments using RS256 - Migrating existing HS256 deployments - Understanding JWKS and public key distribution - Implementing resource server JWT verification All documentation follows the existing style and includes practical examples for developers. --- content/configuration/jwt_algorithms.md | 410 ++++++++++++++++++++++++ content/oauth/endpoints.md | 235 ++++++++++++++ 2 files changed, 645 insertions(+) create mode 100644 content/configuration/jwt_algorithms.md diff --git a/content/configuration/jwt_algorithms.md b/content/configuration/jwt_algorithms.md new file mode 100644 index 0000000..5a0d6d2 --- /dev/null +++ b/content/configuration/jwt_algorithms.md @@ -0,0 +1,410 @@ +--- +title: 'JWT Signing Algorithms' +weight: 6 +--- + +# JWT Signing Algorithms + +Uitsmijter supports two JWT signing algorithms for access tokens: **HS256** (HMAC with SHA-256) and **RS256** (RSA with SHA-256). This guide explains the differences between these algorithms and how to migrate from HS256 to RS256. + +## Algorithm Comparison + +| Feature | HS256 (Symmetric) | RS256 (Asymmetric) | +|---------|-------------------|-------------------| +| **Key Type** | Shared secret (single key) | RSA key pair (public + private) | +| **Security** | Good (if secret is protected) | **Better** (private key never shared) | +| **Key Distribution** | Secret must be shared securely | Public key can be distributed openly | +| **Token Verification** | Requires shared secret | Uses public key (via JWKS) | +| **Key Rotation** | Requires secret update everywhere | **Seamless** (JWKS supports multiple keys) | +| **Performance** | Faster (symmetric crypto) | Slightly slower (asymmetric crypto) | +| **Use Case** | Simple deployments, testing | **Production, microservices** | +| **JWKS Endpoint** | Not used | **Required** (`/.well-known/jwks.json`) | +| **Default** | Yes (backward compatibility) | Recommended for new deployments | + +## HS256 (HMAC with SHA-256) + +HS256 is a symmetric algorithm that uses a shared secret key to both sign and verify JWTs. + +### How it works + +1. Uitsmijter signs JWTs using a secret key (from `JWT_SECRET` environment variable) +2. Resource servers verify JWTs using the **same secret key** +3. The secret must be securely shared between Uitsmijter and all resource servers + +### Configuration + +HS256 is the default algorithm for backward compatibility: + +```yaml +# .env or deployment config +JWT_ALGORITHM: HS256 # or omit this line entirely (defaults to HS256) +JWT_SECRET: your-secret-key-at-least-256-bits +``` + +### When to use HS256 + +- **Development and testing**: Simple setup, no key management +- **Monolithic applications**: Single application verifies tokens +- **Legacy systems**: Already using HS256 and shared secrets +- **High-performance scenarios**: Marginally faster than RS256 + +### Security considerations + +- **Secret management**: The `JWT_SECRET` must be kept confidential +- **Secret distribution**: Every service that verifies tokens needs the secret +- **Key rotation**: Rotating keys requires updating all services simultaneously +- **Compromise risk**: If one service is compromised, the secret is exposed + +## RS256 (RSA with SHA-256) + +RS256 is an asymmetric algorithm that uses an RSA key pair: a private key for signing and a public key for verification. + +### How it works + +1. Uitsmijter generates an RSA key pair (2048-bit) +2. Uitsmijter signs JWTs with the **private key** (kept secret) +3. Uitsmijter publishes the **public key** via the JWKS endpoint (`/.well-known/jwks.json`) +4. Resource servers fetch the public key from JWKS +5. Resource servers verify JWTs using the public key (no secrets needed) + +### Configuration + +Enable RS256 by setting the `JWT_ALGORITHM` environment variable: + +```yaml +# .env or deployment config +JWT_ALGORITHM: RS256 +``` + +That's it! Uitsmijter will automatically: +- Generate RSA key pairs on startup +- Publish public keys at `/.well-known/jwks.json` +- Include `kid` (Key ID) in JWT headers +- Support key rotation + +You do **not** need to manually generate or manage RSA keys. + +### When to use RS256 (Recommended) + +- **Production deployments**: Superior security and key management +- **Microservices architecture**: Each service can verify tokens independently +- **Multi-tenant systems**: Different tenants can have different keys +- **Compliance requirements**: Many standards require asymmetric signing +- **Key rotation**: Seamless rotation without service disruption + +### Security advantages + +- **Private key protection**: Private keys never leave Uitsmijter +- **Public key distribution**: Public keys can be shared openly (via JWKS) +- **No shared secrets**: Resource servers don't need confidential data +- **Key rotation**: Old keys remain in JWKS during grace period +- **Compromise mitigation**: Compromising a resource server doesn't expose signing keys + +## Migrating from HS256 to RS256 + +### Zero-Downtime Migration Strategy + +This migration strategy allows you to switch from HS256 to RS256 without invalidating existing tokens or causing downtime. + +#### Step 1: Understand the impact + +**What changes:** +- JWT signing algorithm changes from HS256 to RS256 +- JWT header includes `kid` field for key identification +- Public keys become available at `/.well-known/jwks.json` +- Resource servers must fetch public keys from JWKS (instead of using shared secret) + +**What stays the same:** +- JWT payload structure (claims remain unchanged) +- Token expiration times +- OAuth endpoints and flows +- Client applications (if using standard OAuth libraries) + +#### Step 2: Update resource servers first + +Before switching Uitsmijter to RS256, update all resource servers to support JWKS-based verification. Most JWT libraries support this with minimal changes. + +**Example: Node.js with `jsonwebtoken` and `jwks-rsa`** + +Before (HS256): +```javascript +import jwt from 'jsonwebtoken'; + +const secret = process.env.JWT_SECRET; + +// Verify token +jwt.verify(token, secret, { algorithms: ['HS256'] }, (err, decoded) => { + // ... +}); +``` + +After (RS256 with JWKS): +```javascript +import jwt from 'jsonwebtoken'; +import jwksClient from 'jwks-rsa'; + +const client = jwksClient({ + jwksUri: 'https://id.example.com/.well-known/jwks.json', + cache: true, + cacheMaxAge: 3600000 // 1 hour +}); + +function getKey(header, callback) { + client.getSigningKey(header.kid, (err, key) => { + const signingKey = key.getPublicKey(); + callback(null, signingKey); + }); +} + +// Verify token (works with both HS256 and RS256) +jwt.verify(token, getKey, { algorithms: ['HS256', 'RS256'] }, (err, decoded) => { + // ... +}); +``` + +**Key points:** +- Keep `HS256` in the `algorithms` array temporarily (supports both algorithms) +- The JWKS client will automatically fetch and cache public keys +- Works with HS256 tokens (falls back to cached secret) and RS256 tokens (uses JWKS) + +#### Step 3: Deploy updated resource servers + +Deploy the updated resource servers that support JWKS. Verify that they can still validate existing HS256 tokens. + +Test with a sample HS256 token: +```bash +curl -H "Authorization: Bearer YOUR_HS256_TOKEN" https://your-api.example.com/protected +``` + +The request should succeed, confirming backward compatibility. + +#### Step 4: Switch Uitsmijter to RS256 + +Update Uitsmijter's configuration to use RS256: + +**Kubernetes/Helm:** +```yaml +# values.yaml +env: + JWT_ALGORITHM: RS256 +``` + +**Docker Compose:** +```yaml +environment: + - JWT_ALGORITHM=RS256 +``` + +**Direct deployment:** +```bash +export JWT_ALGORITHM=RS256 +``` + +#### Step 5: Restart Uitsmijter + +Restart Uitsmijter to apply the new configuration: + +```bash +# Kubernetes +kubectl rollout restart deployment/uitsmijter + +# Docker Compose +docker-compose restart uitsmijter +``` + +Uitsmijter will: +1. Generate a new RSA key pair on startup +2. Start signing new JWTs with RS256 +3. Publish the public key at `/.well-known/jwks.json` + +#### Step 6: Verify RS256 tokens + +Test that new tokens are signed with RS256: + +1. Obtain a new access token: +```bash +# Use your OAuth flow to get a new token +curl -X POST https://id.example.com/token \ + -d grant_type=authorization_code \ + -d code=YOUR_CODE \ + -d client_id=YOUR_CLIENT_ID +``` + +2. Decode the JWT header (without verifying): +```bash +# Extract and decode the header +echo "YOUR_TOKEN" | cut -d'.' -f1 | base64 -d +``` + +Expected output: +```json +{ + "alg": "RS256", + "typ": "JWT", + "kid": "2024-11-08" +} +``` + +3. Verify the token works with your resource servers: +```bash +curl -H "Authorization: Bearer YOUR_RS256_TOKEN" https://your-api.example.com/protected +``` + +#### Step 7: Wait for HS256 tokens to expire + +Old HS256 tokens remain valid until they expire (typically 2 hours by default). During this grace period: +- New tokens are signed with RS256 +- Old HS256 tokens continue to work +- Resource servers support both algorithms + +**Monitor token expiration:** +```bash +# Check when the last HS256 token will expire +# Default token lifetime is 2 hours +``` + +#### Step 8: Remove HS256 support (optional) + +After all HS256 tokens have expired (wait at least `TOKEN_EXPIRATION_IN_HOURS` Γ— 2), you can remove HS256 support from resource servers: + +```javascript +// Remove 'HS256' from algorithms array +jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => { + // Now only accepts RS256 tokens +}); +``` + +You can also remove the `JWT_SECRET` environment variable from resource servers (no longer needed). + +### Rollback Strategy + +If you encounter issues during migration, you can rollback to HS256: + +1. Change `JWT_ALGORITHM` back to `HS256` (or remove it) +2. Restart Uitsmijter +3. New tokens will be signed with HS256 again +4. RS256 tokens issued during the RS256 period will fail verification after rollback + +**Important:** Plan the migration during a maintenance window or low-traffic period to minimize impact. + +## Key Rotation (RS256 only) + +With RS256, you can rotate signing keys without downtime: + +### Manual key rotation + +1. Generate a new key by restarting Uitsmijter +2. The new key gets a new `kid` (current date: `YYYY-MM-DD`) +3. New JWTs are signed with the new key +4. Old public keys remain in JWKS for verification +5. After grace period, old keys can be removed from JWKS + +### Automatic key rotation + +Uitsmijter doesn't currently implement automatic key rotation, but you can implement it using: + +1. **Scheduled restarts**: Restart Uitsmijter monthly/quarterly (generates new key) +2. **External key management**: Use Kubernetes secrets rotation +3. **Manual rotation**: Generate new key via admin endpoint (future feature) + +### Best practices for key rotation + +- **Grace period**: Keep old keys in JWKS for at least 2Γ— token lifetime +- **Monitoring**: Monitor JWT verification failures during rotation +- **Documentation**: Document which `kid` is active at any time +- **Testing**: Test rotation in staging before production + +## Troubleshooting + +### "Invalid signature" errors after switching to RS256 + +**Cause**: Resource servers are still trying to verify RS256 tokens with HS256 secret. + +**Solution**: Ensure resource servers are updated to use JWKS (Step 2 of migration guide). + +### JWKS endpoint returns empty `keys` array + +**Cause**: `JWT_ALGORITHM` is still set to HS256 or not set. + +**Solution**: Verify `JWT_ALGORITHM=RS256` is set and restart Uitsmijter. + +### "kid not found in JWKS" errors + +**Cause**: Resource server's JWKS cache is stale, or key was rotated. + +**Solution**: +- Clear JWKS cache (most libraries auto-refresh) +- Verify JWKS endpoint contains the `kid` from the JWT header +- Check that clocks are synchronized (NTP) + +### Performance degradation after switching to RS256 + +**Cause**: RS256 is slightly slower than HS256 (asymmetric crypto overhead). + +**Solution**: +- Enable JWKS caching in resource servers (default: 1 hour) +- Use CDN or caching proxy for JWKS endpoint +- Consider increasing token expiration time to reduce token issuance frequency + +### Resource server can't reach JWKS endpoint + +**Cause**: Network policy, firewall, or DNS issue. + +**Solution**: +- Verify resource server can reach `https://id.example.com/.well-known/jwks.json` +- Check network policies allow outbound HTTPS +- Use internal DNS or service discovery if applicable + +## Environment Variables + +### JWT_ALGORITHM + +Controls the JWT signing algorithm. + +**Values:** +- `HS256` (default): HMAC with SHA-256 (symmetric) +- `RS256`: RSA with SHA-256 (asymmetric) + +**Example:** +```yaml +JWT_ALGORITHM: RS256 +``` + +### JWT_SECRET + +(HS256 only) The shared secret used for HS256 signing. + +**Requirements:** +- Minimum 256 bits (32 characters) +- Must be kept confidential +- Must match on all services verifying tokens + +**Example:** +```yaml +JWT_SECRET: your-secret-key-at-least-32-characters-long +``` + +**Not used when `JWT_ALGORITHM=RS256`**. + +### TOKEN_EXPIRATION_IN_HOURS + +Controls JWT access token expiration time. + +**Default:** `2` (2 hours) + +**Example:** +```yaml +TOKEN_EXPIRATION_IN_HOURS: 8 +``` + +Affects: +- Access token lifetime +- Grace period for key rotation (should be 2Γ— this value) + +## Further Reading + +- [RFC 7517 - JSON Web Key (JWK)](https://www.rfc-editor.org/rfc/rfc7517) +- [RFC 7518 - JSON Web Algorithms (JWA)](https://www.rfc-editor.org/rfc/rfc7518) +- [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) +- [Available Endpoints](/oauth/endpoints) +- [JWT Decoding](/oauth/jwt_decoding) diff --git a/content/oauth/endpoints.md b/content/oauth/endpoints.md index a50d5a5..5fff060 100644 --- a/content/oauth/endpoints.md +++ b/content/oauth/endpoints.md @@ -171,6 +171,93 @@ 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. +### /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). + +**Why use token revocation?** + +- **Security**: Proactively invalidate tokens when they're no longer needed (user logs out, app uninstalled) +- **Privacy**: Allow users to revoke access granted to third-party applications +- **Best Practice**: Recommended by OAuth 2.0 Security Best Current Practice + +**Request format:** + +The revocation endpoint accepts HTTP POST requests with `application/x-www-form-urlencoded` content type: + +```shell +curl --request POST \ + --url https://id.example.com/revoke \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data 'token=V7vZQbJNNY7zR8IWyV7vZQbJNNY7zR8IW' \ + --data 'token_type_hint=access_token' \ + --data 'client_id=9095A4F2-35B2-48B1-A325-309CA324B97E' \ + --data 'client_secret=secret123' +``` + +**Parameter description:** + +| Parameter | Required | Description | +|-----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| token | Yes | The token value to revoke. Can be either an access token (JWT) or refresh token. | +| token_type_hint | No | Hint about the token type: `access_token` or `refresh_token`. Helps the server optimize token lookup. If the hint is incorrect, the server will search across all token types. | +| client_id | Yes | The unique identifier of the client application making the revocation request. | +| client_secret | Optional | The client secret. REQUIRED for confidential clients (clients with a configured secret). MUST NOT be provided for public clients. The same authentication rules as the token endpoint apply. | + +**Response:** + +Per RFC 7009, the authorization server responds with HTTP 200 OK regardless of whether the token was valid, already revoked, or never existed. This prevents information disclosure about token validity. + +```http +HTTP/1.1 200 OK +``` + +If client authentication fails, the server returns HTTP 401 Unauthorized: + +```http +HTTP/1.1 401 Unauthorized +Content-Type: application/json + +{ + "error": "invalid_client", + "error_description": "ERROR.INVALID_CLIENT" +} +``` + +**Token ownership validation:** + +The server validates that the token belongs to the requesting client before revoking it. If a client tries to revoke a token that belongs to a different client, the revocation is silently ignored (returns 200 OK without revoking the token). + +**Cascading revocation:** + +When revoking a refresh token, Uitsmijter also revokes the associated authorization code. This prevents the client from using the authorization code to obtain new tokens after the refresh token has been revoked. + +> **Note about JWT access tokens**: Access tokens issued by Uitsmijter are stateless JWTs. While the revocation endpoint accepts and validates access tokens, it cannot truly invalidate them before their expiration time. Future enhancements may include a token blacklist or reduced token lifetimes for revoked tokens. + +**Example usage in an application:** + +```javascript +// When user logs out, revoke the refresh token +async function logout(refreshToken, clientId, clientSecret) { + await fetch('https://id.example.com/revoke', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + token: refreshToken, + token_type_hint: 'refresh_token', + client_id: clientId, + client_secret: clientSecret + }) + }); + + // Clear local session + localStorage.removeItem('refresh_token'); + localStorage.removeItem('access_token'); +} +``` + ## Discovery endpoints Discovery endpoints allow OAuth/OpenID Connect clients to automatically discover the configuration and capabilities of Uitsmijter without manual configuration. This is especially useful for dynamic client registration, multi-tenant deployments, and maintaining compatibility across different versions. @@ -210,6 +297,7 @@ This returns a JSON document with the OpenID Provider Metadata: "token_endpoint": "https://id.example.com/token", "userinfo_endpoint": "https://id.example.com/userinfo", "jwks_uri": "https://id.example.com/.well-known/jwks.json", + "revocation_endpoint": "https://id.example.com/revoke", "scopes_supported": ["openid", "profile", "email", "read", "write"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], @@ -258,6 +346,151 @@ The library will automatically fetch the discovery document and configure itself > **Note**: The discovery endpoint is publicly accessible and does not require authentication. This is by design, as clients need to discover the configuration before they can authenticate. +### /.well-known/jwks.json + +The `/.well-known/jwks.json` endpoint provides the JSON Web Key Set (JWKS) containing public keys used to verify JWT signatures. This endpoint implements [RFC 7517 (JSON Web Key)](https://www.rfc-editor.org/rfc/rfc7517) and is essential for clients that need to verify JWT access tokens signed with asymmetric algorithms. + +**Why use JWKS?** + +When Uitsmijter signs JWTs with the RS256 algorithm (RSA with SHA-256), clients need access to the public key to verify the JWT signature. The JWKS endpoint provides these public keys in a standardized JSON format that can be: + +1. **Automatically fetched**: OAuth libraries can automatically download and cache public keys +2. **Rotated safely**: Uitsmijter can rotate signing keys while keeping old keys available during a grace period +3. **Multi-key support**: Multiple active keys can coexist, identified by their `kid` (Key ID) +4. **Standards-compliant**: Works with all standard JWT libraries and validators + +**When is JWKS used?** + +- **RS256 JWT verification**: When Uitsmijter is configured with `JWT_ALGORITHM=RS256` (recommended for production) +- **Token validation**: Resource servers verify access token signatures without contacting Uitsmijter +- **Stateless authentication**: JWTs can be validated entirely client-side using the public key + +**Example**: Fetching the JWKS + +```shell +curl --request GET \ + --url https://id.example.com/.well-known/jwks.json \ + --header 'Accept: application/json' +``` + +This returns a JSON Web Key Set containing one or more RSA public keys: + +```json +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "2024-11-08", + "alg": "RS256", + "n": "0vx7agoebGcQSu...V_3Qb", + "e": "AQAB" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "2024-11-01", + "alg": "RS256", + "n": "xjlCRBqkO8WJ...kQb", + "e": "AQAB" + } + ] +} +``` + +**JWKS Response fields:** + +| Field | Description | +|-------|-------------| +| `keys` | Array of JSON Web Keys. Multiple keys support key rotation. | +| `kty` | Key Type. Always "RSA" for Uitsmijter's asymmetric keys. | +| `use` | Public Key Use. Always "sig" (signature verification). | +| `kid` | Key ID. Matches the `kid` field in JWT headers for key selection. Format: `YYYY-MM-DD`. | +| `alg` | Algorithm. Always "RS256" (RSA Signature with SHA-256). | +| `n` | RSA Modulus. The public key modulus, base64url-encoded. | +| `e` | RSA Exponent. The public key exponent, base64url-encoded. Usually "AQAB" (65537). | + +**Caching:** + +The JWKS endpoint includes caching headers to improve performance: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 +Cache-Control: public, max-age=3600 +``` + +Clients should cache the JWKS for up to 1 hour (3600 seconds) and only re-fetch when: +- The cache expires +- A JWT contains a `kid` that's not in the cached JWKS +- Key validation fails (may indicate key rotation) + +**Verifying JWTs with JWKS:** + +Most JWT libraries support automatic JWKS fetching. Example with `jsonwebtoken` (Node.js): + +```javascript +import jwt from 'jsonwebtoken'; +import jwksClient from 'jwks-rsa'; + +const client = jwksClient({ + jwksUri: 'https://id.example.com/.well-known/jwks.json', + cache: true, + cacheMaxAge: 3600000 // 1 hour +}); + +function getKey(header, callback) { + client.getSigningKey(header.kid, (err, key) => { + const signingKey = key.getPublicKey(); + callback(null, signingKey); + }); +} + +// Verify a JWT +jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => { + if (err) { + console.error('Invalid token:', err); + } else { + console.log('Valid token:', decoded); + } +}); +``` + +**Key rotation:** + +When Uitsmijter rotates signing keys: + +1. A new key is generated with a new `kid` (based on the current date) +2. The new key becomes the active signing key for new JWTs +3. Old keys remain in the JWKS for a grace period (e.g., 7-30 days) +4. Clients can verify both old and new JWTs during the rotation period +5. Expired keys are eventually removed from the JWKS + +This ensures zero-downtime key rotation without invalidating existing JWTs. + +**Algorithm selection:** + +Uitsmijter supports two JWT signing algorithms: + +- **HS256** (HMAC with SHA-256): Symmetric algorithm using a shared secret. Default for backward compatibility. JWKS not used. +- **RS256** (RSA with SHA-256): Asymmetric algorithm using RSA key pairs. **Recommended for production.** Requires JWKS. + +To enable RS256 and JWKS, set the environment variable: + +```yaml +JWT_ALGORITHM: RS256 +``` + +When `JWT_ALGORITHM=HS256` (or not set), the JWKS endpoint still returns a valid (though empty or legacy) response for compatibility, but HS256 tokens don't require JWKS for verification. + +> **Security recommendation**: Use RS256 in production. It provides better security because: +> - Public keys can be distributed safely (via JWKS) +> - Private keys never leave the authorization server +> - Resource servers can verify tokens without sharing secrets +> - Key rotation is safer and more manageable + +> **Note**: The JWKS endpoint is publicly accessible and does not require authentication. Public keys are safe to distributeβ€”they can only verify signatures, not create them. + ## Profile endpoints ### /token/info (UserInfo Endpoint) @@ -348,6 +581,8 @@ well as about the overall system status over time. | `uitsmijter_authorize_attempts` | Histogram of OAuth authorization attempts regardless of result (success/failure). | | `uitsmijter_oauth_success` | Counter of successful OAuth token authorizations (all grant types). | | `uitsmijter_oauth_failure` | Counter of failed OAuth token authorizations (all grant types). | +| `uitsmijter_revoke_success` | Counter of successful token revocations. | +| `uitsmijter_revoke_failure` | Counter of failed token revocations (authentication failures). | | `uitsmijter_token_stored` | Histogram of valid refresh tokens over time. | | `uitsmijter_tenants_count` | Gauge of the current number of managed tenants. | | `uitsmijter_clients_count` | Gauge containing the current number of managed clients for all tenants. |