lucia-auth / lucia

Authentication, simple and clean
https://lucia-auth.com
MIT License
8.35k stars 448 forks source link

[Bug]: The state and storedState is different every time | google oauth ios #1506

Closed kdurek closed 3 months ago

kdurek commented 3 months ago

Package

lucia

Describe the bug

Hi, I have a problem that on ios on first login everything is okay, but every following one failing and returning blank page, because state and storedState is different. It's not working only on ios, on android, mac, windows everything seems fine. Someone have any ideas?

app/api/auth/google/route.ts

import { generateCodeVerifier, generateState } from 'arctic';
import { cookies } from 'next/headers';

import { google } from '@/server/auth';

export async function GET(): Promise<Response> {
  const state = generateState();
  const codeVerifier = generateCodeVerifier();

  const url = await google.createAuthorizationURL(state, codeVerifier, {
    scopes: ['profile', 'email'],
  });

  cookies().set('google_oauth_state', state, {
    path: '/',
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 60 * 10,
    sameSite: 'lax',
  });

  cookies().set('google_oauth_code_verifier', codeVerifier, {
    path: '/',
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 60 * 10,
    sameSite: 'lax',
  });

  return Response.redirect(url);
}

app/api/auth/google/callback/route.ts

import { type GoogleTokens, OAuth2RequestError } from 'arctic';
import { cookies } from 'next/headers';

import { google, lucia } from '@/server/auth';
import { db } from '@/server/db';

interface GoogleUser {
  sub: string;
  name: string;
  given_name: string;
  family_name: string;
  picture: string;
  email: string;
  email_verified: boolean;
  locale: string;
}

export async function GET(request: Request): Promise<Response> {
  const url = new URL(request.url);

  const code = url.searchParams.get('code');
  const state = url.searchParams.get('state');

  const storedState = cookies().get('google_oauth_state')?.value ?? null;
  const storedCodeVerifier = cookies().get('google_oauth_code_verifier')?.value ?? null;

  if (!code || !state || !storedState || !storedCodeVerifier || state !== storedState) {
    return new Response(null, {
      status: 400,
    });
  }

  try {
    const tokens: GoogleTokens = await google.validateAuthorizationCode(code, storedCodeVerifier);
    const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
      headers: {
        Authorization: `Bearer ${tokens.accessToken}`,
      },
    });
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const googleUser: GoogleUser = await response.json();

    const existingUser = await db.user.findUnique({
      where: {
        googleId: googleUser.sub,
      },
    });

    if (existingUser) {
      const session = await lucia.createSession(existingUser.id, {});
      const sessionCookie = lucia.createSessionCookie(session.id);
      cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
      return new Response(null, {
        status: 302,
        headers: {
          Location: '/',
        },
      });
    }

    const user = await db.user.create({
      data: {
        googleId: googleUser.sub,
        name: googleUser.name,
        email: googleUser.email,
        image: googleUser.picture,
      },
    });

    const session = await lucia.createSession(user.id, {});
    const sessionCookie = lucia.createSessionCookie(session.id);
    cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
    return new Response(null, {
      status: 302,
      headers: {
        Location: '/',
      },
    });
  } catch (e) {
    if (e instanceof OAuth2RequestError && e.message === 'bad_verification_code') {
      return new Response(null, {
        status: 400,
      });
    }
    return new Response(null, {
      status: 500,
    });
  }
}
kdurek commented 3 months ago

actually it now changed a little, my cookie is null on callback page but properly set on auth page

pilcrowOnPaper commented 3 months ago

So the state stored in the cookie and the one returned by Google is different? Do you see any patterns?

kdurek commented 3 months ago

Thanks for the quick reply, im dealing with it for so long :D I just pushed console logs to my prod build I see that my generated google auth url

const url = await google.createAuthorizationURL(state, codeVerifier, {
  scopes: ['profile', 'email'],
});

is different that the one in callback

const url = new URL(request.url);

Precisely, ?state= is different On prod I am using this package for PWA https://github.com/serwist/serwist if it make difference (I am testing on normal page, not installed one)

pilcrowOnPaper commented 3 months ago

can you also try console logging the url returned by createAuthorizationURL()?

console.log(url.toString())
kdurek commented 3 months ago

Yeah i consoled that, I am pasting my logs, with malformed data with alphabet numbers like aaaa

| πŸš€ > _____START_AUTH_____:
| πŸš€ > _____GENERATED_____:
| πŸš€ > state: aaaaa
| πŸš€ > codeVerifier: bbbbb
| πŸš€ > url: URL {
|   href: 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=sssss&state=aaaaa&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=lllll&nonce=_',
|   origin: 'https://accounts.google.com',
|   protocol: 'https:',
|   username: '',
|   password: '',
|   host: 'accounts.google.com',
|   hostname: 'accounts.google.com',
|   port: '',
|   pathname: '/o/oauth2/v2/auth',
|   search: '?response_type=code&client_id=sssss&state=aaaaa&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=lllll&nonce=_',
|   searchParams: URLSearchParams {
|     'response_type' => 'code',
|     'client_id' => 'sssss',
|     'state' => 'aaaaa',
|     'scope' => 'profile email openid',
|     'redirect_uri' => 'https://example.com/api/auth/google/callback',
|     'code_challenge_method' => 'S256',
|     'code_challenge' => 'lllll',
|     'nonce' => '_' },
|   hash: ''
| }
| πŸš€ > _____STORED_____:
| πŸš€ > storedState: aaaaa
| πŸš€ > storedCodeVerifier: bbbbb
| πŸš€ > _____END_AUTH_____:
| πŸš€ > _____START_AUTH_____:
| πŸš€ > _____GENERATED_____:
| πŸš€ > state: zzzzz
| πŸš€ > codeVerifier: uuuuu
| πŸš€ > url: URL {
|   href: 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=sssss&state=zzzzz&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=bbbbb&nonce=_',
|   origin: 'https://accounts.google.com',
|   protocol: 'https:',
|   username: '',
|   password: '',
|   host: 'accounts.google.com',
|   hostname: 'accounts.google.com',
|   port: '',
|   pathname: '/o/oauth2/v2/auth',
|   search: '?response_type=code&client_id=sssss&state=zzzzz&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=bbbbb&nonce=_',
|   searchParams: URLSearchParams {
|     'response_type' => 'code',
|     'client_id' => 'sssss',
|     'state' => 'zzzzz',
|     'scope' => 'profile email openid',
|     'redirect_uri' => 'https://example.com/api/auth/google/callback',
|     'code_challenge_method' => 'S256',
|     'code_challenge' => 'bbbbb',
|     'nonce' => '_' },
|   hash: ''
| }
| πŸš€ > _____STORED_____:
| πŸš€ > storedState: zzzzz
| πŸš€ > storedCodeVerifier: uuuuu
| πŸš€ > _____END_AUTH_____:
| πŸš€ > _____START_CALLBACK_____:
| πŸš€ > _____PARAMS_____:
| πŸš€ > url: URL {
|   href: 'https://0.0.0.0:3000/api/auth/google/callback?state=aaaaa&code=hhhhh&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none',
|   origin: 'https://0.0.0.0:3000',
|   protocol: 'https:',
|   username: '',
|   password: '',
|   host: '0.0.0.0:3000',
|   hostname: '0.0.0.0',
|   port: '3000',
|   pathname: '/api/auth/google/callback',
|   search: '?state=aaaaa&code=hhhhh&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none',
|   searchParams: URLSearchParams {
|     'state' => 'aaaaa',
|     'code' => 'ppppp',
|     'scope' => 'email profile https://www.googleapis.com/auth/userinfo.email openid https://www.googleapis.com/auth/userinfo.profile',
|     'authuser' => '0',
|     'prompt' => 'none' },
|   hash: ''
| }
| πŸš€ > code: ppppp
| πŸš€ > state: aaaaa
| πŸš€ > _____STORED_____:
| πŸš€ > storedState: zzzzz
| πŸš€ > storedCodeVerifier: uuuuu
| πŸš€ > _____END_CALLBACK_____:
| πŸš€ > _____START_AUTH_____:
| πŸš€ > _____GENERATED_____:
| πŸš€ > state: iiiii
| πŸš€ > codeVerifier: ooooo
| πŸš€ > url: URL {
|   href: 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=sssss&state=iiiii&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=yyyyy&nonce=_',
|   origin: 'https://accounts.google.com',
|   protocol: 'https:',
|   username: '',
|   password: '',
|   host: 'accounts.google.com',
|   hostname: 'accounts.google.com',
|   port: '',
|   pathname: '/o/oauth2/v2/auth',
|   search: '?response_type=code&client_id=sssss&state=iiiii&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=yyyyy&nonce=_',
|   searchParams: URLSearchParams {
|     'response_type' => 'code',
|     'client_id' => 'sssss',
|     'state' => 'iiiii',
|     'scope' => 'profile email openid',
|     'redirect_uri' => 'https://example.com/api/auth/google/callback',
|     'code_challenge_method' => 'S256',
|     'code_challenge' => 'yyyyy',
|     'nonce' => '_' },
|   hash: ''
| }
| πŸš€ > _____STORED_____:
| πŸš€ > storedState: iiiii
| πŸš€ > storedCodeVerifier: ooooo
| πŸš€ > _____END_AUTH_____:
| πŸš€ > _____START_AUTH_____:
| πŸš€ > _____GENERATED_____:
| πŸš€ > state: eeeee
| πŸš€ > codeVerifier: mmmmm
| πŸš€ > url: URL {
|   href: 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=sssss&state=eeeee&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=ccccc&nonce=_',
|   origin: 'https://accounts.google.com',
|   protocol: 'https:',
|   username: '',
|   password: '',
|   host: 'accounts.google.com',
|   hostname: 'accounts.google.com',
|   port: '',
|   pathname: '/o/oauth2/v2/auth',
|   search: '?response_type=code&client_id=sssss&state=eeeee&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=ccccc&nonce=_',
|   searchParams: URLSearchParams {
|     'response_type' => 'code',
|     'client_id' => 'sssss',
|     'state' => 'eeeee',
|     'scope' => 'profile email openid',
|     'redirect_uri' => 'https://example.com/api/auth/google/callback',
|     'code_challenge_method' => 'S256',
|     'code_challenge' => 'ccccc',
|     'nonce' => '_' },
|   hash: ''
| }
| πŸš€ > _____STORED_____:
| πŸš€ > storedState: eeeee
| πŸš€ > storedCodeVerifier: mmmmm
| πŸš€ > _____END_AUTH_____:
| πŸš€ > _____START_AUTH_____:
| πŸš€ > _____GENERATED_____:
| πŸš€ > state: ttttt
| πŸš€ > codeVerifier: nnnnn
| πŸš€ > url: URL {
|   href: 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=sssss&state=ttttt&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=yyyyy&nonce=_',
|   origin: 'https://accounts.google.com',
|   protocol: 'https:',
|   username: '',
|   password: '',
|   host: 'accounts.google.com',
|   hostname: 'accounts.google.com',
|   port: '',
|   pathname: '/o/oauth2/v2/auth',
|   search: '?response_type=code&client_id=sssss&state=ttttt&scope=profile+email+openid&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fauth%2Fgoogle%2Fcallback&code_challenge_method=S256&code_challenge=yyyyy&nonce=_',
|   searchParams: URLSearchParams {
|     'response_type' => 'code',
|     'client_id' => 'sssss',
|     'state' => 'ttttt',
|     'scope' => 'profile email openid',
|     'redirect_uri' => 'https://example.com/api/auth/google/callback',
|     'code_challenge_method' => 'S256',
|     'code_challenge' => 'yyyyy',
|     'nonce' => '_' },
|   hash: ''
| }
| πŸš€ > _____STORED_____:
| πŸš€ > storedState: ttttt
| πŸš€ > storedCodeVerifier: nnnnn
| πŸš€ > _____END_AUTH_____:
| πŸš€ > _____START_CALLBACK_____:
| πŸš€ > _____PARAMS_____:
| πŸš€ > url: URL {
|   href: 'https://0.0.0.0:3000/api/auth/google/callback?state=eeeee&code=vvvvv&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none',
|   origin: 'https://0.0.0.0:3000',
|   protocol: 'https:',
|   username: '',
|   password: '',
|   host: '0.0.0.0:3000',
|   hostname: '0.0.0.0',
|   port: '3000',
|   pathname: '/api/auth/google/callback',
|   search: '?state=eeeee&code=vvvvv&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none',
|   searchParams: URLSearchParams {
|     'state' => 'eeeee',
|     'code' => 'bbbbb',
|     'scope' => 'email profile https://www.googleapis.com/auth/userinfo.email openid https://www.googleapis.com/auth/userinfo.profile',
|     'authuser' => '0',
|     'prompt' => 'none' },
|   hash: ''
| }
| πŸš€ > code: bbbbb
| πŸš€ > state: eeeee
| πŸš€ > _____STORED_____:
| πŸš€ > storedState: ttttt
| πŸš€ > storedCodeVerifier: nnnnn
| πŸš€ > _____END_CALLBACK_____:
kdurek commented 3 months ago

Seems like disabling PWA entirely fixed issue, gonna test it more and open issue on 'serwist' side probably? Will close this one if I confirm it's their fault

kdurek commented 3 months ago

https://github.com/serwist/serwist/discussions/28#discussioncomment-8900344