nowaythatworked / auth-astro

Community maintained Astro integration of @auth/core
295 stars 44 forks source link

Authentication fails with @auth/core/providers/twitch #24

Open Benzolio opened 1 year ago

Benzolio commented 1 year ago

I'm working on refactoring a project to use Astro, and auth-astro seems like a great way to handle authentication, but I'm having trouble getting it to work with the Twitch provider from @auth/core. Everything builds and sign in with GitHub works correctly showing my GitHub username in the session. I'm failry sure I have the clientId and clientSectret set up properly since the twitch authentication process does recognize that I've authenticated and it shows up in my connections on my twitch account, but when returning from id.twitch.tv to the callbackURL http://localhost:3000/api/auth/callback/twitch after authorizing my app, I get redirected to: http://localhost:3000/api/auth/error?error=CallbackRouteError which shows "Error localhost:3000 in the browser", and I get the following error in the node console from Astro:

[auth][cause]: OperationProcessingError: "response" body "scope" property must be a string
    at processGenericAccessTokenResponse (file:///D:/devroot/astro-migration/web/node_modules/oauth4webapi/build/index.js:917:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Module.processAuthorizationCodeOpenIDResponse (file:///D:/devroot/astro-migration/web/node_modules/oauth4webapi/build/index.js:1011:20)
    at async handleOAuth (file:///D:/devroot/astro-migration/web/node_modules/@auth/core/lib/oauth/callback.js:79:24)
    at async Module.callback (file:///D:/devroot/astro-migration/web/node_modules/@auth/core/lib/routes/callback.js:14:41)
    at async AuthInternal (file:///D:/devroot/astro-migration/web/node_modules/@auth/core/lib/index.js:64:38)
    at async Proxy.Auth (file:///D:/devroot/astro-migration/web/node_modules/@auth/core/index.js:100:30)
    at async eval (/node_modules/auth-astro/server.ts:44:17)
    at async Module.get (/node_modules/auth-astro/server.ts:65:14)
    at async call (file:///D:/devroot/astro-migration/web/node_modules/astro/dist/core/endpoint/index.js:72:20)
[auth][details]: {
  "provider": "twitch"
}

Astro doesn't retain a session from that login attempt, and just returns null for await getSession(Astro.request) where using the GitHub provider to log in gives me name, email, etc in the session as expected.

I have tested with the same twitch client id and secret using SvelteKitAuth @auth/sveltekit also using @auth/core/providers/twitch in a separate project and authenticated there successfully. Since that also uses @auth/core, I'm hoping there is just some minor tweak I'm missing.


My astro.config.mjs:

import { defineConfig } from "astro/config";
import { loadEnv } from "vite";
import node from "@astrojs/node";
import auth from "auth-astro";
import TwitchProvider from "@auth/core/providers/twitch";
import GithubProvider from "@auth/core/providers/github";
const env = loadEnv( "production", process.cwd(), "" );

const Twitch = TwitchProvider( {
  clientId     : env.TWITCH_CLIENT_ID,
  clientSecret : env.TWITCH_CLIENT_SECRET,
} );

const GitHub = GithubProvider( {
  clientId     : env.GITHUB_CLIENT_ID,
  clientSecret : env.GITHUB_CLIENT_SECRET,
} );

export default defineConfig( {
  output  : "server",
  adapter : node( {
    mode : "middleware"
  } ),
  integrations : [
    auth( {
      providers : [
        Twitch,
        GitHub,
      ],
    } ),
  ]
} );

I've tried with standalone instead of middleware for the node adapter mode, and it there is no difference in the resulting behavior when running astro dev (although I do need eventually middleware mode since this is going to be built and imported to another project serving with express and used just as part of a site.)


index.astro for testing:

---
import Layout from '../layouts/Layout.astro';

import type { Session } from '@auth/core/types';
import { SignIn, SignOut } from 'auth-astro/components';
import { getSession } from 'auth-astro/server';

const session = await getSession(Astro.request);
const sessionDump = JSON.stringify(session);
---

<Layout title="Auth test">
  <main>
    <h1>Welcome to <span class="text-gradient">Astro</span></h1>
    <p class="instructions">
      This is just a test.
    </p>
        {session ? 
          <SignOut>Logout</SignOut>
        :
          <SignIn>Login</SignIn>
        }
        <p>
          {session ? `Logged in as ${session.user?.name}` : 'Not logged in'}
        </p>
    sessionDump: {sessionDump}
  </main>
</Layout>

<style>
  main {
    margin: auto;
    padding: 1.5rem;
    max-width: 60ch;
  }
  h1 {
    font-size: 3rem;
    font-weight: 800;
    margin: 0;
  }
  .text-gradient {
    background-image: var(--accent-gradient);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-size: 400%;
    background-position: 0%;
  }
  .instructions {
    line-height: 1.6;
    margin: 1rem 0;
    border: 1px solid rgba(var(--accent), 25%);
    background-color: white;
    padding: 1rem;
    border-radius: 0.4rem;
  }
</style>

Is there something missing in auth-astro integration regarding the handling of the response body from the oidc/oauth process when the scope is returned? Any idea what I'm missing here?

Benzolio commented 1 year ago

I'm using a dirty hack to workaround this so i can get on with migrating the rest of this project. I really don't recommend this because it will just get overwritten by any package manager that updates oauth4webapi that I'm a. But, it does show that this is because of Twitch using a non standard representation for scope, as an array, instead of the expected string. I modified my locally cached node_modules/oauth4webapi/build/index.js to include:

    if (json.scope !== undefined && Array.isArray(json.scope) {
        console.warn("Detected array typed scope, attempting to stringify.");
        json.scope = json.scope.join(" ");
    }

...on line 916, which is just before where it checks that the type of scope conforms with spec. With this hack in place, I can log in with Twitch, and I get my name, email, and profile image in the session.

I'm not sure how the response is getting that far since the Twitch implementation in @auth/core has a conform function that already joins array typed scopes into strings, and I would expect that to be used. Is there anything about the way auth-astro talks to @auth/core that would bypass the type checking of providers? maybe since astro.config.mjs is not typescript the TwitchProvider type isn't being detected and something ends up treating the response from Twitch as a generic conforming provider without applying the special confirm function or something?