Cloudron makes it easy to run web apps like WordPress, Nextcloud, GitLab on your server. Find out more or install now.


Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Bookmarks
  • Search
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
Brand Logo

Cloudron Forum

Offical apps | Community apps | Demo | Docs | Install
  1. Cloudron Forum
  2. Directus
  3. Directus + Cloudron SSO: "Invalid token" login loop

Directus + Cloudron SSO: "Invalid token" login loop

Scheduled Pinned Locked Moved Directus
5 Posts 3 Posters 210 Views 2 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • D
    D
    dbruehlmeier
    wrote last edited by
    #1

    TL;DR — After Cloudron's OIDC refresh token for a Directus user expires or is revoked, Directus can never log that user in again, even though Cloudron successfully authenticates them. The dead token sits in directus_users.auth_data, Directus reuses it on every login, and treats Cloudron's invalid_grant as fatal. Clearing the column fixes it instantly.

    Environment

    • Cloudron 10.1.5, official Directus app, Directus 12.5.0 on PostgreSQL
    • SSO via Cloudron's OIDC provider: AUTH_PROVIDERS=cloudron, AUTH_CLOUDRON_DRIVER=openid, issuer https://my.example.com/openid

    Symptom — Clicking "Login with Cloudron" bounces back to the login page with reason=INVALID_TOKEN. The log shows a loop:

    GET /auth/login/cloudron?redirect=...                       302
    GET /auth/login/cloudron/callback?code=...&state=...&iss=... 302
    WARN: [OpenID] Invalid grant
    OPError: invalid_grant (grant request is invalid)
        at Client.grant (openid-client/lib/client.js:1381)
        at async Client.refresh (openid-client/lib/client.js:1125)
        at async OpenIDAuthDriver.refresh (directus/api/dist/auth/drivers/openid.js:250)
        at async AuthenticationService.login (.../services/authentication.js:140)
    WARN: Invalid token.
    GET /admin/login?reason=INVALID_TOKEN                       200
    GET /auth/login/cloudron/?...&prompt=true                    302   <- automatic retry, fails again
    

    It never recovers, no matter how often you retry.

    What is not broken — the stack is the giveaway: the failing grant is Client.refresh, called from AuthenticationService.login. The authorization-code exchange (Client.callback) already succeeded — Cloudron authenticated the user, issued a valid code, and Directus identified the Directus user. So client ID, client secret, redirect URI, PKCE, scopes and the user mapping are all fine. That's why the usual advice (check redirect URI, rotate the secret, clear cookies) does nothing here.

    Root cause — api/src/auth/drivers/openid.ts:

    override async login(user: User): Promise<void> {
        return this.refresh(user);              // login re-uses the token stored last time
    }
    
    override async refresh(user: User): Promise<void> {
        if (authData?.['refreshToken']) {
            const tokenSet = await client.refresh(authData['refreshToken']); // invalid_grant -> InvalidTokenError
        }
    }
    

    and, when the user is looked up during the callback:

    auth_data: tokenSet.refresh_token && JSON.stringify({ refreshToken: tokenSet.refresh_token }),
    

    Three things combine into a permanent lockout:

    1. Cloudron's refresh token is only valid for as long as the Cloudron/OIDC session. This matches what Directus maintainers state in directus/directus#22279: once the provider session expires, the user has to log in again. That is expected behaviour.
    2. When the token has expired, the next login still calls client.refresh() with the old token before completing. invalid_grant → InvalidTokenError → login aborted, and the fresh code exchange is thrown away.
    3. auth_data is only overwritten when the code exchange returns a new refresh token. If it doesn't, the key is undefined, updateOne() strips it, and the stale token stays in the database forever. So the automatic retry with prompt=true (the callback handler's InvalidToken → ./?prompt=true redirect) can never succeed either. It's a deadlock that only a direct database update breaks.

    Immediate fix — clear the stored token for the affected user(s):

    SELECT id, email, provider, auth_data FROM directus_users WHERE provider = 'cloudron';
    UPDATE directus_users SET auth_data = NULL WHERE provider = 'cloudron';
    

    On Cloudron, the app's web terminal works:

    PGPASSWORD=$CLOUDRON_POSTGRESQL_PASSWORD psql \
      -h "$CLOUDRON_POSTGRESQL_HOST" -U "$CLOUDRON_POSTGRESQL_USERNAME" \
      -d "$CLOUDRON_POSTGRESQL_DATABASE" -c \
      "UPDATE directus_users SET auth_data = NULL WHERE provider = 'cloudron';"
    

    The next SSO login succeeds immediately. (The POST /auth/refresh 400 calls in the log are unrelated — that's the Studio retrying a dead Directus session cookie, and it clears itself once a new session exists.)

    Proposed clean fix (upstream, Directus) — two small changes, either of which removes the deadlock:

    1. Never persist a stale provider token — make the absence explicit:
    auth_data: tokenSet.refresh_token ? JSON.stringify({ refreshToken: tokenSet.refresh_token }) : null,
    
    1. Treat an unusable stored token as "no token" during login, and only fail on a genuine refresh attempt:
    catch (e) {
        // during login a dead stored token must not block the (already authenticated) login
        if (e.error === 'invalid_grant' && <called from login>) { await clearAuthData(user.id); return; }
        throw handleError(e);
    }
    

    Arguably login() should not call refresh() at all: it has just received fresh tokens from the authorization code, and the refresh call only validates a session that is being re-established regardless.

    Quick triage for anyone else hitting this — if the log shows [OpenID] Invalid grant with Client.refresh in the stack (not Client.callback), it's this bug. Don't touch your client ID/secret; clear directus_users.auth_data for that user.

    1 Reply Last reply
    1
    • jamesJ
      jamesJ
      james
      Staff
      wrote last edited by
      #2

      Hello @dbruehlmeier
      As you have identified correctly, this is an upstream issue.
      So please report this to Directus https://github.com/directus/directus/issues

      1 Reply Last reply
      1
      • D
        D
        dbruehlmeier
        wrote last edited by
        #3

        Thanks @james !

        Done: https://github.com/directus/directus/issues/28389

        1 Reply Last reply
        0
        • J
          J
          jdbaker
          wrote last edited by
          #4

          I had this same issue! I think I found the same solution, except through the GUI. I had to log in as another administrator and change the affected user's login provider to default, save, and then change it back to Cloudron.

          1 Reply Last reply
          1
          • D
            D
            dbruehlmeier
            wrote last edited by
            #5

            @jdbaker good tip, thanks! Let's hope the root cause will be adressed by Directus.

            1 Reply Last reply
            0

            Hello! It looks like you're interested in this conversation, but you don't have an account yet.

            Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.

            With your input, this post could be even better 💗

            Register Login
            Reply
            • Reply as topic
            Log in to reply
            • Oldest to Newest
            • Newest to Oldest
            • Most Votes


            • Login

            • Don't have an account? Register

            • Login or register to search.
            • First post
              Last post
            0
            • Categories
            • Recent
            • Tags
            • Popular
            • Bookmarks
            • Search