Hey Guys,
I found many examples trying to combine Hasura or other GraphQL projects with Auth0 and NextJS. Is there a best practise out there where to set the accessToken for the graphql client? I found several closed issues but haven't found one which showcases it with the current beta.
Thanks!
I currently try with following approach:
// apollo-config.ts
let apolloToken
export const checkAuth0Token = async () => {
if (typeof window !== 'undefined') {
const res = await fetch('/api/session')
if (res.ok) {
const ses = await res.json()
apolloToken = ses?.idToken ?? null
}
}
}
const authLink = setContext(async (_, { headers }) => {
await checkAuth0Token()
const currentHeaders = {
...headers
}
if (apolloToken) {
currentHeaders.authorization = `Bearer ${apolloToken}`
}
return {
headers: currentHeaders
}
})
(...) // apolloToken is now available for apollo client setup
// /api/session.ts
export default async function getIdToken(req: NextApiRequest, res: NextApiResponse) {
const ses = await getSession(req, res)
const resBody = {
idToken: ses?.idToken ?? null
}
res.status(200).json(resBody)
}
On first sight it seems to work but if the browser tab keeps open for quite a while the JWT Session Token expires and then the tab is not usable any longer with following error from the backend of Hasura:
Error: Could not verify JWT: JWTExpired
Do I miss anything in the Auth0 config or is the approach I use with calling /api/session not correct?
Hi @dohomi
Fetching the Access Token from the middleware is a reasonable approach. However, we don't have any documentation or examples for this use and we don't provide a bespoke api endpoint for doing this because, strictly speaking, you shouldn't be using an access token on a public client (the frontend) that you got with a confidential client (the backend).
We do however recognise that this is the only way to combine Hasura or other GraphQL projects with Auth0 and NextJS and we're currently looking into what sort of advice we can offer.
Error: Could not verify JWT: JWTExpired
To deal with your issue specifically - the first problem is that you should be using an Access Token to access a third party API, not an ID Token. You can get an access token using the getAccessToken helper.
If you also request a refresh token (by adding offline_access to your requested scopes) then getAccessToken will fetch you a new one when the old one expires.
something like:
AUTH0_SCOPE="openid profile email offline_access"
// pages/api/token.js
import { getAccessToken, withApiAuthRequired } from '@auth0/nextjs-auth0';
export default withApiAuthRequired(async function token(req, res) {
const { accessToken } = await getAccessToken(req, res);
res.status(200).json({ accessToken });
});
Fetch the Access Token in the front end and use it to access an External API directly.
const response = await fetch('/api/token');
const { accessToken } = await response.json();
@adamjmcgrath I want to authenticate the Hasua graphql with the authorization header. Thats not the accessToken as far I can see? I followed the Hasura NextJS example but it all seems pretty outdated.
@dohomi
I want to authenticate the Hasua graphql with the authorization header. Thats not the accessToken as far I can see?
ID tokens are meant for use by the application only. You should not use ID tokens to gain access to an API. Each token contains information for the intended audience, for your ID Token the audience (indicated by the aud claim) must be the client ID of the application making the authentication request (Your Next.js application, not your Graph QL API). If this is not the case, you should not trust the token.
I followed the Hasura NextJS example but it all seems pretty outdated.
Which one are you looking at, in this one (https://hasura.io/learn/graphql/nextjs-fullstack-serverless/apollo-client/) they're using the Access Token to access the API
@dohomi it's not the beta yet, but getAccessToken() should work the same. I have been using NextJS with Hasura using Auth0 for with the accessToken following this setup https://hasura.io/docs/1.0/graphql/core/guides/integrations/auth0-jwt.html#configure-auth0-rules-callback-urls and then I proxy all my frontend client requests through pages/api/graphql that does the Auth Headers for Hasura
// pages/api/graphql.js
import auth0 from '@lib/auth0';
import { GraphQLClient } from 'graphql-request';
const endpoint = process.env.HASURA_GRAPHQL_URL;
/**
* Check req for auth token from session.
* w/o it calls will have a 'pubic' role in the header
* calls to `tokenCache` throw on failure
* @param {Object} req request
* @param {Object} res response
*/
const userRequestHeader = async (req, res) => {
const headers = {};
try {
const tokenCache = auth0.tokenCache(req, res);
const { accessToken } = await tokenCache.getAccessToken();
headers['Authorization'] = `Bearer ${accessToken}`;
} catch (error) {
headers['X-Hasura-Role'] = 'public';
} finally {
return headers;
}
};
export default async function graphql(req, res) {
try {
// Get user acces_token IF available
const headers = await userRequestHeader(req, res);
const graphQLClient = new GraphQLClient(endpoint);
graphQLClient.setHeaders(headers);
const { query, variables } = req.body;
const data = await graphQLClient.request(query, variables);
res.send(data);
} catch (error) {
console.error(error);
res.status(error.status || 500).json({
code: error.code,
error: error.message,
});
}
}
then in the app I can make my query and mutations like this
function fetchGQL(operationsDoc, variables = {}) {
return fetch('/api/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: operationsDoc,
variables,
}),
})
hope that helps
@dohomi Closing this - feel free to ping me if you want me to reopen
@adamjmcgrath @jtomchak
Sorry I haven't had time to follow up yet. But now I am trying to archive it. My checklist:
Calling any gql query in ApolloClient results in:
Error: Could not verify JWT: JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 1)
== example console.log:
Bearer Ed8QcUP1psUrSyKrNZ7Y93Voysem8NiU
The accessToken is also very short, it seems there is something missing. So why is the getAccessToken so small? If I drop it into the Hasura Console It also states that the access token is not valid.
Do I need to enable auto rotation inside of the Auth0 web app? Do I need to set up an Auth0 API to generate the token?
Thanks in helping out
I was trying to find a solution but all other projects are using auth0-react and there they use the idToken and not the accessToken
https://github.com/hasura/graphql-engine/issues/5676#issuecomment-688039826
Not sure how that would be applied with nextjs-auth0
For testing purpose I was adjusting my get-access-token to see if I get the app running:
const getToken = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession(req, res)
const accessToken = await getAccessToken(req, res)
res.status(200).json({ accessToken, session })
}
export default withApiAuthRequired(getToken)
If i use now the token from session.idToken the token is valid to do queries from Hasura. I am just not sure as discussed above this seems not the right approach?
I stacked same travel.
I would like to refresh id token by using refresh token. does sdk have that kind of function?
I'm not very familiar with it, so I'm really sorry if I make mistakes.... :bow::bow::bow:
@dohomi - Have a look at https://hasura.io/docs/latest/graphql/core/guides/integrations/auth0-jwt.html#create-an-auth0-api
you also need to create an API so that the access token issued by Auth0 is following the JWT standard
Navigate to the Auth0 dashboard.
Click on the APIs menu option on the left sidebar and then click the + Create API button.
In the New API window, set a name for your API and enter an identifier (e.g. hasura)
In your application code, configure your API identifier as the audience when initializing Auth0, e.g.:
The "API Identifier" should be specified as the audience (you can use the env var AUTH0_AUDIENCE or authorizationParams.audience see https://github.com/auth0/nextjs-auth0/blob/beta/EXAMPLES.md#access-an-external-api-from-an-api-route)
I would like to refresh id token by using refresh token. does sdk have that kind of function?
Hi @bino98 - you can't refresh the ID Token (unless you login again). If you are trying to refresh the ID Token, this is probably a sign that you should be using the Access Token instead.
@adamjmcgrath The documentation of Hasura states that in case I use the SPA version of Auth0, but I am using a Regular Web Application as suggested by the Readme of nextjs-auth0. I think there is quite a confusion and mixtures in the documentation how that whole setup needs to look like (https://hasura.io/docs/latest/graphql/core/guides/integrations/auth0-jwt.html#create-an-auth0-application). I currently followed the docs to do:
After adding an additional API and setting the audience I receive: "claims key: 'https://hasura.io/jwt/claims' not found". Where do I need to set that? I did set up the Auth0 Rule actually
Ok I finally found a working setup, I had to adjust the custom Auth0 rule to following:
function (user, context, callback) {
const namespace = "https://hasura.io/jwt/claims";
const claim = {
'x-hasura-default-role': 'user',
// do some custom logic to decide allowed roles
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.user_id
};
if(context.idToken){
context.idToken[namespace] = claim;
}
if(context.accessToken){
context.accessToken[namespace] = claim;
}
callback(null, user, context);
}
Now the claims are attached correctly and the Hasura Authentication works with the Regular Web Application. Is it recommend to enable auto rotating tokens? Thanks @adamjmcgrath for your help.
Is it recommend to enable auto rotating tokens?
It's compulsory for public clients, but for confidential clients (like your nextjs-auth0 client) it's optional. The breach detection makes it more secure if your RT is stolen, but it also makes it more brittle - in highly distributed environments with lots of requests you can get FERRT errors, when breach detection is unwantedly triggered due to concurrent requests causing the IDP to mistakingly think that stale RTs are being used.
I'm a little confused. What's the recommended approach if I want to use hasura for backend and nextjs + apolloclient for frontend (mostly for its client-side cache) with lots of additional graphql requests to hasura after the initial page load (e.g. a dynamic private dashboard page or a messaging app)?
4 solutions are floating around but I haven't found a comparison other than "be aware of the security risks".
nextjs-auth0 + regular web app + /pages/api/token.js endpoint (see this comment by @adamjmcgrath)nextjs-auth0 + regular web app + /pages/api/proxy.js (e.g. with http-proxy-middleware)auth0-spa-js + single page app (which means no SSR)@kratam I found that this blog post is useful to a certain degree, but it also introduces even more possibilities (like using auth0-react and auth0.js).
Also, initially, I was thinking of using ApolloLink to add the Authorization Bearer token, which is explicitly mentioned as a use case in the docs for ApolloLink, but now I'm just confused by all the available options.
auth0-react uses auth0-spa-js under the hood, so it's the same approach. You can only use the ApolloLink/authLink if the client has access to the token (via token.js or via the auth flow of auth0-spa-js/auth0-react). If you use a proxy, you have to extract the token from the cookie and modify the request headers on the fly.
Right now I'm using example 1 because it allows me to use SSR for the initial render and a direct connection between the client and hasura for the updates (e.g. refetching the same query). But this comment suggests that it's not an ideal setup... Maybe a combination where the auth flow returns 2 sets of tokens, one for the session cookie (which can be used for SSR) and one for the client with different lifetimes and settings?
Most helpful comment
Ok I finally found a working setup, I had to adjust the custom Auth0 rule to following:
Now the claims are attached correctly and the Hasura Authentication works with the Regular Web Application. Is it recommend to enable auto rotating tokens? Thanks @adamjmcgrath for your help.