-
-
Notifications
You must be signed in to change notification settings - Fork 454
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
(exchanges) implement auth exchange #939
Conversation
💥 No ChangesetLatest commit: 6ee33b3 Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂 If these changes should be published to npm, you need to add a changeset. This PR includes no changesetsWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Click here to learn what changesets are, and how to add one. Click here if you're a maintainer who wants to add a changeset to this PR |
Hey @kadikraman! Thanks for this 🥳 . We're working on authentication in our app and it seems to work. One small note; it doesn't seem to play well with URQL DevTools; it throws a promise which contains the introspection query operation. cc @kitten |
I've just added the planned The last task is to check which events we'd like to add for the devtools. |
Avoid calling getAuth before the rest of the exchange has completely run.
67ecc1a
to
8f51a67
Compare
Hey @StevenLangbroek - just an FYI that |
Great feature. Thank you very much. I found a problem with our custom auth exchange and this came right in time to save us from grappling with wonka streams. That said, there's a bit of inflexibility with Token refresh on app start This is done outside of urql with a RESTful API request. The problem is by the time the request is made urql will have called The workaround is to have an Clear token on logout This should be a much more common use case. When the user logs out, we clear the tokens from the storage, but urql will blissfully keep adding the token it stored in |
Hi @MilosRasic - thanks for the feedback! Token refresh on app start - could you do this? const getAuth = async ({ authState }) => {
if (!authState) {
const initialAuthState = await doTokenRefreshOnAppStart();
return initialAuthState;
}
} Clear token on logout - we've just published some documentation relating to the auth exchange today. After logout, it's best to reinitialise the client to ensure the cache is cleared. This will also reset any auth state - docs |
Thanks for the suggestions, @kadikraman . For refresh on app start, I didn't find anything in the docs that implies that
For logout, makes sense, thanks. Much better than anything I could come up with and takes care of stale cache as well. |
Is you auth flow like so:
If so, you could do something like this: const getAuth = async ({ authState }) => {
// authState is not defined, this means that it's the initial launch
if (!authState) {
const tokenFromStorage = localStorage.getItem('token');
const refreshTokenFromStorage = localStorage.getItem('refreshToken');
const { error, token, refreshToken } = await doTokenRefresh({
token: tokenFromStorage,
refreshToken: refreshTokenFromStorage
});
localStorage.setItem('token', token);
localStorage.setItem('refreshToken', refreshToken);
if (error) {
throw error;
}
return { token, refreshToken };
} else {
// we will only get here if an auth error has occurred
// you can either refresh the token or log the user out
return {};
}
}; |
@MilosRasic just as a side note ✌️ I hope we can resolve your questions, but please move them to a new topical GitHub issue or a Spectrum thread for discoverability and to keep the noise on this implementation-specific PR thread low. Cheers! 🙏 |
Summary
Currently, every user will have to build their own auth exchange for authentication, and token refresh. The motivation for this exchange is to provide a general api that enables auth and token refresh for both React and React Native.
High level overview
The exchange will allow flexibility in:
Once the exchange is created, we trigger
getAuth
for the first time. Here you should fetch your existing auth state from storage and return it. If the storage is asynchronous (in case of React Native), the exchange will pause the execution of requests until the auth is fetched and can be added to requests. Note that we don't callgetAuth
for every request, but store theauthState
in the exchange.When the
authState
has been returned,addAuthToOperation
will be called for each operation. You should use the auth state to add the proper auth to each request (e.g. the auth header).Next,
willAuthError
is called. This is an opportunity to bail out early before doing a request. E.g if the JWT is expired we'll know there's no need to do the request, and it'll definitely fail.Once the request has been done, if an error occurs, then
didAuthError
is called to check whether the error was an auth error. If this returns true, then the auth library will callgetAuth
- this is where you should use the existing auth state to create a new (hopefully valid) auth state, e.g. use the refresh token to refresh your JWT. TheauthExchange
then callsaddAuthToOperation
with the new auth state and attempts to do the request again. If the second attempt also fails with an authentication error, we let the error fall through.Finally, we use the
errorExchange
to handle critical auth failures and log out. We know that once the error reached theerrorExchange
, authentication refresh has already been attempted:Note! The
errorExchange
must be placed before theauthExchange
in the exchanges array, because exchanges get executed in reverse order.Configuration options
getAuth: ({ authState: T | null }) => Promise<T | null>;
Allows you to handle retrieving your auth state (usually a token and refresh token). Here is there the bulk of the auth is handled.
This function is called during initial load and whenever an auth error occurs. This way you can use is to fetch your token from storage on initial load,
or do a token refresh during auth failure.
addAuthToOperation: ({ authState: T | null; operation: Operation; }) => Operation;
Given the auth state you have defined, use that to add auth to each operation. Usuall this involves adding a JWT to the request header.
didAuthError: ({ error: CombinedError; authState: T | null; }) => boolean;
This gets called whenever a graphQL error occurs. Return
true
if the error should count as an auth error,false
otherwise.willAuthError: ({ operation: Operation; authState: T | null; }) => boolean;
This is checked before a network request is done and gives you the opportunity to bail out early.
For example if you already you have an expiry on your jwt, this could be stored in
authState
and checked in this function.If the token is expired, we'll know the request will fail with an auth error without having to do the request.
Defaults to
false
Set of changes