-
-
Notifications
You must be signed in to change notification settings - Fork 393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: expo auth #720
Merged
Merged
feat: expo auth #720
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
18bb853
feat: expo auth
juliusmarminge dc38d95
rm
juliusmarminge 5a21451
fix format
juliusmarminge d2778a0
fix lock
juliusmarminge 83980ab
feat: use expo-linking to construct urls for expo auth (#832)
jmcmullen 35452bc
expo install --fix
juliusmarminge 0603aff
Merge branch 'main' into 11-02-feat_expo_auth
juliusmarminge fa58c38
nit
juliusmarminge 0db5bee
fix cookie
juliusmarminge fe487d7
Merge branch 'main' into 11-02-feat_expo_auth
juliusmarminge 6ab0a75
Merge branch 'main' into 11-02-feat_expo_auth
juliusmarminge 9e23aa6
Merge branch 'main' into 11-02-feat_expo_auth
juliusmarminge 8f58650
Merge branch 'main' into 11-02-feat_expo_auth
juliusmarminge e01dc76
Merge branch 'main' into 11-02-feat_expo_auth
juliusmarminge fc4a591
use sync session store api
juliusmarminge 4b95224
add custom handler back
juliusmarminge cb1a111
fix exports
juliusmarminge 21d0ea9
Merge branch 'main' into 11-02-feat_expo_auth
juliusmarminge 3442dd2
Use .find() instead of always first cookie (#1043)
dBianchii 1c8655b
Merge branch 'main' into 11-02-feat_expo_auth
juliusmarminge a8deb69
feat: Expo Auth without setting AUTH_URL. (#1054)
Wundero 5804c19
some light refactoring
juliusmarminge f5f5395
dont' mutate args
juliusmarminge 8eee5ca
helper
juliusmarminge 59d869a
cmt
juliusmarminge 2c8fc0d
fmt
juliusmarminge File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import * as Linking from "expo-linking"; | ||
import * as Browser from "expo-web-browser"; | ||
|
||
import { api } from "./api"; | ||
import { getBaseUrl } from "./base-url"; | ||
import { deleteToken, setToken } from "./session-store"; | ||
|
||
export const signIn = async () => { | ||
const signInUrl = `${getBaseUrl()}/api/auth/signin`; | ||
const redirectTo = Linking.createURL("/login"); | ||
const result = await Browser.openAuthSessionAsync( | ||
`${signInUrl}?expo-redirect=${encodeURIComponent(redirectTo)}`, | ||
); | ||
|
||
if (result.type !== "success") return; | ||
const url = Linking.parse(result.url); | ||
const sessionToken = String(url.queryParams?.session_token); | ||
if (!sessionToken) return; | ||
|
||
setToken(sessionToken); | ||
}; | ||
|
||
export const useUser = () => { | ||
const { data: session } = api.auth.getSession.useQuery(); | ||
return session?.user ?? null; | ||
}; | ||
|
||
export const useSignIn = () => { | ||
const utils = api.useUtils(); | ||
|
||
return async () => { | ||
await signIn(); | ||
await utils.invalidate(); | ||
}; | ||
}; | ||
|
||
export const useSignOut = () => { | ||
const utils = api.useUtils(); | ||
const signOut = api.auth.signOut.useMutation(); | ||
|
||
return async () => { | ||
const res = await signOut.mutateAsync(); | ||
if (!res.success) return; | ||
await deleteToken(); | ||
await utils.invalidate(); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import Constants from "expo-constants"; | ||
|
||
/** | ||
* Extend this function when going to production by | ||
* setting the baseUrl to your production API URL. | ||
*/ | ||
export const getBaseUrl = () => { | ||
/** | ||
* Gets the IP address of your host-machine. If it cannot automatically find it, | ||
* you'll have to manually set it. NOTE: Port 3000 should work for most but confirm | ||
* you don't have anything else running on it, or you'd have to change it. | ||
* | ||
* **NOTE**: This is only for development. In production, you'll want to set the | ||
* baseUrl to your production API URL. | ||
*/ | ||
const debuggerHost = Constants.expoConfig?.hostUri; | ||
const localhost = debuggerHost?.split(":")[0]; | ||
|
||
if (!localhost) { | ||
// return "https://turbo.t3.gg"; | ||
throw new Error( | ||
"Failed to get localhost. Please point to your production server.", | ||
); | ||
} | ||
return `http://${localhost}:3000`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import * as SecureStore from "expo-secure-store"; | ||
|
||
const key = "session_token"; | ||
export const getToken = () => SecureStore.getItem(key); | ||
export const deleteToken = () => SecureStore.deleteItemAsync(key); | ||
export const setToken = (v: string) => SecureStore.setItem(key, v); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,55 @@ | ||
export { GET, POST } from "@acme/auth"; | ||
import type { NextRequest } from "next/server"; | ||
import { cookies } from "next/headers"; | ||
import { NextResponse } from "next/server"; | ||
|
||
import { GET as DEFAULT_GET, POST } from "@acme/auth"; | ||
|
||
export const runtime = "edge"; | ||
|
||
const EXPO_COOKIE_NAME = "__acme-expo-redirect-state"; | ||
const AUTH_COOKIE_PATTERN = /authjs\.session-token=([^;]+)/; | ||
|
||
export const GET = async ( | ||
req: NextRequest, | ||
props: { params: { nextauth: string[] } }, | ||
) => { | ||
const nextauthAction = props.params.nextauth[0]; | ||
const isExpoSignIn = req.nextUrl.searchParams.get("expo-redirect"); | ||
const isExpoCallback = cookies().get(EXPO_COOKIE_NAME); | ||
|
||
if (nextauthAction === "signin" && !!isExpoSignIn) { | ||
// set a cookie we can read in the callback | ||
// to know to send the user back to expo | ||
cookies().set({ | ||
name: EXPO_COOKIE_NAME, | ||
value: isExpoSignIn, | ||
maxAge: 60 * 10, // 10 min | ||
path: "/", | ||
}); | ||
} | ||
|
||
if (nextauthAction === "callback" && !!isExpoCallback) { | ||
cookies().delete(EXPO_COOKIE_NAME); | ||
|
||
const authResponse = await DEFAULT_GET(req); | ||
const setCookie = authResponse.headers | ||
.getSetCookie() | ||
.find((cookie) => cookie.startsWith("authjs.session-token")); | ||
const match = setCookie?.match(AUTH_COOKIE_PATTERN)?.[1]; | ||
|
||
if (!match) | ||
throw new Error( | ||
"Unable to find session cookie: " + | ||
JSON.stringify(authResponse.headers.getSetCookie()), | ||
); | ||
|
||
const url = new URL(isExpoCallback.value); | ||
url.searchParams.set("session_token", match); | ||
return NextResponse.redirect(url); | ||
} | ||
|
||
// Every other request just calls the default handler | ||
return DEFAULT_GET(req); | ||
}; | ||
|
||
export { POST }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work for me, as the cookie starts differently and does not find any, throwing the error just below. Here's an example of my value:
Since the cookie pattern is defined as a regex in this file, could we just use it here to match the cookie?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern itself is hardcoded as well - if you change the authjs cookie name, this will have issues no matter what. Perhaps it might make more sense to customize the cookie to be
acme.session-token
instead, and that way users of the app can just find+replaceacme
and will cover the cookie as well?An alternative would be to just ignore the prefix with something like this:
And then apply your suggestion to test the cookie.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Wundero I just realised that I misscopied the contents of the cookie 🤦
The part of the
authjs.session-token
does not change (at least on my case), I just wanted to highlight that the cookie contains another starting part (theauthjs.pkce.code_verifier=...
which caused that the.startsWith(...)
condition to not comply.With that being clarified, your point is still valid as NextAuth allows to configure cookies (including changing its names), so even the
.session-token
part could be changed. Is this something that needs to be supported? IMHO it's too of an edge case and not worth it, but still, adding a possible way below.Details
I guess the regex could be created from exported
authConfig
's cookie configuration (if any), defaulting to the current one:Notes:
satisfies NextAuthConfig
: the actual exportedauthConfig
does not have thecookies
and the nested properties, so TS coplains of theauthConfig.cookies?.sessionToken?.name
access, so this would need to be handledauthConfig
object needs to be exported from@acme/auth
.
with\\.
so the regexp is properly created... there could be more edge cases so things could breakThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
startsWith
should still work, since thegetSetCookie
will return a list of all named cookies - theauthjs.pkce.code_verifier
(or any other authjs cookie) is a separate entry in the list, and should fail that check. In your example, it would split the cookies at the,
and return these:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why (maybe different system, code or package versions?), but In my case it does not return the same as you.
What you provided:
What I'm getting:
So, in my case there's a single item that does not start with
authjs.session-token
.To add a bit more into the mix, I'm doing some tests with HTTPS and the name of the cookie changes, which would make the
startsWith
to not work even if your case (note the__Secure-authjs.session-token
names). TBH I'm trying withnextjs --experimental-https
, haven't tried this in a production site or anything, so not sure if that would affect:In any case, IMHO it feels more consistent and resilient doing the
AUTH_COOKIE_PATTERN.test(...)
, as just below it's doing asetCookie?.match(AUTH_COOKIE_PATTERN)
which also allows the cookie to be in any place of the string.