-
Notifications
You must be signed in to change notification settings - Fork 8
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: replace redirect url pattern with route key pattern #398
base: main
Are you sure you want to change the base?
feat: replace redirect url pattern with route key pattern #398
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Datadog ReportBranch report: ✅ 0 Failed, 11 Passed, 0 Skipped, 10.84s Total Time |
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 feel like we can keep the changes to a minimum, instead of changing all the variables to something new. The concept is still the same right, just that the "redirect URL"s are now restricted to what the user has explicitly set. should we just change the callbackUrl schema and keep everything else the same?
(trying to make it easy for starter-kit derived project to update their code too, since there might be too many things to change in here for them)
|
||
useEffect(() => { | ||
if (!selectProfileStep) { | ||
setHasLoginStateFlag() | ||
if (!response.success) { | ||
void router.replace( |
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.
should this be run through the zod validation schema too?
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 think it's not necessary because SIGN_IN is a hardcoded constant
|
||
export const SgidLoginButton = (): JSX.Element | null => { | ||
const router = useRouter() | ||
const sgidLoginMutation = trpc.auth.sgid.login.useMutation({ | ||
onSuccess: async ({ redirectUrl }) => { | ||
await router.push(redirectUrl) | ||
window.location.href = redirectUrl |
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.
when using nextjs, it is best practice to check for the location of window.location
first, since this component might be SSR'd.
this might be a subtle bug if somehow invoked from the server.
why was the router.push
call replaced with this? was there a particular reason?
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.
hmm it's because redirectUrl is already known to be an external URL, so I thought there'd be no benefits from router.push
ing it, but I was not aware of the possibility of SSR'ing the component, I thought it's always CSR unless there's getServerSideProps
.
src/utils/url.ts
Outdated
export const resolveRouteKey = ( | ||
v: unknown, | ||
): (typeof AllRoutes)[keyof typeof AllRoutes] => { | ||
return AllRoutes[routeKeySchema.parse(v)] |
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.
should we return a fallback here? unsure, but better than users not changing routes maybe?
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.
routeKeySchema.parse(v)
will fallback to HOME
after parsing. routeKeySchema
is guaranteed to produce keyof AllRoutes
Re: minimizing changes to variables, I think conceptually it is quite different though, previous: In theory we can remove this layer of indirection and change it to something like I'll make a commit with the above and we can see how that looks |
Reverted to the redirect url pattern, but now
Testing checklist
|
|
||
return <FullscreenSpinner /> | ||
} | ||
|
||
/** | ||
* Page wrapper that renders children only if the login state localStorage flag has been set. | ||
* Otherwise, will redirect to the route passed into the `redirectTo` prop. | ||
* Otherwise, will redirect to SIGN_IN, which will redirect to route passed into the `redirectTo` prop. |
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.
seems like this will redirect to HOME
instead? is that intended?
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 component redirects to /sign-in?callbackUrl=<redirectTo>
when not signed in, redirectTo
defaults to HOME
if not provided.
|
||
export const HOME = '/home' | ||
export const PROFILE = '/profile' | ||
export const SETTINGS_PROFILE = '/settings/profile' | ||
|
||
export const ALLOWED_CALLBACK_ROUTES = [ |
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 wonder if this would work for dynamic routes (like /user/[userId]
). If not, might break many autoredirects/ cause confusion.
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.
No it doesnt, it will definitely break dynamic routes.
The issue is because router.push
does too much magic (e.g. you can do router.push('/home/[x]?x=../signout')
to /signout
)
It became quite challenging to validate that router.push
does not cause unintended navigation with user supplied input (?callbackUrl
param) (especially across next versions), so the alternative is to use a hardcoded whitelist approach like the PR shows.
appUrlSchema
can still be used to validate urls if the dev wishes to use dynamic urls, just that the additional friction is supposed to discourage that
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.
Happy to hear about alternatives though, not sure what's the best approach tbh
Context
Due to yet another
open redirect
VAPT finding (care360: GTA-55-008
), it has proved to be very challenging to validate thecallbackUrl
parameter.Approach
This PR completely removes the
callbackUrl
parameter in favor of theredirectRouteKey
parameter.In all situations where we redirect ,
redirectRouteKey
is first validated to bekeyof AllRoutes
, falling back toHOME
when invalid. (AllRoutes
is a dict with all the routes in the app).Then we resolve the
route key
to the actual route.How to read this PR
Please look at all the utilities
lib/
schemas/
utils/
first to familiarize with the constructs that enable this route key pattern.Then evaluate the components involved in the email login flow followed by the SGID login flow.
Finally you can evaluate utility component wrappers under
AuthWrappers/
Risks and Testing
Manually tested on
https://starter-3ela7h4z5-ogp-tooling.vercel.app/