Your Authentik login dies on 'not all parts available' — and two different bugs produce it


I wired Forgejo up to Authentik for single sign-on. The redirect worked, Authentik authenticated me, the browser came back to the callback with a valid ?code= — and Forgejo threw a 500:

UserSignIn: oauth2: error decoding JWT token:
jws: invalid token received, not all parts available

That message is about as unhelpful as it gets. It names no field, no scope, no key. And it is thrown by two completely unrelated misconfigurations — so you can find the first one, fix it correctly, retry, and watch the exact same error come back. At which point the natural conclusion is that your fix was wrong.

It wasn’t. There was just a second one behind it.

What the error actually means

Forgejo (and Gitea) sign in through goth’s OpenID Connect provider, which decodes the id_token with roughly this:

jwtParts := strings.Split(jwt, ".")
if len(jwtParts) != 3 {
    return nil, errors.New("jws: invalid token received, not all parts available")
}

That is the whole test. Exactly three dot-separated parts, or this error. So the message really means: what I got where an id_token should be was not a three-part JWS. It could be empty. It could be something else entirely. The error does not distinguish, which is precisely the problem.

Two things commonly land you outside “three parts.”

Cause 1: the openid scope isn’t actually being granted

Without the openid scope, the request is not an OIDC request at all — it’s plain OAuth2. The provider returns an access_token and no id_token. Forgejo tries to decode an empty string, gets one part, and throws.

This needs to be right in two places, and people usually only check one:

  1. The client must request it. In Forgejo, Site Administration → Authentication Sources → your source → Additional Scopesopenid email profile. If you typed custom scopes and left openid out, you never asked.
  2. The Authentik provider must be willing to grant it. The provider’s Scopes property mappings must include authentik default OAuth Mapping: OpenID 'openid'.

Miss the second and Authentik does something quietly hostile: it intersects what you asked for with what it allows, drops the rest, and proceeds. It tells you, but only in its own log:

{
  "event": "Application requested scopes not configured, setting to overlap",
  "scope_allowed": "{'email', 'profile'}",
  "scope_given":   "{'email', 'profile', 'openid'}"
}

There’s your smoking gun. Search authentik-server logs for setting to overlap.

The check that will lie to you

Here is the part that cost me real time. The obvious way to verify the provider’s scopes is the discovery document:

GET https://auth.example.com/application/o/<slug>/.well-known/openid-configuration

I pulled it, saw this, and declared Authentik healthy:

"scopes_supported": ["openid", "profile", "email"]

It was not healthy. scopes_supported advertises openid regardless of whether the mapping is assigned to that provider — it’s OIDC’s mandatory scope, so it’s always listed. Meanwhile scope_allowed was {email, profile}. The discovery doc and the actual behaviour disagreed, and I trusted the wrong one.

Verify against the provider’s assigned property mappings, or against the log line above. Not discovery.

Cause 2: the provider has an Encryption Key set

Fix the scope, retry, same error. This is where you start doubting yourself.

An Authentik OAuth2 provider has two key fields, and they do very different things:

  • Signing Key — signs the token. Produces a JWS: header.payload.signature, three parts.
  • Encryption Keyencrypts the token. Produces a JWE: header.encrypted_key.iv.ciphertext.tag, five parts.

Set an Encryption Key and Authentik stops issuing signed tokens and starts issuing encrypted ones. Five parts. len(jwtParts) != 3. Same error, entirely different cause.

Forgejo and Gitea have no JWE support at all. Neither do most self-hosted apps — Grafana, Portainer, and friends all expect a plain signed JWT. No amount of scope fiddling will ever fix this one.

Mine had both fields set to the same certificate, which is an easy thing to do when a dropdown sits directly under another dropdown you just filled in correctly.

The ten-second diagnostic: count the dots

You don’t have to guess which cause you have. Count the separators in an issued token. If you run Authentik with Postgres:

SELECT a.expires,
       a._scope,
       length(a.token) - length(replace(a.token, '.', '')) AS dots
FROM authentik_providers_oauth2_accesstoken a
JOIN authentik_providers_oauth2_oauth2provider o
  ON o.provider_ptr_id = a.provider_id
WHERE o.client_id = '<your client id>'
ORDER BY a.expires DESC
LIMIT 5;

Read it like this:

dots meaning action
2 3 parts — JWS token shape is fine
4 5 parts — JWE clear the Encryption Key

And _scope tells you about cause 1 in the same row. If openid is missing there, that’s your problem — regardless of what discovery claims.

Before my fix, the newest row read email profile and dots=4: both bugs, visible in one query. After: email profile openid and dots=2.

The fix

  • Authentik → Providers → your provider → Advanced protocol settings → Encryption Key → blank. Leave Signing Key alone. You want signed, not encrypted; the channel is already TLS-protected, and encrypted id_tokens are for a narrow set of clients that specifically support them.
  • Provider Scopes must include the openid mapping.
  • Forgejo’s Additional Scopes: openid email profile.

One nicety: editing the auth source in Forgejo’s web UI re-registers the provider live — no container restart. You can confirm what it’s really sending by reading the redirect it generates:

curl -sS -D - http://<forgejo>:3000/user/oauth2/<source-name> | grep -i '^location:'

The scope= in that URL is the truth, not whatever the database says.

The takeaway

When one error string has multiple causes, fixing one cause looks exactly like fixing nothing. That’s the trap — not the individual misconfigurations, which are both mundane, but the way the identical symptom hides the fact that you’re making progress.

The defence is to stop reasoning from the error and find a signal with more resolution. Here that was two of them: Authentik’s own setting to overlap log line, and a dot count that distinguishes JWS from JWE in one integer. Both take seconds once you know they exist, and either one would have skipped the entire detour.

Also: when a status endpoint agrees that everything is fine and the behaviour disagrees, believe the behaviour. scopes_supported was not lying, exactly — it was answering a different question than the one I was asking it.


Hostnames, paths, identifiers and product-specific strings in this post are illustrative. The failure modes, commands and fixes are real.