-
-
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
Auth Hooks #1993
Auth Hooks #1993
Conversation
waspc/data/Generator/templates/server/src/auth/providers/config/github.ts
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/user.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts
Outdated
Show resolved
Hide resolved
72dc06f
to
ff5ce18
Compare
2f39592
to
382f643
Compare
@sodic and I jumped on quick call to discuss some of the OAuth hooks since I felt I didn't design them to be that useful. We concluded the following:
|
382f643
to
9bcd660
Compare
waspc/data/Generator/templates/server/src/auth/providers/config/github.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
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.
Gotta run now, will finish the review tomorrow morning.
Nice work with this!
waspc/data/Generator/templates/server/src/auth/providers/oauth/cookies.ts
Outdated
Show resolved
Hide resolved
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.
Ok, finally done with everything 😄
I didn't try the playing around with the feature, I trust it works. Let me know if you still want me to try it out.
All in all, this is looking pretty nicely. I left a lot of comments mostly because of:
- The docs.
- Refactoring suggestions.
- The fact that I'm the only one reviewing this.
Some refactoring comments talk about the previous version of the code. Decide what you want to tackle and what not (I don't think any of them were a big deal).
As for the stuff that must be fixed, it's just one type error (ST
instead of OST
, you'll know when you find the comment). And please figure out why that's not failing anywhere.
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/user.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/handler.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
waspc/data/Generator/templates/server/src/auth/providers/oauth/handler.ts
Outdated
Show resolved
Hide resolved
Signed-off-by: Mihovil Ilakovac <[email protected]>
Signed-off-by: Mihovil Ilakovac <[email protected]>
Signed-off-by: Mihovil Ilakovac <[email protected]>
Signed-off-by: Mihovil Ilakovac <[email protected]>
export type OAuthState<UsesCodeVerifier extends boolean = false> = { | ||
state: string; | ||
} & (UsesCodeVerifier extends true |
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.
Boolean type arguments are generally considered an antipattern (even more than boolean arguments in functions, who also get a bad rep). This is because they move the type system away from "declaring sets of values" and into "implementing decision algorithms":
Foo<User>
- It's aFoo
of Users (whateverFoo
is).Foo<true>
- Hmm.
Of course, boolean type arguments are sometimes necessary, and we do use them throughout the codebase. But if we can help it, we should avoid them.
All in all, this is a pretty complex type, it includes: a type parameter (and a boolean one at that), a default value, a type constraint, an intersection type, and an inline conditional type.
I understand this code, but put yourself in someone else's shoes. Would you say the prevented duplication/extra keystrokes are worth all the additional complexity?
It might be necessary, I haven't looked at the details, you'll know best. I just wanted to say "If there's a way to have the same thing with a little more duplication and dumber generics, I'd go for it." You can resolve when you're happy.
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.
Ok, I've decided to be explicit about the OAuth flow type we are using i.e. with or without PKCE and this is what will result in the state object type.
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.
Something like
export type OAuthStateFor<
OT extends OAuthType
> = OAuthStateForOAuthType[OT];
export type OAuthStateWithCodeFor<OT extends OAuthType> = OAuthStateFor<OT> & {
code: string;
};
export type OAuthType = keyof OAuthStateForOAuthType;
export type OAuthStateFieldName = keyof OAuthState | keyof OAuthStateWithPKCE;
type OAuthStateForOAuthType = {
OAuth2: OAuthState,
OAuth2WithPKCE: OAuthStateWithPKCE,
};
type OAuthState = {
state: string;
};
type OAuthStateWithPKCE = {
state: string;
codeVerifier: string;
};
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
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.
Everything looks great, approved (finally, sorry for the delay 😅)
I left some comments though, so check them out.
/** | ||
* This is a no-op function since the user didn't define the onAfterSignup hook. | ||
*/ | ||
export const onAfterSignupHook: InternalFunctionForHook<OnAfterSignupHook> = async (params) => {} |
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.
Maaaybe just add an underscore to params.
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
*/ | ||
stateTypes: ST[], | ||
userSignupFields: UserSignupFields | undefined, | ||
oAuthType: OT |
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 like the new API where you specify the type instead of the details.
export function generateAndStoreOAuthState<OT extends OAuthType>({ | ||
oAuthType, | ||
provider, | ||
res, | ||
}: { | ||
oAuthType: OT, | ||
provider: ProviderConfig, | ||
res: ExpressResponse, | ||
): { [name in ST]: string } { | ||
const result = {} as { [name in StateType]: string } | ||
|
||
if (stateTypes.includes('state' as ST)) { | ||
const state = generateState(); | ||
setOAuthCookieValue(provider, res, 'state', state); | ||
result.state = state; | ||
} | ||
res: ExpressResponse | ||
}): OAuthStateFor<OT> { | ||
return { | ||
...generateAndStoreState(provider, res), | ||
...(oAuthType === 'OAuth2WithPKCE' && generateAndStoreCodeVerifier(provider, 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.
This is not important enough to hold the PR (especially because this is all Wasp private code), and I should have mentioned it when you first introduced the change (it slipped my mind).
So, you don't need to change anything. I'm writing this just FYI for the future.
It's better to have:
entireState = generateEntireState()
storeEntireState(entireState)
Than:
state1 = generateAndStoreState1()
state2 = generateAndStoreState2()
entireState = { state1, state2 }
In other words, the code should be as pure as possible for as long as possible, keeping side effects at the system's edge.
That's why my initial suggestion looked like this: #1993 (comment). My bad, I should have emphasized it more.
Haskell is great because it enforces this rule with its type signatures. You only want to deal with monads if necessary, so you keep them at the edge of your code. It pushes you into the "pit of success"
When making design decisions like this, I personally found it helpful to imagine TypeScript isn't as permissive with side effects and think, "How would I write this in Haskell."
It's important to note that this isn't exclusive to side effects. The same principle applies whenever you divide responsibilities across nested function calls.
Another code smell you can look out for is having more than a single function called doFooAndBar
:
- If it's only the top function, then all good.
- If the
fooAndBar
naming continues all the way down into subfunctions, that's often a sign that you've divided the responsibilities along the wrong axis.
In this example, we have:
generateAndStoreState
- generating logic
- storing logic
generateAndStoreCodeVerifier
- generating logic
- storing logic
generateAndStoreOAuthState
- generating logic
The generating logic is sprinkled across multiple methods, while the storing logic is repeated twice. If you decided to use localStorage instead of cookies, you'd have to remember to change it in two places.
Compare that with something like this:
generateAndStoreOAuthState
entireState = generateOAuthState()
storeState(entireState)
You could build these methods from what I suggested earlier:
// begin generateOAuthState function
const shouldGenerateCodeVerifier = optionalStateTypes.includes(
'codeVerifier' as OST
)
const result = {
state: generateState(),
...shouldGenerateCodeVerifier && { codeVerifier: generateCodeVerifier() },
}
// end generateOAuthState function
// begin storeOAuthState function
let key: keyof typeof result
for (key in result) {
setOAuthCookieValue(provider, res, key, result[key])
}
// end storeOAuthState function
return result
With this approach, all storage calls are defined in the same place. Also, the generating logic is better localized.
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.
Makes sense 👍
I'll split up the code to work in "stages". Thanks for the suggestion!
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 now written the methods as:
export function generateAndStoreOAuthState(...) {
const state: OAuthStateFor<OT> = {
...generateState(),
...(oAuthType === 'OAuth2WithPKCE' && generateCodeVerifier()),
};
storeOAuthStateInCookies(provider, res, state);
return state;
}
export function validateAndGetOAuthState(...) {
const state: OAuthStateWithCodeFor<OT> = {
...getCode(req),
...getState(req),
...(oAuthType === 'OAuth2WithPKCE' && getCodeVerifier(provider, req)),
};
validateOAuthState(provider, req, state);
return state;
}
Doing the for (key in result)
bit gives me these kinds of errors:
Argument of type 'string' is not assignable to parameter of type 'OAuthStateFieldName'.ts(2345)
and
Argument of type 'OAuthStateFor<OT>[keyof OAuthStateFor<OT>]' is not assignable to parameter of type 'string'.
Type 'OAuthStateFor<OT>[string] | OAuthStateFor<OT>[number] | OAuthStateFor<OT>[symbol]' is not assignable to type 'string'.
Type 'OAuthStateFor<OT>[string]' is not assignable to type 'string'.
so I went with a more basic approach of doing hardcoded calls.
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 found the reason for these type errors is using generics e.g. OAuthStateFor<OT>
vs. just using all the available values with OAuthStateFor<OAuthType>
. Works now 👍
waspc/data/Generator/templates/server/src/auth/providers/oauth/state.ts
Outdated
Show resolved
Hide resolved
} | ||
|
||
function generateAndStoreCodeVerifier( | ||
function storeOAuthStateInCookies<OT extends OAuthType>( |
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 would leave the implementation details out of the function's name -> storeOAuthState
.
Makes it easier to change storage mechanisms too, the beauty of encapsulation :)
state: OAuthStateFor<OT> | ||
): void { | ||
setOAuthCookieValue(provider, res, 'state', state.state); | ||
if (isOAuthStateWithPKCE(state)) { |
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 is this typeguard necessary?
We'll have to add a new "save" statement whenever we add a field, and the type system does not enforce it (i.e., it's possible to forget).
Why not simply loop over the state object and store everything that's inside? That way, if we add a new field, it will work automatically.
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.
Because state is a union of OAuthState
and OAuthStateWithPKCE
.
As I mentioned here: #1993 (comment)
Iterating over the values requires from me to assert the types 😢 I can do that ofc
Closes #1556
Adding hooks:
onAfterOAuthTokenReceivedLeft to do: