create-t3-turbo icon indicating copy to clipboard operation
create-t3-turbo copied to clipboard

feat: expo auth

Open juliusmarminge opened this issue 2 years ago • 24 comments

Closes #486

juliusmarminge avatar Nov 02 '23 00:11 juliusmarminge

  • #720 Graphite 👈
  • #715 Graphite
  • main

This stack of pull requests is managed by Graphite. Learn more about stacking.

Join @juliusmarminge and the rest of your teammates on Graphite Graphite

juliusmarminge avatar Nov 02 '23 00:11 juliusmarminge

I wanna use this implementation in my new application. Is there anything stopping this from being merged?

psycho-baller avatar Dec 06 '23 04:12 psycho-baller

I wanna use this implementation in my new application. Is there anything stopping this from being merged?

The hard-coded ip adress is a deal-breaker. Need to figure that out before I merge this. You should be fine to use this though given you know about that limitatino and can work around it.

Haven't had time to look into a more general solution for it.

juliusmarminge avatar Dec 14 '23 21:12 juliusmarminge

Any improvements on IP address issue? I would like to use next-auth for expo as well. What would be the redirectTo URL workaround if anyone found a way?

Merging this PR would be huge as currently there is no way to use next-auth with expo 🙏🏼

Re4GD avatar Dec 26 '23 20:12 Re4GD

Haven't had time to dig in yet...

juliusmarminge avatar Dec 26 '23 21:12 juliusmarminge

Appreciate your time man, you are doing Gods work 🙏🏼

As this is a redirect issue, I thought that any social login method would have to use some kind of redirect to our app's scheme in order to finish the login flow. Came accross this stackoverflow question:

https://stackoverflow.com/questions/76651842/google-auth-in-expo-49

import { makeRedirectUri } from 'expo-auth-session';

const EXPO_REDIRECT_PARAMS = {
  useProxy: true,
  projectNameForProxy: '@yourUsername/yourScheme',
};

const NATIVE_REDIRECT_PARAMS = { native: 'yourScheme://' };

const REDIRECT_PARAMS =
  Constants.appOwnership === 'expo'
    ? EXPO_REDIRECT_PARAMS
    : NATIVE_REDIRECT_PARAMS;

makeRedirectUri(REDIRECT_PARAMS)

Tried this on Expo Go, makeRedirectUri gave exp://192.168.x.x:8081 as expected. I was not able to create a development build to try this out due to some issues on my end. Hope this helps

Re4GD avatar Dec 26 '23 23:12 Re4GD

Woooow, thanks a million Can't thank you enough for this feat.

raphaelm-gioa avatar Dec 30 '23 13:12 raphaelm-gioa

Amazing work 🎆

Does anybody know if this would be possible using Fastify instead of NextJS?

Sorry for the out of context comment! :)

ck-euan avatar Jan 17 '24 17:01 ck-euan

Does anybody know if this would be possible using Fastify instead of NextJS?

can’t see why it wouldn’t

juliusmarminge avatar Jan 17 '24 20:01 juliusmarminge

Hope you're doing well, I used this branch to have authentication via expo on my project and ran into couple of issues on my android device with expo.

  1. Authentication bug The Browser.openAuthSessionAsync on Android always returns "dismiss." when we try to login, I found a fix, it should works with the following lines on auth.ts.
  const signInUrl = `${getBaseUrl()}/api/auth/signin`;
  const redirectTo = Linking.createURL("login");
  const result = await Browser.openAuthSessionAsync(
    `${signInUrl}?expo-redirect=${encodeURIComponent(redirectTo)}`,
    redirectTo,
  );
  1. Logout error Logout wasn't working because of a session token format issue, I tweaked it, and now it's all good. It should fixes the following error:
Invalid `prisma.session.delete()` invocation:


An operation failed because it depends on one or more records that were required but not found. Record to delete does not exist.

I'd love to contribute these fixes! Just wanted to check in to see if they align with the branch vibe. Also, any other hot issues I should know about?

Thanks

arnaud-zg avatar Jan 18 '24 00:01 arnaud-zg

Does anybody know if this would be possible using Fastify instead of NextJS?

can’t see why it wouldn’t

Yeah managed to get it working nicely :)

Do you think there's any way this setup could work for things that require the auth request on the native side not the browser? i.e. sign in with Apple?

ck-euan avatar Jan 20 '24 00:01 ck-euan

Any ETA for this feature to be merged?

HarunKilic avatar Jan 23 '24 12:01 HarunKilic

@arnaud-zg were you able to get this to work in production? I log in with Discord but don't get redirected to the app with the cookie set.

[GET] /api/auth/callback/discord?code=0gaMMdbGfOjraF86RgBx7rwIvnHsgx&nxtPnextauth=callback%2Fdiscord status=500 setCookie __Secure-authjs.pkce.code_verifier=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax ⨯ Error: Unable to find session cookie

IMG_0718

trevorpfiz avatar Feb 09 '24 18:02 trevorpfiz

@arnaud-zg were you able to get this to work in production? I log in with Discord but don't get redirected to the app with the cookie set.

[GET] /api/auth/callback/discord?code=0gaMMdbGfOjraF86RgBx7rwIvnHsgx&nxtPnextauth=callback%2Fdiscord status=500 setCookie __Secure-authjs.pkce.code_verifier=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax ⨯ Error: Unable to find session cookie

IMG_0718

I was able to make it by changing setCookie to use second array element within apps/nextjs/src/app/api/auth/[...nextauth]/router.ts like:

        const setCookie = authResponse.headers.getSetCookie()[1]
        const match = setCookie?.match(AUTH_COOKIE_PATTERN)?.[1]

HarunKilic avatar Feb 09 '24 22:02 HarunKilic

@HarunKilic / @arnaud-zg

Have any of you got this working without the middle auth screen, ie. click login with google from a react native touchableopacity straight to the google login?

danvernon avatar Feb 27 '24 14:02 danvernon

Anyone have any luck getting this to work with a local setup? My cloud deployment works fine, but when I click Sign in with Google from the webview, it immediately changes the header title to localhost and I get Safari can't open the page because it couldn't connect to the server.

adamspotlite avatar Mar 15 '24 05:03 adamspotlite

@HarunKilic / @arnaud-zg

Have any of you got this working without the middle auth screen, ie. click login with google from a react native touchableopacity straight to the google login?

I implemented the auth flow with Expo AuthSession, but it proved to be quite unstable on Android devices, the redirection works randomly, I couldn't find the root cause of the issue. For now, I'm resorting to using the middle screen instead.

arnaud-zg avatar Mar 15 '24 15:03 arnaud-zg

Anyone have any luck getting this to work with a local setup? My cloud deployment works fine, but when I click Sign in with Google from the webview, it immediately changes the header title to localhost and I get Safari can't open the page because it couldn't connect to the server.

I'm getting the exact same error here

dBianchii avatar Apr 19 '24 13:04 dBianchii

I'm also getting this error when clicking the sign in button on expo app: image

dBianchii avatar Apr 19 '24 13:04 dBianchii

You must set the auth_url to your ip. It's mentioned in the readme i think? Localhost doesnt work since your mobile cant reach that

juliusmarminge avatar Apr 19 '24 13:04 juliusmarminge

@juliusmarminge

"In order for the CSRF protection to work when developing locally, you will need to set the AUTH_URL to the same IP address your expo dev server is listening on. This address is displayed in your Expo CLI when starting the dev server."

So, my metro is running currently at exp://192.168.0.192:8081. Can I just set AUTH_URL as AUTH_URL="http://192.168.0.192:8081" ? Not sure I get it

dBianchii avatar Apr 19 '24 13:04 dBianchii

@dBianchii It needs to be set to the port that your NextJS app is running at, so http://192.168.0.192:3000 probably

ck-euan avatar Apr 22 '24 13:04 ck-euan

@juliusmarminge hi julius, is there a blocker for this pr? maybe i can help you.

necmettindev avatar May 03 '24 00:05 necmettindev

Yea feel free to pick it up and finish the remaining pieces

I'd love for a better local experience (not having to put in your ip address in env variables)

Also just go over and make sure things work, it's been a while since I touched it now

juliusmarminge avatar May 03 '24 05:05 juliusmarminge

CleanShot 2024-05-12 at 00 01 32

Merged some stuff in and verified it still works.

But I really wanna find a fix so you don't have to have AUTH_URL='http://192.168.10.xxx:3000' or similar in .env...

juliusmarminge avatar May 11 '24 22:05 juliusmarminge

CleanShot 2024-05-12 at 00 01 32

Merged some stuff in and verified it still works.

But I really wanna find a fix so you don't have to have AUTH_URL='http://192.168.10.xxx:3000' or similar in .env...

Something like this doesn't work?

const isDev = process.env.NODE_ENV === 'development';
const { expoConfig } = Constants;

const baseUrl = isDev
    ? `http://${expoConfig!.hostUri?.split(':').shift()}:3000` : `https://your_real_url.com`

shiroyasha9 avatar May 12 '24 20:05 shiroyasha9

The problem isnt on the expo side, it's on the next side. The URL must match for csrf reasons and since the expo app hits using the LAN IP, the server must accept that URL 😵‍💫😵‍💫

juliusmarminge avatar May 12 '24 20:05 juliusmarminge

Is setting the AUTH_URL env variable in a script a proper solution? (in apps/nextjs/package.json) "dev": "AUTH_URL=http://$(ipconfig getifaddr en0):3000 pnpm with-env next dev" appears to work equally well.

Azzerty23 avatar May 14 '24 21:05 Azzerty23

Is setting the AUTH_URL env variable in a script a proper solution? (in apps/nextjs/package.json) "dev": "AUTH_URL=http://$(ipconfig getifaddr en0):3000 pnpm with-env next dev" appears to work equally well.

This likely won't work on all developer's machines (I believe ipconfig is windows/mac, while ifconfig is linux - might be wrong about this). Personally not a fan of using platform-specific commands. Additionally, this would then cause issues if you use web since I think AUTH_URL should be localhost or 127.0.0.1 instead.

I think a more comprehensive solution would be to support multiple URLs on nextauth's side, or try to hack into nextauth a bit to check where the request comes from.

Another possible solution is to disable CSRF checks entirely, and then use a different mechanism for CSRF (particularly, checking origin/host headers) in a middleware. See this section on the OWASP cheatsheet for details on why this works. Per Lucia's docs, it can be implemented as such:

// middleware.ts
import { verifyRequestOrigin } from "lucia";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(request: NextRequest): Promise<NextResponse> {
	if (request.method === "GET") {
		return NextResponse.next();
	}
	const originHeader = request.headers.get("Origin");
	// NOTE: You may need to use `X-Forwarded-Host` instead
	const hostHeader = request.headers.get("Host");
	if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) {
		return new NextResponse(null, {
			status: 403
		});
	}
	return NextResponse.next();
}

Obviously, this uses a lucia utility, but the premise remains that this could supplant CSRF and thus fix the need for AUTH_URL.

The trusted URLs section of the call (in this case, the host header) can also just use hard-coded values or check against loopback addresses.

(For reference, this is the impl from lucia (technically oslo)):

export function verifyRequestOrigin(origin: string, allowedDomains: string[]): boolean {
	if (!origin || allowedDomains.length === 0) return false;
	const originHost = safeURL(origin)?.host ?? null;
	if (!originHost) return false;
	for (const domain of allowedDomains) {
		let host: string | null;
		if (domain.startsWith("http://") || domain.startsWith("https://")) {
			host = safeURL(domain)?.host ?? null;
		} else {
			host = safeURL("https://" + domain)?.host ?? null;
		}
		if (originHost === host) return true;
	}
	return false;
}

function safeURL(url: URL | string): URL | null {
	try {
		return new URL(url);
	} catch {
		return null;
	}
}

Wundero avatar May 14 '24 23:05 Wundero

Is setting the AUTH_URL env variable in a script a proper solution? (in apps/nextjs/package.json) "dev": "AUTH_URL=http://$(ipconfig getifaddr en0):3000 pnpm with-env next dev" appears to work equally well.

This likely won't work on all developer's machines (I believe ipconfig is windows/mac, while ifconfig is linux - might be wrong about this). Personally not a fan of using platform-specific commands. Additionally, this would then cause issues if you use web since I think AUTH_URL should be localhost or 127.0.0.1 instead.

I think a more comprehensive solution would be to support multiple URLs on nextauth's side, or try to hack into nextauth a bit to check where the request comes from.

Another possible solution is to disable CSRF checks entirely, and then use a different mechanism for CSRF (particularly, checking origin/host headers) in a middleware. See this section on the OWASP cheatsheet for details on why this works. Per Lucia's docs, it can be implemented as such:

// middleware.ts
import { verifyRequestOrigin } from "lucia";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(request: NextRequest): Promise<NextResponse> {
	if (request.method === "GET") {
		return NextResponse.next();
	}
	const originHeader = request.headers.get("Origin");
	// NOTE: You may need to use `X-Forwarded-Host` instead
	const hostHeader = request.headers.get("Host");
	if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) {
		return new NextResponse(null, {
			status: 403
		});
	}
	return NextResponse.next();
}

Obviously, this uses a lucia utility, but the premise remains that this could supplant CSRF and thus fix the need for AUTH_URL.

The trusted URLs section of the call (in this case, the host header) can also just use hard-coded values or check against loopback addresses.

(For reference, this is the impl from lucia (technically oslo)):

export function verifyRequestOrigin(origin: string, allowedDomains: string[]): boolean {
	if (!origin || allowedDomains.length === 0) return false;
	const originHost = safeURL(origin)?.host ?? null;
	if (!originHost) return false;
	for (const domain of allowedDomains) {
		let host: string | null;
		if (domain.startsWith("http://") || domain.startsWith("https://")) {
			host = safeURL(domain)?.host ?? null;
		} else {
			host = safeURL("https://" + domain)?.host ?? null;
		}
		if (originHost === host) return true;
	}
	return false;
}

function safeURL(url: URL | string): URL | null {
	try {
		return new URL(url);
	} catch {
		return null;
	}
}

As a followup to this, unfortunately CSRF isn't the only thing that doesn't work when you don't set the AUTH_URL. A couple of points I found:

  • OAuth callbacks will still point to localhost, and I can't seem to get the auth proxy to work with this either.
  • NextJS seems to infer the request.url property as localhost even when your origin is the 192.168.x.y ip, despite the request being made to the proper location. You can introspect the real host using the host header, and set the request url to use that, but that seems hacky at best, and doesn't really seem to cover all urls anyways

Ultimately, I have been unable to get local auth to work without the env var, and I don't want to try to hack nextauth to get it to work even though that seems like the only way this could be made to work.

I have had more success using Lucia and integrating auth, however Lucia has its own caveats:

  • Lucia's auth is far more involved, requiring you to implement all of these features by yourself:
    • Pages (e.g. sign in pages, sign out pages, etc.)
    • Routing (handling OAuth routes, etc.) - this is especially noticeable when you are trying to implement multiple providers
    • Redirects (NextAuth protects redirects and handles next pages, callback urls, etc. for you, but you have to implement and secure these yourself in Lucia.)
    • Auth proxy for preview environments (Especially if you want to secure this, you have to implement some custom cryptographic logic, either signing or encrypting the state payload of an OAuth callback)
  • Lucia is focused on giving users good primitives + guides to implement secure apps using those primitives, but if CT3T (or CT3A by proxy) implements Lucia, it could lead less experienced developers to make mistakes if they want to change some auth stuff unless careful consideration into the code design is taken.

Wundero avatar May 15 '24 04:05 Wundero