Common issues

This page covers the errors you're most likely to hit while integrating Kenni and how to fix them. If you've worked through it and you're still stuck, email us — include the client_id, the time of the failure, and the full request URL or response body if you have it.

Authorization errors

These errors come back as a redirect to your redirect_uri with error=...&error_description=... in the query string.

redirect_uri did not match any registered redirect_uri

The redirect_uri on the authorization request is not on the application's allow-list.

  • Check Settings → Redirect URIs for the application. Every URI is exact-match — https://app.example.is/callback does not cover https://app.example.is/callback/.
  • Local development typically uses http://localhost:<port>/callback. Add the exact port your dev server runs on.
  • For native and Expo apps, the URI is usually a custom scheme (com.example.app:/callback). Match it down to the casing.

access_denied — Consent not enabled for this client

The application is requesting a scope that requires consent (display_name, email, picture, phone_number, …) but consent is not enabled on the application.

  • Open the application's Consent tab and tick Enable consent, then add the scopes the user must agree to.
  • If your plan doesn't support consent yet, the tab is hidden — see Consent.

access_denied — Delegation not supported

The authorization request includes prompt=delegation or prompt=delegation_admin, but the application's plan tier doesn't support either company or custom delegations.

  • Confirm your plan tier supports delegations. See Company delegations and Custom delegations.
  • If you didn't intend to ask for delegation, drop the prompt parameter from the authorization request.

access_denied — Self delegation is disabled and no delegates found for this user

The user has no company or custom delegations, and Support self-delegation is off on the application.

  • Either turn on Support self-delegation in the application's Delegations tab so users without delegations can sign in as themselves, or
  • Make sure the user has been granted the relevant company or custom delegations before they try to sign in.

access_denied — Delegate does not have permission to manage delegations

A prompt=delegation_admin request reached a user who has no grantable delegation types in the current context.

  • For personal grants, the user needs at least one custom delegation type whose Allow personal granting is on.
  • For sub-delegation, the actor must hold at least one type listed in the target type's Required delegation types. See Custom delegations → Access control.

access_denied — End-User aborted interaction

The user clicked the back button or closed the login screen. Not a configuration issue — handle it in your application by showing a friendly "Sign-in cancelled" message.

invalid_scope

The authorization request includes a scope the application is not authorized to use.

  • Custom API scopes must be enabled on the application's API Scopes tab before they can be requested.
  • Identity scopes that require consent only resolve when consent is enabled (see above).
  • Scopes are case-sensitive: Email and email are different scopes, and the second one is the right one.

Token endpoint errors

These come back as 4xx responses from POST /oidc/token with a JSON body.

invalid_client

Your client_id or client_secret didn't match Kenni's records.

  • Confirm the client_id matches what the developer portal shows for the application — the runtime form is @team-domain/short-name.
  • For confidential clients, confirm the secret. If you've lost it, rotate it from the application's Settings tab — there's no way to recover the original.
  • Authentication method matters. Web and M2M clients default to client_secret_basic (HTTP Basic Auth) or client_secret_post (form body). Check what your library is sending.

invalid_grant

The most common culprits, in order:

  • Authorization code reuse. Codes are single-use and short-lived. If your library is retrying token requests, make sure it's caching the response — not re-submitting the code.
  • PKCE mismatch. The code_verifier on the token request must hash to the code_challenge sent on the authorization request. The most common cause is reusing a stale verifier on a retry.
  • Refresh token expired or revoked. Refresh tokens have a TTL set on the application's Settings tab. Once expired, the user has to sign in again.
  • redirect_uri mismatch on the token request. The redirect_uri you send to /oidc/token must be byte-identical to the one you sent to /oidc/auth.

Refresh tokens are not issued for M2M

Machine-to-Machine clients use the client_credentials grant, which doesn't issue refresh tokens. If you need a fresh token, just call the token endpoint again with your credentials.

ID token / JWT validation failures

Signature does not validate

  • Fetch the signing keys from https://idp.kenni.is/<team-domain>/oidc/jwks and cache them with respect for Cache-Control — Kenni rotates keys periodically.
  • Don't pin keys by kid. When a key rotates, refetch JWKS and try again before failing.

iss mismatch

The iss claim is https://idp.kenni.is/<team-domain>, including the team domain. A mismatch usually means your validator is configured with the wrong issuer URL — make sure it includes the path.

aud mismatch

For ID tokens, aud is your client_id. For JWT access tokens, aud is your team's resource indicator. Configure your validator with the right value for whichever token you're checking.

Clock skew

If validation passes everywhere except exp or iat, your server's clock is probably off. Most JWT libraries accept a clockTolerance option (typically 30–60 seconds) — set it.

Scopes are silently dropped from tokens

If you request a scope and the corresponding claim doesn't show up in the ID token or user-info response, work through the list:

  1. The application isn't authorized for the scope. Custom API scopes must be enabled on the application's API Scopes tab. Identity scopes that require consent need consent enabled.
  2. The scope's subject type doesn't match. company_* scopes only release for company subjects; display_name/email/etc. only release for individual subjects. See Scopes and claims → Subject types.
  3. The user declined the scope on the consent screen. They can come back and consent later — the next time they hit the consent screen for that application, the previously-declined scopes are listed again.
  4. The user has no value for the claim. Editable claims (display_name, email, picture, …) only release if the user has set them.

The authorization request itself does not fail when a scope is unsupported — Kenni issues the token with whatever it can release. Always check the actual claims in the response, not just the scopes you asked for.

Discovery and CORS

/.well-known/openid-configuration returns 404

The discovery URL is https://idp.kenni.is/<team-domain>/.well-known/openid-configuration. The team domain is the value you set when creating the team — visible at the top of the developer portal.

CORS errors from a browser app

Kenni's token, user-info, and JWKS endpoints are CORS-enabled for SPA clients. If you're getting a CORS error:

  • Confirm your application type is Web (SPA), not Web. Server-rendered Web clients don't need CORS, but they also don't get it.
  • Confirm the request is going to https://idp.kenni.is, not a different host. Mixing in a proxy in front of Kenni typically requires you to forward the relevant headers yourself.

Session expired during sign-in

If the user pauses too long mid-login (e.g. on the consent or delegation screen), Kenni's interaction session times out and the next click renders a session expired page with a button back to your application.

This is expected — there's no value in keeping login interactions alive forever. If your users are running into it routinely, it's usually because something else (an external authenticator app, a slow network, a sleep/wake cycle on mobile) is stalling the flow. The right fix is a fresh authorization request, not a longer session.

Still stuck?

Email us with as much of the following as you can:

  • The client_id of the application.
  • The exact time of the failure (with timezone).
  • The full request URL or the response body of the failing request.
  • A brief description of what you expected to happen vs. what happened.

The more concrete the detail, the faster we can find it on our side.