Firebase ID Token expiration in an hour - javascript

so I am using redux-saga in my react-native app and tried to use refresh token but didn't work, so my approach was the following in app.js in order to get the token specifically for each request and force refresh it:
handleResponse = async () => {
const {dispatch} = this.store;
await axios.interceptors.request.use(config => {
// Important: request interceptors **must** return the request.
console.log("refreshToken")
let user = firebase.auth().currentUser;
firebase.auth().onAuthStateChanged(function(user) { if (user) {
console.log("auth changed: ",user)
user.getIdToken(true).then((token) => {
setAccessToken(token);
config.headers.authorization = token;
}
);
} else { console.log("didn't auth change") } });
console.log("req in handle response: ",JSON.stringify(config));
return config;
});
axios.interceptors.response.use(config => config, (err) => {
if (err.response) {
const response = err.response;
const state = this.store.getState();
if (
response.status === 401
&& state.auth.isAuthenticated
) {
dispatch(logout());
}
}
return Promise.reject(err);
});
};
But it always ends up after an hour throwing me the following error::
Firebase ID token has expired. Get a fresh token from your client app and try again (auth/id-token-expired). See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
so I was wondering if there's another approach I can try to solve the issue from my side?
Thanks in advance.

Firebase auth tokens automatically refresh every hour. That's a function of the SDK, and you don't have to do anything to enable it.
You definitely do not want to call firebase.auth().onAuthStateChanged for each request. That starts a persistent listener, and adds a new callback every time it's used. Typically you add just one listener globally for your app, and use its updates as they happen.
If you need to know when the SDK refreshes the token in order to get a new one immediately, you should instead use onIdTokenChanged to set up a callback that will be invoked every time the user's token changes. Again, you should only set up one of these globally for your app, and use its current value at the time of the request.

Related

onError Authentication with refresh token

In the Apollographql documentation it states:
The onError link can retry a failed operation based on the type of GraphQL error that's returned. For example, when using token-based authentication, you might want to automatically handle re-authentication when the token expires.
This is followed up by their sample code:
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
// Apollo Server sets code to UNAUTHENTICATED
// when an AuthenticationError is thrown in a resolver
case "UNAUTHENTICATED":
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
// Retry the request, returning the new observable
return forward(operation);
}
}
}
// To retry on network errors, we recommend the RetryLink
// instead of the onError link. This just logs the error.
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
My question is in regards to the getNewToken(), as no code was provided for this function, I want to know (assuming this is another request to the backend and I am not sure how it could not be), if you are able to and or supposed to use query/mutation in graphql or make the request through axios for example.
One problem, if it can/should be a graphql query or mutation, is to get the new token, the onError code is defined in the same file as the ApolloClient as ApolloClient needs access to onError, thus when trying to implement this as retrieving a new token through a graphql mutation I got the following error:
React Hook "useApolloClient" is called in function "refresh" that is
neither a React function component nor a custom React Hook function.
After trying to useQuery/useMutation hook and realizing I cannot outside of a react component and at the top level I found this post whose answers suggested you can use useApolloClient.mutate instead but I still ran into issues. My code was (and tried multiple iterations of this same code like useApolloClient() outside of the function and inside etc.):
const refresh = () => {
const client = useApolloClient();
const refreshFunc = () => {
client
.mutate({ mutation: GET_NEW_TOKEN })
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
};
refreshFunc();
};
I could capitalize Refresh but this still would not work and would break the rules of hooks.
And to clarify all the above would do is I would replace the console.logs with setting session storage to the retrieved new token and then re trying the original request with onError.
Now in another post I found when looking into this, the users getNewToken request was a rest request using axios:
const getNewToken = async () => {
try {
const { data } = await axios.post(
"https://xxx/api/v2/refresh",
{ token: localStorage.getItem("refreshToken") }
);
localStorage.setItem("refreshToken", data.refresh_token);
return data.access_token;
} catch (error) {
console.log(error);
}
};
Now from my understanding, if I wanted to implement it this way I would have to change my backend to include express as I am only using apolloserver. Now I could definitely be wrong about that as my backend knowledge is quite limited and would love to be corrected their.
So my question is, what is the best way to do this, whether natively using graphql queries/mutations (if possible), doing it with axios, or maybe their is another best practice for this seemingly common task I am unaware of.

Most ideal way to call firebase getIdToken

i am implementing user authentication with the help of firebase in my React project. So, I am confused over something.
I am verifying the user from firebase and then getting a token on frontend which is sent to backend via headers and verfied there once.
I read the docs and came to know that firebase token gets expired after 1 hr by default so we have to use "getIdToken" like
firebase.auth().onAuthStateChanged(async user => {
if (user) {
console.log(user, 'user123 inside firebaseAuth')
const token = await user.getIdToken()
Cookies.set('my_token', token, { domain: domain })
}
})
but how do i manage this function , do i have to call it everytime the component updates or everytime before hitting api or first time the component renders ?
The thing is i do not want this token to get expire until the user logs out himself / herself even if he is in a different component and sitting ideal for too long.
You can get the Firebase ID Token every time you are making an API call to your server:
async function callAPI() {
const user = firebase.auth().currentUser
if (user) {
const token = await user.getIdToken()
const res = await fetch("url", {
headers: {authorization: `Bearer ${token}`}
})
} else {
console.log("No user is logged in")
}
}
You could get the ID token once when the component mounts but then you'll have to deal with onIdTokenChanged to keep it updated in your state. Using the method above you'll get a valid token always.

Firebase Auth setCustomClaims() not working

I am facing a problem with setting custom claims for Firebase Authentication service's token. I am using Cloud function to set the custom claims for Hasura. The cloud function executes upon new user create event to set the custom claims. Here's my code running in cloud function
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.processSignup = functions.auth.user().onCreate(user => {
// create custom claims for hasura
const hasuraClaims = {
"x-hasura-default-role": "user",
"x-hasura-allowed-roles": ["user"],
"x-hasura-user-id": user.uid
}
// attach claims to user auth object
return admin.auth().setCustomUserClaims(user.uid, hasuraClaims)
.then(_ => {
functions.logger.info('SUCCESS: Custom claims attached');
})
.catch(err => {
console.log('ERROR: ', err);
})
})
In my frontend web page, I am running the following code to get the idToken
// subscribe to user state change
firebase.auth().onAuthStateChanged(async user => {
console.log('Firebase auth state changed');
if (user) {
// User is signed in.
window.User = user;
let idToken = await user.getIdTokenResult();
console.log('idToken: ', idToken);
}
})
I don't know what I'm doing wrong, but the token doesn't contain the custom claims that I've set in my Cloud function processSignup(). I know that the function executed without error because I can check my function logs and find the info entry SUCCESS: Custom claims attached.
Can anyone please help me solve this problem?
Updating claims does not trigger an onAuthStateChanged (the auth state of being logged in or not has not changed, but the users' claims have) and tokens are minted and then used for ~1h.
You are calling getIdTokenResult but not forcing a refresh, try:
let idToken = await user.getIdTokenResult(true);
which will force a new token to be fetched from the server and will (hopefully) include your custom claims.

Firebase Custom Claims doesn't propagate

I'm working in an Angular6 app with angularfire2. I'm setting the roles as custom claims in user creation, but it doesn't seem to propagate.
When I'm creating the user I send the userid, businessid and role to a cloud function:
bid > businessid
urole > role
req.body.uid > userid
const customClaims = {
roles: { [bid]: urole }
}
admin.auth().setCustomUserClaims(req.body.uid, customClaims)
.then(result => {
res
.status(200)
.send()
})
The problem is when the call to cloud function finishes and I want to redirect the user to a route which requires the user to have the custom claim set, but it fails. After some debugging, I've found out that if run:
this.angularFireAuth.auth.currentUser.getIdTokenResult(true).then(result => {
return result.claims.roles
})
immediately after the call to the cloud function "result.claims.roles" is undefined, but if I refresh the page, "result.claims.roles" have the data I set before.
I've already tried the reload method, and getIdToken(true) but I'm getting the same problem.
Is there a way to avoid refreshing the page and get the custom claims?
Thank you!
When the user is signed in, they get an ID token that is valid for about an hour. If you set a custom claim, their (server-side) profile is updated immediately, but their ID token is not auto-updated. So you'll need to refresh their ID token to get the new custom claims.
As far as I know this ID token is only refreshed by calling getIdTokenResult if it has expired. If that's the cause, calling user.reload() and then getting the ID token should give you the updated claims.
For me it simply worked taking the advice from one of the comments:
// --------
// Frontend
// --------
// Triggering the cloud function
const url: string = 'url-to-your-cloud-function'
await this.http.post<unknown>(url, {}).toPromise();
// After cloud function was run and custom claim was set -> refresh the id token
// The 'currentUser' is a reference to the firebase user
await this.authService.currentUser.getIdToken(true);
// --------
// Cloud Function - createSubscription
// --------
const createSubscription = () => {
await admin.auth().setCustomUserClaims(userId, {
subscriber: true
})
}

How should I handle JWT token refresh in my Redux app?

Our Redux application use JWT tokens for authentication. The access_token expires every 15 minutes and the refresh_token expires every 30 days. Both of them are provided by our API every time you log in and stored in the browser's local storage. If a secure endpoint receives a request with an expired token, it returns a 401 HTTP error.
Unfortunately, I don't know how to proceed to handle the refresh process without having a negative impact on the user. From a technical point of view, here is what I would like to achieve:
Action creator calls the API with an expired token
Client receives a 401 HTTP error
Client triggers a function that calls the API to obtain a new token (by providing the refresh token).
If the call fails (refresh_token is expired), prompt the user the re-enter its credentials to re-obtain both tokens then re-attempt the original request.
If the call succeeds, re-attempt the original request.
I would like to have a function that would handle the refreshing process and that would be called in the error handling portion of the action creator.
Here is what I have tried so far:
export function handleError(dispatch, current_func, error, handling) {
if(error.response) {
if(error.response.status === 401 && readToken("token") !== null) {
return attemptTokenRefresh(dispatch, current_func)
}
if(error.response.status === 422 && readToken("token") === null) {
return attemptTokenRefresh(dispatch, current_func)
}
}
return(handling())
}
export function attemptTokenRefresh(dispatch, on_success) {
let token = readToken("refresh_token");
let instance = axios.create({
headers: {"Authorization": token}
});
instance.post("api/refresh").then(response => {
if (response.data["token"]) {
storeToken("token", response.data["token"]);
on_success();
}
}).catch(error => {
//TODO: Allow user to sign back (prevent wiping the state)
});
}
dispatch refers to the dispatch function provided by Redux
current_func refers to the action creator
error refers to the error returned by the API
handling refers to the error handling function for other types of errors
Any help would be greatly appreciated :)

Categories

Resources