Repository navigation
09 OIDC Authentication
The MQTTProbe web host (self-hosted web binary and Docker) can sign users in through an OpenID Connect (OIDC) identity provider instead of a local password. mqttprobe reads one claim from the ID token and admits the user only when its value matches what you configured. There is no "allow everyone" fallback.
Authentication applies only to the web-based app. The Windows, macOS, Linux, and Android apps do not have app authentication. OIDC is one of the web host's sign-in options; the web host can also use the local password described in Getting Started: First-run setup.
| Setting | Behavior |
|---|---|
Local (default) |
One web-host admin account created at first run on /Setup. The web login page takes a username and password. |
OIDC |
The login page shows a single "Sign in with …" button and hands off to your provider. /Setup is skipped. |
The mode value is case-insensitive: OIDC, Oidc, and oidc all work. Any other value stops the app at startup with Unknown authentication mode '<value>'. Expected 'Local' or 'OIDC'.
The two modes are independent. Turning on OIDC does not delete or change a stored local password, and turning it back off returns you to the same local account. In OIDC mode the login page never validates a local password and never redirects to /Setup.
On sign-in, mqttprobe:
- Runs the authorization code flow with PKCE (
S256) and requests exactly two scopes,openidandprofile. It does not call the userinfo endpoint. - Requires the ID token to carry non-empty
iss(issuer) andsub(subject) claims. A token missing either one is denied. - Reads the claim named by
AdmissionClaimstraight from the token payload. - Compares the value against
AcceptedValues, or checks every entry ofAcceptedValuesagainst the claim if the claim is an array. - Admits the user when anything matches, and otherwise shows the Access Denied page.
Claim value rules:
| Claim shape in the ID token | Result |
|---|---|
| A string equal to an accepted value | Admitted |
| An array of strings where any element matches | Admitted |
| An array containing any non-string element | Denied |
Missing, null, a number, a boolean, or an object |
Denied |
Both the claim name and the value comparison are case-sensitive and exact. groups and Groups are different claim names, and mqttprobe-admins does not match Mqttprobe-Admins.
A token that repeats the same top-level property name is also rejected. JSON parsers normally keep the last value silently, which would make the signed content ambiguous.
Signing in again while already signed in revokes the previous mqttprobe session first, so a browser never accumulates overlapping sessions.
The Access Denied page shows the user's display name, resolved in this order: name, then preferred_username, then sub. The URL carries a random single-use handle rather than the name or the token, and it stops resolving after five minutes or one use.
All OIDC settings live under the Authentication section.
{
"Authentication": {
"Mode": "OIDC",
"Oidc": {
"Authority": "https://idp.example.com/realms/myrealm",
"ClientId": "mqttprobe",
"ClientSecret": "your-client-secret",
"ProviderDisplayName": "Keycloak",
"AdmissionClaim": "groups",
"AcceptedValues": ["/mqttprobe-admins"],
"PublicBaseUrl": "https://mqtt.example.com"
}
}
}In Docker Compose you can set these as environment variables instead of editing appsettings.json. Arrays use the double-underscore index syntax:
services:
mqttprobe:
image: bluegrassiot/mqttprobe
environment:
Authentication__Mode: OIDC
Authentication__Oidc__Authority: https://idp.example.com/realms/myrealm
Authentication__Oidc__ClientId: mqttprobe
Authentication__Oidc__ClientSecret: your-client-secret
Authentication__Oidc__ProviderDisplayName: Keycloak
Authentication__Oidc__AdmissionClaim: groups
Authentication__Oidc__AcceptedValues__0: /mqttprobe-admins
Authentication__Oidc__PublicBaseUrl: https://mqtt.example.comIncrement the index for more accepted values:
Authentication__Oidc__AcceptedValues__0: /mqttprobe-admins
Authentication__Oidc__AcceptedValues__1: /mqttprobe-operators| Setting | Description |
|---|---|
Mode |
OIDC (case-insensitive). Local or anything else disables OIDC. |
Authority |
The provider's issuer URL. An absolute HTTPS URI. A path is allowed, for example https://idp.example.com/realms/myrealm. No userinfo, query string, or fragment. |
ClientId |
The OIDC client ID registered with your provider. |
ClientSecret |
The client secret for a confidential client. |
ProviderDisplayName |
Name shown on the login and Access Denied pages, for example Keycloak. The login button reads "Sign in with …". |
AdmissionClaim |
The ID token claim name to check. Case-sensitive. |
AcceptedValues |
A non-empty array of strings. Blank or whitespace-only entries are rejected. |
| Setting | Description |
|---|---|
PublicBaseUrl |
The HTTPS origin browsers use to reach mqttprobe, for example https://mqtt.example.com. Origin only, no path. If you omit it, AllowedHosts must name your real hostnames instead of a wildcard. |
Settings are validated at startup. If any rule is broken the app refuses to start and logs each problem:
| Message | Cause |
|---|---|
Unknown authentication mode '<value>'. Expected 'Local' or 'OIDC'. |
Mode is neither Local nor OIDC. |
Authority is required in OIDC mode. |
Authority missing or blank. |
ClientId is required in OIDC mode. |
ClientId missing or blank. |
ClientSecret is required in OIDC mode. |
ClientSecret missing or blank. |
ProviderDisplayName is required in OIDC mode. |
ProviderDisplayName missing or blank. |
AdmissionClaim is required in OIDC mode. |
AdmissionClaim missing or blank. |
At least one AcceptedValue is required in OIDC mode. |
AcceptedValues is empty. |
AcceptedValues must not contain blank entries. |
An accepted value is empty or whitespace. |
Authority '<value>' is not a valid absolute URI. |
Authority is not an absolute URI. |
Authority '<value>' must use HTTPS. |
Authority uses http://. |
Authority '<value>' must not contain userinfo. / … a query string. / … a fragment.
|
Authority carries that URI part. |
PublicBaseUrl '<value>' must use HTTPS. and the matching is not a valid absolute URI, must not contain userinfo, must not contain a query string, must not contain a fragment
|
PublicBaseUrl breaks the same rules. |
PublicBaseUrl '<value>' must not contain a path (subpath hosting is deferred). |
PublicBaseUrl includes a path. Hosting mqttprobe under a subpath is not supported. |
AllowedHosts must be configured (not wildcard) when PublicBaseUrl is not set in OIDC mode. |
PublicBaseUrl omitted and AllowedHosts is blank, *, 0.0.0.0, ::, or [::]. |
HTTPS is mandatory in OIDC mode. Authority and PublicBaseUrl must be HTTPS and the authorization request must arrive over HTTPS, so put a TLS-terminating reverse proxy in front of mqttprobe. See Docker Deployment: Add TLS.
mqttprobe builds the exact redirect_uri and post_logout_redirect_uri values from its own public origin, so it needs to know that origin.
"PublicBaseUrl": "https://mqtt.example.com"This works even when AllowedHosts is a wildcard, and it removes any ambiguity about which origin the proxy presents to browsers.
Omit PublicBaseUrl and mqttprobe derives the origin from the incoming request's scheme and Host header. That requires:
-
AllowedHostsset to your actual hostname(s), not a wildcard. Multiple hosts are semicolon-separated. - The request arriving over HTTPS. Plain HTTP fails at request time with
Request-derived origin must use HTTPS in OIDC mode. -
PublicBaseUrlabsent andAllowedHostsnon-restrictive, which fails at request time withPublicBaseUrl must be set or AllowedHosts must be configured when using OIDC mode.
"AllowedHosts": "mqtt.example.com;mqtt-backup.example.com"mqttprobe reads X-Forwarded-Proto and X-Forwarded-Host through the standard ASP.NET Core forwarded-headers middleware. Make sure your proxy sets both, and tell mqttprobe which proxies to trust:
| Setting | Purpose |
|---|---|
ForwardedHeaders:ForwardLimit |
How many proxy hops to unwrap. Defaults to 1. |
ForwardedHeaders:KnownProxies |
Proxy IP addresses to trust. |
ForwardedHeaders:KnownNetworks |
Proxy IP networks to trust. |
docker-compose.prod.yml sets these for the Caddy network. mqttprobe has no separate OIDC-specific proxy setting.
| Path | Purpose |
|---|---|
/Login |
Login page. Anonymous. |
/Logout |
Logout. POST only, antiforgery-protected. |
/Setup |
Local-mode first-run password setup. Never reached in OIDC mode. |
/AccessDenied |
Shown when the admission claim does not match. |
/health |
Health probe. Anonymous. |
/signin-oidc |
Authorization callback. Register this as a redirect URI. |
/signout-callback-oidc |
Post-logout callback. Register this as a post-logout redirect URI. |
/signout-oidc |
Provider-initiated front-channel sign-out. Browser-facing, provided by the OIDC middleware. Do not register it as a redirect URI. |
/oidc/backchannel-logout |
Provider-initiated back-channel sign-out. Server-to-server POST. See below. |
Every other page requires authentication.
Cookie behavior differs by mode:
| OIDC mode | Local mode | |
|---|---|---|
HttpOnly |
Yes | Yes |
Secure |
Always | Same as request |
SameSite |
Lax |
Strict |
| Expiry | 8 hours, absolute | 8 hours, sliding |
Lax in OIDC mode is what lets the cookie survive the cross-site redirect back from the provider. The OIDC correlation and nonce cookies are SameSite=None with Secure for the same reason.
OIDC mode also keeps a server-side session record keyed by an ID in the cookie. Every request revalidates the cookie principal against that record, and a record that is gone or altered rejects the principal and signs the user out. Signing in again, signing out, or a back-channel logout revokes the record and stops that session's MQTT connections and emulators.
Login POSTs are rate limited per client IP: 5 per minute, then HTTP 429 with Too many login attempts. Please try again later.
Signing out is a POST to /Logout and happens in this order:
- The app session is revoked, which stops that session's MQTT connections and emulators.
- The local cookie is cleared before any call to the provider, so an unreachable provider can never leave an authenticated browser session.
- mqttprobe reads the provider's discovery document. If that fails, or the provider publishes no
end_session_endpoint, the user lands back on/Loginwith a message saying they are signed out of mqttprobe but the provider session could not be ended. - Otherwise the browser is redirected to the provider's end-session endpoint. mqttprobe sends
post_logout_redirect_uriset to your origin plus/signout-callback-oidc, anid_token_hintwhen an ID token is present in the session, andclient_idotherwise so the provider can still identify the relying party.
mqttprobe does not retain refresh tokens or access tokens, so there is no background token refresh and no provider-side revocation to wait on.
Two separate paths handle the provider ending a session.
The provider redirects the browser to /signout-oidc after a user-initiated logout at the provider. This is the OIDC middleware's own browser-facing route and is separate from the callbacks above. It is not a redirect URI and not a post-logout redirect URI.
The provider makes a server-to-server POST to revoke sessions without involving a browser. This endpoint is registered only in OIDC mode.
Request shape:
| Property | Value |
|---|---|
| Method |
POST only. Anything else returns 405. |
| Content type | Form-encoded. Anything else returns 415. |
| Body field | Exactly one logout_token form value. |
| Body size | 64 KiB maximum, enforced while reading. Larger returns 413. |
The endpoint is anonymous and skips antiforgery, so every decision comes from the signed logout token. It accepts a token only when all of the following hold:
| Requirement | Detail |
|---|---|
| Signature | Verified against the provider's published signing keys. Asymmetric algorithms only: RS256/384/512, PS256/384/512, ES256/384/512. none and all HS* algorithms are refused outright. |
iss |
Matches the trusted issuer from the discovery document. |
aud |
Contains your ClientId. |
iat and exp
|
Both present. exp must be in the future, later than iat, within 30 minutes of iat, and iat no more than 10 minutes old. Two minutes of clock skew is allowed. |
jti |
Present and non-empty. Used for replay protection. |
events |
Contains a member whose type is http://schemas.openid.net/event/backchannel-logout. |
nonce |
Must be absent. A logout token carrying a nonce is rejected. |
| Session claim | At least one of sub or sid. |
| JSON shape | No duplicate property names at any level. |
An accepted token revokes every matching mqttprobe session, matching on sid when present and falling back to sub. The jti is then remembered until the token's exp, so the same token cannot be replayed. Metadata is refreshed at most once a minute, so a token with an unknown key ID cannot turn every POST into a fetch against your provider.
Responses:
| Status | Meaning |
|---|---|
| 200 | Token accepted and sessions revoked (including the case where nothing matched). |
| 400 | Token rejected. The reason is logged, not returned, because the caller is unauthenticated. |
| 405 / 413 / 415 | Wrong method, body too large, or wrong content type. |
| 500 | The token was valid but session revocation did not complete. |
Register the endpoint with your provider as a back-channel logout URI:
- Keycloak: set the client attribute Backchannel logout URL to the endpoint, and turn on Backchannel logout session required.
-
Authentik: set the provider's Logout URI to the endpoint and Logout Method to
backchannel.
The provider reaches this URL server to server, so it must be reachable from the provider, not only from a user's browser. In Docker, use the in-network address such as http://mqttprobe:8080/oidc/backchannel-logout. Keep the browser-facing redirect and post-logout URLs on the public HTTPS origin.
Most providers can put group membership, a role, or a user attribute into the ID token. All three work; they differ in what the value looks like.
| Source | Typical claim | Value shape | Watch out for |
|---|---|---|---|
| Group membership | groups |
Array of strings | Keycloak's group mapper with Full group path on emits a leading slash, /mqttprobe-admins. |
| User attribute | anything you choose, for example mqttprobe_access
|
String or array | You maintain the value per user by hand. |
| Expression or role mapping | anything you choose | String or array | Best fit when admission depends on more than one fact. |
Because mqttprobe requests only openid and profile, any custom mapping has to be attached to one of those scopes. A mapping hung on a scope mqttprobe never asks for is silently never evaluated.
These notes describe Keycloak 26.x. Field names move between versions, so match on the label rather than the exact wording.
- In the admin console, select or create a realm, for example
mqttprobe. - Go to Clients and click Create client.
- Set Client type to OpenID Connect and Client ID to
mqttprobe, then click Next. - On the Capability config step turn Client authentication on, leave Authorization off, and under Authentication flow check only Standard flow. Leave Direct access grants and Service account roles off.
- On the Login settings step set:
-
Valid redirect URIs:
https://mqtt.example.com/signin-oidc -
Valid post logout redirect URIs:
https://mqtt.example.com/signout-callback-oidc -
Web origins:
https://mqtt.example.com
-
Valid redirect URIs:
- Click Save.
- Open the Advanced tab and set Proof Key for Code Exchange Code Challenge Method to
S256. Leave Implicit flow off. - Open the Credentials tab and copy the Client Secret.
The Authority is the realm issuer URL:
https://keycloak.example.com/realms/mqttprobe
Confirm it by opening https://keycloak.example.com/realms/mqttprobe/.well-known/openid-configuration and reading the issuer field. It has to match exactly.
- Go to Groups and create a group, for example
mqttprobe-admins. - Add the users who should have access.
- Go to Clients > mqttprobe > Client scopes > the dedicated scope > Configure a new mapper.
- Choose Group Membership (
oidc-group-membership-mapper) and set:-
Token Claim Name:
groups - Full group path: on
- Add to ID token: on
- Add to access token: off
- Add to userinfo: off
-
Token Claim Name:
- Save the mapper.
With Full group path on, the claim value carries a leading slash, so the accepted value is /mqttprobe-admins.
Do not use a built-in admin or role mapper that grants access to every user. That defeats the point of the check.
Useful when group paths are awkward or you want a value unrelated to your directory structure.
- Give each user a custom attribute, for example
mqttprobe_access, with valueadmin. - Add a User Attribute mapper (
oidc-usermodel-attribute-mapper) on the client's dedicated scope:-
User Attribute:
mqttprobe_access -
Token Claim Name:
mqttprobe_access - Add to ID token: on
-
User Attribute:
- Set
AdmissionClaimtomqttprobe_accessandAcceptedValuesto["admin"].
This is what the bundled Keycloak lab uses, so it is the fastest path to a working end-to-end test.
{
"Authentication": {
"Mode": "OIDC",
"Oidc": {
"Authority": "https://keycloak.example.com/realms/mqttprobe",
"ClientId": "mqttprobe",
"ClientSecret": "paste-the-client-secret-here",
"ProviderDisplayName": "Keycloak",
"AdmissionClaim": "groups",
"AcceptedValues": ["/mqttprobe-admins"],
"PublicBaseUrl": "https://mqtt.example.com"
}
}
}services:
mqttprobe:
image: bluegrassiot/mqttprobe
environment:
Authentication__Mode: OIDC
Authentication__Oidc__Authority: https://keycloak.example.com/realms/mqttprobe
Authentication__Oidc__ClientId: mqttprobe
Authentication__Oidc__ClientSecret: paste-the-client-secret-here
Authentication__Oidc__ProviderDisplayName: Keycloak
Authentication__Oidc__AdmissionClaim: groups
Authentication__Oidc__AcceptedValues__0: /mqttprobe-admins
Authentication__Oidc__PublicBaseUrl: https://mqtt.example.comAdmitted user:
- Add a user to the accepted group (or give them the accepted attribute value).
- Open
https://mqtt.example.com. - Click Sign in with Keycloak and enter the user's credentials.
- You land back on the MQTTProbe main page.
Denied user:
- Use a user who authenticates successfully but lacks the accepted value.
- Sign in through the provider.
- You land on the Access Denied page showing the user's display name.
Logout:
- While signed in, use the logout button.
- The provider's logout page appears, then
/signout-callback-oidc, then the login page. - The browser Back button does not restore the session.
- Go to Applications > Providers and click Create.
- Choose OAuth2/OpenID Provider.
- Configure:
-
Name:
mqttprobe - Authorization flow: the default provider authorization flow is fine
- Client type: Confidential
-
Client ID:
mqttprobe - Client Secret: copy the generated secret
-
Redirect URIs: two entries
-
https://mqtt.example.com/signin-oidc, type Authorization -
https://mqtt.example.com/signout-callback-oidc, type Logout
-
- Signing Key: the default signing key
-
Name:
- Under Advanced protocol settings leave Include claims in ID token on and keep the
openidandprofilescope mappings selected. - Click Create.
The Logout-typed redirect URI is the post_logout_redirect_uri used after a sign-out you started from mqttprobe. The provider's separate Logout URI field is for back-channel logout and wants the /oidc/backchannel-logout endpoint instead.
- Go to Applications > Applications and click Create.
- Set Name to
mqttprobeand Slug tomqttprobe. The slug appears in the issuer URL. - Select the provider you just created and click Create.
Note the Issuer URL on the provider detail page:
https://authentik.example.com/application/o/mqttprobe/
That trailing slash matters. This is your Authority.
Authentik's built-in profile scope mapping includes a groups claim holding group names as a JSON array with no leading slash:
{
"groups": ["mqttprobe-admins", "other-group"]
}So with the group approach, AcceptedValues is ["mqttprobe-admins"], not ["/mqttprobe-admins"].
To gate on something else, add a custom scope mapping and attach it to the profile scope. This is required, not optional: mqttprobe requests only openid and profile, so a mapping on any other scope name is never evaluated. An expression mapping works well:
if ak_is_group_member(request.user, name="mqttprobe Users"):
return {
"mqttprobe_access": "admin",
}
return {}
Return an empty result for users who should be denied. They still authenticate successfully at the provider; mqttprobe denies them afterwards.
Whichever claim you choose, remember to log out and back in (or use a private window) after changing it, because the claim lives in a token issued at sign-in.
{
"Authentication": {
"Mode": "OIDC",
"Oidc": {
"Authority": "https://authentik.example.com/application/o/mqttprobe/",
"ClientId": "mqttprobe",
"ClientSecret": "paste-the-client-secret-here",
"ProviderDisplayName": "Authentik",
"AdmissionClaim": "groups",
"AcceptedValues": ["mqttprobe-admins"],
"PublicBaseUrl": "https://mqtt.example.com"
}
}
}services:
mqttprobe:
image: bluegrassiot/mqttprobe
environment:
Authentication__Mode: OIDC
Authentication__Oidc__Authority: https://authentik.example.com/application/o/mqttprobe/
Authentication__Oidc__ClientId: mqttprobe
Authentication__Oidc__ClientSecret: paste-the-client-secret-here
Authentication__Oidc__ProviderDisplayName: Authentik
Authentication__Oidc__AdmissionClaim: groups
Authentication__Oidc__AcceptedValues__0: mqttprobe-admins
Authentication__Oidc__PublicBaseUrl: https://mqtt.example.comAuthentik's default logout is app-scoped: the user leaves mqttprobe but stays signed in to Authentik and any other application.
To end the Authentik session too, give the provider an invalidation flow that includes a user logout stage. That is an Authentik-side setting. See the Authentik guide on enabling full single logout for RP-initiated logout.
- Add a user to the accepted group.
- Open
https://mqtt.example.comand click Sign in with Authentik. - Enter the user's credentials; you land back on the MQTTProbe main page.
- Sign out, confirm you pass through the Authentik logout page and return to
/signout-callback-oidc, and confirm Back does not restore the session. - Sign in as a user outside the accepted group: authentication succeeds and you land on the Access Denied page.
The repository ships two ready-made labs for developing and testing OIDC locally. Each starts mqttprobe configured for that provider, behind a Caddy reverse proxy with its own local CA.
| Keycloak lab | Authentik lab | |
|---|---|---|
| Directory | deploy/keycloak/ |
deploy/authentik/ |
| Also starts | Keycloak (dev database), Caddy, CA init | Authentik server and worker, PostgreSQL, Caddy, CA init |
| Identity provider | https://keycloak.localhost:8443 |
https://authentik.localhost:9443 |
| mqttprobe | https://localhost:5001 |
https://localhost:5001 |
| Authority | https://keycloak.localhost:8443/realms/mqttprobe-test |
https://authentik.localhost:9443/application/o/mqttprobe/ |
| Admission claim |
mqttprobe_access (user attribute) |
mqttprobe_access (expression mapping) |
| Accepted value | admin |
admin |
| Back-channel logout | Configured | Configured |
Each lab has a README with the quick start, test credentials, CA trust steps for your platform, smoke script, and cleanup. Run the smoke test with bash smoke.sh, or .\smoke.ps1 on Windows.
cd deploy/keycloak # or deploy/authentik
cp .env.example .env
docker compose up --build -dTwo things to know before you start:
-
Authentik takes 30 to 90 seconds to finish migrations on first start. Wait for the
serverservice to report healthy. -
Trust the Caddy root CA or every browser request fails. The lab README has the
certutil,security add-trusted-cert, andupdate-ca-certificatescommands.
Test accounts:
| Lab | Account | Password | Result |
|---|---|---|---|
| Keycloak | admitted-user |
test-password |
Admitted: the attribute value is admin. |
| Keycloak | denied-user |
test-password |
Denied: the attribute value is viewer, which is not accepted. |
| Keycloak admin console | admin |
from .env, default admin
|
Provider administration at https://keycloak.localhost:8443/admin. |
| Authentik | none pre-created | - | Use akadmin from your .env, then create users yourself. Add admitted-user to the mqttprobe Users group, or leave denied-user out of it. |
Both labs are development-only. They bind to 127.0.0.1, use test secrets, and generate an ephemeral CA that is recreated whenever you wipe the volumes.
Read the startup log. Each broken rule is reported on its own line, and the table in Fail-fast rules maps every message to its cause.
Authentication:Mode is not OIDC, or it did not take effect. In Docker, environment variable names are case-sensitive and use double underscores: Authentication__Mode.
Startup fails with AllowedHosts must be configured (not wildcard) when PublicBaseUrl is not set in OIDC mode
Either set PublicBaseUrl to your HTTPS origin, or set AllowedHosts to your real hostname(s). *, 0.0.0.0, ::, and [::] all count as non-restrictive.
Serve mqttprobe from the root of its own hostname. https://host/mqttprobe is not supported.
The registered redirect URI does not match. It has to be exactly https://your-host/signin-oidc, including scheme, host, port, and path. Trailing slashes matter. Check Valid redirect URIs in the provider.
The ID token's claim does not match. Check, in order:
-
AdmissionClaimis spelled and cased correctly (groups, notGroups). - The value matches an accepted value exactly, including case.
- Leading slashes line up. Keycloak with Full group path on emits
/mqttprobe-admins; Authentik'sgroupsclaim and user-attribute values are bare. - The claim is actually in the ID token. A mapping on a scope mqttprobe does not request is never evaluated, and mqttprobe asks only for
openidandprofile. - The token carries
issandsub, and does not repeat a top-level property name.
Inspect the token with your provider's own tooling: Keycloak has Client scope > Evaluate, and Authentik has a provider Test view. If you paste a token into an online decoder, remember that an ID token is a credential.
mqttprobe fetches the provider's discovery document before it redirects you, so an unreachable or slow provider fails fast instead of bouncing you to a broken login page. Check that Authority resolves from the mqttprobe container or host, and that the provider's TLS certificate is trusted there. The same page also shows a timeout message and a generic authentication-failed message.
mqttprobe needs the provider's discovery document to advertise an end_session_endpoint. If discovery fails or the endpoint is missing, you are signed out of mqttprobe and the login page says the provider session could not be ended. Open the provider's .well-known/openid-configuration and check for end_session_endpoint.
Rejected tokens are logged with a reason and never returned to the caller. The usual ones:
| Logged reason | Meaning |
|---|---|
duplicate_token |
That jti was already used. Expected on retries after a successful call. |
invalid_signature |
The signature did not verify. An unknown key ID triggers one metadata refresh, so a normal key rotation recovers on its own; a persistent failure means the signature really does not match the provider's keys. |
invalid_issuer / invalid_audience
|
The iss or aud does not match what mqttprobe trusts. Check Authority and ClientId. |
disallowed_algorithm |
The provider signed with none or an HS* algorithm, which mqttprobe refuses. Use an RSA, ECDSA, or PSS key. |
token_too_old / token_lifetime_too_long
|
iat is over 10 minutes old, or the token lives longer than 30 minutes. |
token_expired |
exp has passed. Check clock skew between mqttprobe and the provider. |
missing_session_claims |
Neither sub nor sid is present, so mqttprobe cannot tell which session to end. |
nonce_present |
Logout tokens must not carry a nonce. This usually means the wrong token was sent. |
invalid_events |
The events member is missing or does not contain the back-channel logout event type. |
claim_cache_full |
Too many unexpired jti values cached. Usually a provider loop. |
If the provider cannot reach the endpoint at all, nothing is logged by mqttprobe. Check that the logout URI you registered is server-reachable, and use the in-network address in Docker rather than a browser-facing one.
- Admission is all or nothing. Every admitted user is granted the built-in Admin role. There is no role mapping from provider claims to MQTTProbe roles, so treat the admission claim as the single gate: whoever matches it can do everything an administrator can, including reading broker credentials and client certificates held by the server. Size your admitted group accordingly.
- Deny by default. No claim match means no access. There is no allow-all path and no fallback to the local account while OIDC mode is on.
-
Exact redirect URIs. mqttprobe builds
redirect_uriandpost_logout_redirect_urifrom your configured origin, never from user input, which prevents open redirects into the OIDC handshake. - Local cookie cleared first. Sign-out clears the mqttprobe cookie before contacting the provider, so a broken provider cannot leave a usable session.
- No tokens retained. mqttprobe keeps no refresh or access tokens, so there is no background refresh and nothing to leak from a stolen cookie beyond the session itself.
- An 8-hour stale-authorization ceiling. The session cookie is absolute, not sliding. Removing a user from the accepted group or value does not cut off an existing session; it ends at the next sign-in, sign-out, or back-channel logout.
- Server-side session registry. Every request revalidates the cookie against a server-side record, so revoking the record signs the user out everywhere, not just in one tab.
- Provider logout is best-effort. It depends on the provider's discovery document and end-session endpoint being reachable. When it is not, mqttprobe says so instead of pretending.
-
Secrets.
ClientSecretcomes fromappsettings.jsonor the environment. In Docker prefer an environment variable or a secrets manager over baking it into an image. See Security and Storage.
- Security and Storage: where secrets live and how they are encrypted
- Docker Deployment: Compose, TLS, reverse proxy, and the OIDC labs
- Getting Started: web-host local-mode first-run setup
- OpenID Connect Core 1.0
- OpenID Connect Back-Channel Logout
- Keycloak documentation
- Authentik documentation