-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Use Arctic for OAuth providers #1851
Conversation
try { | ||
const { accessToken } = await github.validateAuthorizationCode(code); | ||
|
||
// TODO: maybe an additional request to get the user's email? |
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 passport strategy did it by default if the scopes included user
or user:email
https://github.com/cfsghost/passport-github/blob/master/lib/strategy.js#L119
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.
What are the options? Either we get it for them, or they get it on their own.
Maybe let's do the same as Passport strategy did, it sounds useful in practice, and will be a less of a breaking change.
But if you feel that is too much, we can also skip it.
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.
Okay, this comment got outdated with latest changes. I replicated the logic that Passport had to keep the same information available. I think that makes the most sense!
f714353
to
45614f2
Compare
return <Redirect to="{= onAuthSucceededRedirectTo =}" />; | ||
} | ||
|
||
/* TODO: Decide if we want to redirect to a page that shows the error message */ |
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 vote that we show the user an error instead of redirecting to "onAuthFailedRedirectTo" or if we were to redirect to "onAuthFailedRedirectTo" we attach an error message to the redirect.
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.
yeah that sounds about right! I guess "onAuthFailedRedirectTo" serves multiple things -> where to send user if they try to open a page that needs auth but they are not authenticated, but then also where to send user on failed login. First scenario is ok, but we don't want to use it upon failed login / signup -> better to just show an error message.
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.
better to just show an error message.
I agree. Let's do 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.
I don't think failed logins and signups do any redirecting as it currently stands. Or do they?
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 OAuth login and signup, do in fact redirect. We are taking about this line: https://github.com/wasp-lang/wasp/blob/main/waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCodeExchange.jsx#L58
@@ -16,19 +16,11 @@ export type ProviderConfig = { | |||
// Unique provider identifier, used as part of URL paths | |||
id: ProviderName; | |||
displayName: string; | |||
// Each provider config can have an init method which is ran on setup time | |||
// e.g. for oAuth providers this is the time when the Passport strategy is registered. | |||
init?(provider: ProviderConfig): Promise<InitData>; |
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.
Not needed anymore
env.GITHUB_CLIENT_SECRET, | ||
); | ||
|
||
router.get('/login', async (_req, res) => { |
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.
Hard coded path, ideally we would move it to Haskell
|
||
// We need a user id to create the auth token, so we either find an existing user | ||
// or create a new one if none exists for this provider. | ||
async function getAuthIdFromProviderDetails( |
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.
Existing function
@@ -1,9 +1,7 @@ | |||
export function config() { | |||
console.log('Inside user-supplied GitHub config') | |||
return { | |||
clientID: process.env['GITHUB_CLIENT_ID'], |
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 need for client ID or client secret in the user config any more. Although, I believe that wasn't needed for some time since we merged our default options and their options.
@@ -119,28 +118,8 @@ genOAuthConfig provider maybeUserConfig pathToConfigDst = return $ C.mkTmplFdWit | |||
relPathFromAuthConfigToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir) | |||
relPathFromAuthConfigToServerSrcDir = [reldirP|../../../|] | |||
|
|||
getJsonForOAuthConfigProps :: OAuthAuthProvider -> [Aeson.Value] |
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.
Moved the env variables knowledge in the template
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.
How come you decided to do this?
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 believe we prematurely abstracted stuff here. This code assumes that there are only client ID and client secret env vars, but in reality, there can be 2, 3 or 4 even env vars needed to set up a provider. We don't use the knowledge about the env vars in Haskell so it wasn't really useful.
Also, I wanted to keep things as simple as possible to make the rewrite easier :)
Generator FileDraft | ||
genOAuthConfig provider maybeUserConfig pathToConfigDst = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) | ||
genOAuthConfig provider maybeUserConfig pathToConfigTmpl = return $ C.mkTmplFdWithData tmplFile (Just tmplData) |
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.
We used _oauth.ts
as a common template before, but now each provider has their own template file because their code now differs (the way we fetch the user info, some providers require extra steps, some not)
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.
Understood!
Still, the new provider files have a lot in common. There are differences, sure, but there's definitely repetition. Could we extract the similar parts into functions and maximize the signal-to-noise ratio in the createRouter
functions.
This will probably make adding new providers simpler. Still, I haven't studied the similarities and differences between the provider files in extreme detail, you'll be a better judge of whether such an effort makes sense or not.
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've given it a shot to minimise the repeated code. We could probably go further, but I didn't to be too smart about inventing new abstractions, this now feels okay, check it out and tell me what do you think. Should we go further?
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 that's enough abstraction for now. We'll come back to it when we add more providers and are smarter.
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.
Nice job @infomiho , I like the analytical and detailed approach!
I answered some top level stuff, and will leave the details to @sodic . Let me know if I missed something that you would like to get my opinion on.
scope -> scopes: why did that change? Because underlying thing names it that way? What is better, sounds like there is some semantic involved with those names? Is it actually scope or scopes, what makes more sense? I wouldn't try to avoid breaking change, I would instead go for the name that we think works the best, be it a breaking change or not, doesn't matter.
Passport strategies mapped the provider data into a specific shape -> how breaking is it? Will they need to migrate the database, because we stored that stuff in the DB? If so, then maybe it might make sense to do that mapping. If we are doing the mapping, is it complex, does it feel silly, or does it make sense, do we lose some info, ... -> how do we feel about it?
If there is no DB migration involved, then I would certainly not bother with any mapping (unless you think they way data is returned by Artcic is ugly and could benefit from mapping).
1965d1b
to
da2cd6e
Compare
I don't think we should focus on the breaking change part - we're not stable enough for such sacrifices in consistency (we shouldn't support both names).. Pick the more appropriate name, regardless of whether it causes a breaking change or not. What we can do is throw a nice error. Instead of "Unknown property: scope". We can say "Please change 'scope' to 'scopes' in this file".
I think the same principle applies here. Wasp is still immature enough to have the luxury of breaking stuff on account of creating something of high quality. Of course, change purely for the sake of change is also something we should avoid (especially when it impacts users). In short:
By the way, can we create migration scripts for these things? |
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.
A couple of old comments
waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCallback.tsx
Outdated
Show resolved
Hide resolved
return <Redirect to="{= onAuthSucceededRedirectTo =}" />; | ||
} | ||
|
||
/* TODO: Decide if we want to redirect to a page that shows the error message */ |
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 don't think failed logins and signups do any redirecting as it currently stands. Or do they?
@infomiho just a reminder not to miss this comment. |
Of course! I've been waiting to get input from both Filip and you and I'll give some sort of a conclusion after we agree on what is the best here 👍 |
1cbb6dd
to
fdf33df
Compare
We just passed through the options to Passport before. Passport used IMHO We'll need good migration docs from 0.12 to 0.13 (Maybe even a script is possible?)
Makes sense 👍
This could be a nice little touch. I've done stuff like this e.g. "Hey this error might mean you didn't run migrations" and it seemed to help the user.
As I said above, the format is something Passport invented and tried to stick to. But they are not the "one true solution" for OAuth, so I'd say, maybe stick to just passing through the info we get from providers. At least that way, it matches what the providers' might have in their docs?
+1 from me. I'd like to have at least good 0.12 to 0.13 migration docs. |
I would then say let's do what is easy for us and also gives us nice DX. Wasp is in Beta, and it is ok we are changing fast. |
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.
Left improvement suggestions, but no major problems.
Here's the only major thing I'd like us to discuss: #1851 (comment)
Nice work!
waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCallback.tsx
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCallback.tsx
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCallback.tsx
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCallback.tsx
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCallback.tsx
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/env.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/env.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/oneTimeCode.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/redirect.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/user.ts
Outdated
Show resolved
Hide resolved
export function OAuthCallbackPage() { | ||
const { isLoading, error, user } = useOAuthCallbackHandler(); | ||
|
||
if (user !== undefined && user !== null) { |
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.
How come it can be both of these? I'm guessing because undefined
happens on the first render, while null
happens when the user isn't logged in?
If so, there's a trick that many linters recommend (although I'm not sure I like it). It sits halfway between being fully explicit and using coercion:
if (user != null) {
// ...
}
This covers both undefined
and null
.
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'd like to skip the non strict check if possible. I know this looks ugly e.g. checking both undefined
and null
but it's at least not relying on implicit JS behaviour.
waspc/data/Generator/templates/server/src/auth/providers/oauth/user.ts
Outdated
Show resolved
Hide resolved
const { id: authId } = await tokenStore.verifyToken(code); | ||
const auth = await findAuthWithUserBy({ id: authId }) | ||
|
||
if (!auth) { |
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.
Explicit check (if you want)
function verifyToken(token: string) { | ||
return validateJWT<{ id: string }>(token); | ||
} |
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.
Why not:
function getVerifiedToken(token: string) Promise<id: string> {
return validateJWT(token);
}
It's a part of the public API through the returned object, so it makes sense to type it explicitly. Also, I'd say it's always better to type function signatures and leave the type inference for the body than the other way around.
const result = {} as { [name in StateType]: string } | ||
|
||
if (stateTypes.includes('state' as ST)) { | ||
const state = generateState(); | ||
setOAuthCookieValue(provider, res, 'state', state); | ||
result.state = state; | ||
} | ||
|
||
if (stateTypes.includes('codeVerifier' as ST)) { | ||
const codeVerifier = generateCodeVerifier(); | ||
setOAuthCookieValue(provider, res, 'codeVerifier', codeVerifier); | ||
result.codeVerifier = codeVerifier; | ||
} | ||
|
||
return result as { [name in ST]: string }; |
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.
A less error prone way to implement this is by generating the resulting state first, and then using that to set the value. In pseudocode:
// construct the result
setOAuthCookieValue(provider, res, result); // this function would iterate the object and set all cookies
return result
I probably wouldn't do this now though and would just merge this. Not a big deal.
} | ||
result.code = code; | ||
|
||
return result as { [name in ST]: string } & { code: string }; |
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 don't think you need this cast as the function is currently written.
|
||
const authId = await getAuthIdFromProviderDetails(providerId, providerProfile, userSignupFields); | ||
|
||
if (!authId) { |
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.
Explicit check if you want
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 function always returns a value or it throws an error, so I've removed this check.
Generator FileDraft | ||
genOAuthConfig provider maybeUserConfig pathToConfigDst = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) | ||
genOAuthConfig provider maybeUserConfig pathToConfigTmpl = return $ C.mkTmplFdWithData tmplFile (Just tmplData) |
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 that's enough abstraction for now. We'll come back to it when we add more providers and are smarter.
Signed-off-by: Mihovil Ilakovac <mihovil@ilakovac.com>
5813990
to
13b0c5c
Compare
Status
Needs to be implemented
serverUrl
in our server environment (Define the
WASP_SERVER_URL
env var #1856configFn
againGOOGLE_CLIENT_ID
)email
which is important to Wasp users)configFn
before to say "Hey, we changed this, you have to use "scopes" now" to make the migration easier (bonus)Improvements
createToken
andverifyToken
functions need to be refactored (tech debt from old auth)Replaces
jsonwebtoken
withoslo/jwt
#1852google.ts
andgithub.ts
(to reduce the effort of adding next providers)Changes for user
Open questions
configFn
property in Wasp file) to set scopes changed fromscope
toscopes
(Closes #1817