all systems self-hosted

~/selfhostjournal $

← All articles

,

Authentik: “The request is otherwise malformed” after creating a provider through the API

3–5 minutes

In short

Problem: Every login through a new Authentik OAuth2/OpenID provider fails with invalid_request: “The request is otherwise malformed”.

Solution: The provider was created through the API, and its grant_types list was empty. Set it to authorization_code and refresh_token, then test the authorize URL with curl.

Tested with: Authentik 2026.8, WordPress with the OpenID Connect Generic plugin 3.11

I add most apps to Authentik through the admin interface. For this blog I wanted to script it, so I created the OAuth2 provider with a single API call. The call succeeded, the application showed up, the WordPress plugin was configured with the right client ID, secret and endpoints. And then not a single login worked.

The fix turned out to be one field. Finding it took longer than it should have, because the error message points in the wrong direction.

1. The symptom: “request is otherwise malformed”

Clicking “Login with OpenID Connect” in WordPress sent me to Authentik, and Authentik sent me straight back with an error page instead of a login form:

invalid_request
The request is otherwise malformed

No username prompt, no password prompt. Authentik refused the request before the login flow even started.

2. What I checked first (and why it misled me)

“Malformed request” sounds like the client sent something wrong. So I went through the usual suspects on the WordPress side:

  • Redirect URI: character by character against the provider. Authentik uses strict matching, so a missing slash or a different query string is enough to break it. It matched.
  • Client ID: copied again, no stray spaces.
  • Scopes: openid email profile, all three available in the provider.
  • Endpoints: taken from the provider’s .well-known/openid-configuration.

Everything was correct. The request from WordPress was fine. The problem was on the Authentik side, and the browser error doesn’t tell you that.

3. The real cause: an empty grant_types list

The Authentik server log had the more useful message. With Docker Compose:

docker compose logs server --since 10m | grep -i grant

It said “Invalid grant_type for provider”. Every OAuth2 provider in Authentik has a list of allowed grant types. When you create a provider in the admin interface, the form fills in sensible defaults. The API doesn’t: grant_types is an optional field, and if you leave it out, the provider is saved with an empty list. An empty list means no grant type is allowed, so not even the normal authorization code flow works.

My API call looked roughly like this (shortened):

curl -X POST https://id.example.com/api/v3/providers/oauth2/ \
  -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Provider for My Blog",
    "client_type": "confidential",
    "authorization_flow": "<flow uuid>",
    "invalidation_flow": "<flow uuid>",
    "redirect_uris": [
      {"matching_mode": "strict",
       "url": "https://blog.example.com/wp-admin/admin-ajax.php?action=openid-connect-authorize"}
    ],
    "signing_key": "<certificate uuid>",
    "property_mappings": ["<openid>", "<email>", "<profile>"]
  }'

Every required field is there, so the API accepts it without a warning. The one field that decides whether anyone can log in is the one that’s missing.

4. The fix

Set the grant types on the existing provider. A login flow for a website needs authorization_code, and refresh_token if the app renews its tokens:

curl -X PATCH https://id.example.com/api/v3/providers/oauth2/<provider id>/ \
  -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"grant_types": ["authorization_code", "refresh_token"]}'

You can also open the provider in the admin interface and save it there with both grant types ticked. For new providers, add the line straight to the create call:

    "grant_types": ["authorization_code", "refresh_token"],

While you’re at it, compare the token lifetimes too (access_token_validity, refresh_token_threshold) with a provider you created in the interface. API-created providers don’t always get the same values, and it’s easier to keep all providers alike.

Warning: only allow the grant types the app really uses. password and client_credentials skip the login flow, and with it your MFA and policies. A website login doesn’t need them.

5. How to verify (without a browser)

You don’t need to click through the login to test a provider. Call the authorize endpoint with curl and look at where Authentik sends you:

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
  "https://id.example.com/application/o/authorize/?client_id=<client id>&response_type=code&scope=openid&redirect_uri=https%3A%2F%2Fblog.example.com%2Fwp-admin%2Fadmin-ajax.php%3Faction%3Dopenid-connect-authorize"
  • Working: a 302 to Authentik’s own login flow, for example /if/flow/default-authentication-flow/….
  • Broken: a 302 back to your app’s redirect URI with error=… in the URL. The error name tells you what Authentik didn’t like.

Tip: the redirect URI in the curl call must be URL-encoded and match the provider exactly, otherwise you test the wrong problem. I now run this check after every provider I create, whether through the API or the interface. It takes five seconds.

What I learned

  • “Malformed request” in the browser doesn’t mean the client is wrong. Read the identity provider’s server log before you change anything in the app.
  • The admin interface and the API don’t create the same object. The interface fills in defaults, the API stores exactly what you send.
  • A curl test of the authorize URL checks a provider in seconds, without logging in.

How the blog login is set up end to end is in Single sign-on for WordPress with Authentik. All provider fields are described in the official Authentik documentation.

Everything here worked in my setup at the time of writing. Software changes, and your setup is different: back up first, test carefully, and use the steps at your own risk. Details in the legal notice.