auth0 / nextjs-auth0

Next.js SDK for signing in with Auth0
MIT License
2.07k stars 389 forks source link

How to get accesstoken client-side #67

Closed BjoernRave closed 3 years ago

BjoernRave commented 4 years ago

So I am using graphql and instantiating apollo-client in a hoc wrapped around app.js. Here I am passing the accessToken I get from const session = await auth0.getSession(ctx.ctx.req) as Header to my client so it gets send to the server and the request can be verified. Now I get the error: Error in error pagegetInitialProps: Error: "The getSession method can only be used from the server side" So I am wondering how I should be able to get the accessToken on the client side. I guess I could store the accessToken on a cookie, but that wouldnt feel right

vgrafe commented 4 years ago

I wish I had a better solution, but could not find one and ended up storing the idToken in a cookie.

matthieuh commented 4 years ago

I would suggest you to avoid using the idToken on the client side. If you need to authentify http/ws calls I would suggest you to proxify your calls putting the bearer token on server side. For example you can have a pages/api/fetch.js page using http-proxy-middleware.

vgrafe commented 4 years ago

That's a good suggestion. I hope I can put some time in fixing my public repo soon. Thanks!

matthieuh commented 4 years ago

@vgrafe if you struggle on this feel free to ask :) I have some code samples

vgrafe commented 4 years ago

@matthieuh I'd love to see that! I can't spend lots of time on this repo these days, and since it has a few stars I'd love some help providing best practices rather than something that is not recommended. Feel free to post on my repo's issue or here.

matthieuh commented 4 years ago

Hello @vgrafe I have a pages/api/graphql.js file with something like this:

import proxy from 'http-proxy-middleware';
import express from 'express';
import auth0 from '../../utils/auth0';

const app = express();

app.use('*', async (req, _, next) => {
  const session = = await auth0.getSession(req);

  return proxy({
    target: process.env.GRAPHQL_URL,
    changeOrigin: true,
    proxyTimeout: 5000,
    secure: false,
    headers: {
      Connection: 'keep-alive'
    },
    pathRewrite: {
      '^/api/graphql': ''
    },
    onError: (err, req, res) => {
      console.log('err', err, res.data);
      res.writeHead(500, {
        'Content-Type': 'text/plain'
      });
      res.end(
        'Something went wrong. And we are reporting a custom error message.'
      );
    },
    onProxyReq: async (proxyReq, req, res) => {
      if (session) {
        proxyReq.setHeader('Authorization', `Bearer ${session.idToken}`);
      }

      if (req.body) {
        let bodyData = JSON.stringify(req.body);
        // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json
        proxyReq.setHeader('Content-Type', 'application/json');
        proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
        // stream the content
        proxyReq.write(bodyData);
      }
    }
  })(req, _, next);
});

export default app;

Then you just need to call api/graphql as graphql endpoint without taking care at all of the bearer token.

Something to note is that this proxy will not work with subscription which are using websocket. So to make it works I added an other file pages/api/graphql-ws.js with


import proxy from 'http-proxy-middleware';
import express from 'express';
import auth0 from '../../utils/auth0';

const app = express();

app.use('*', async (req, _, next) => {
  const session = await auth0.getSession(req);

  const wsProxy = proxy({
    target: process.env.GRAPHQL_URL,
    changeOrigin: true,
    logLevel: 'debug',
    ws: true,
    timeout: 30000,
    proxyTimeout: 30000,
    pathRewrite: {
      '^/api/graphql-ws': ''
    },
    onProxyReqWs: proxyReq => {
      if (session) {
        proxyReq.setHeader('Authorization', `Bearer ${session.idToken}`);
      }
    }
  });

  app.on('upgrade', wsProxy.upgrade);

  return wsProxy(req, _, next);
});

export default app;`
shansmith01 commented 4 years ago

Thanks for the above code @matthieuh. Any chance you can provide the client side code?

vgrafe commented 4 years ago

Wow! Thank you. I'll update my repo sometime this week.

Pytal commented 4 years ago

@matthieuh does this work with the Next.js serverless target?

jagyas commented 4 years ago

Hi, I have also created one proxy for graphql. I have api/graphql.js file with below gist code:

https://gist.github.com/jagyas/a120fa57349e2f7f854f97d18c4dfd87

I have created above code mainly for hiding Graphql endpoint.

If we will fetch session in same code and then pass it to graphql endpoint, I think it will also help to hide idToken.

Let me know your views @vgrafe @matthieuh @Pytal

Widcket commented 3 years ago

Hi everyone, with the new v1.0.0-beta.0 release we have included a way to use an access token from the frontend. However, keep in mind that it is less secure than proxying the requests through API routes, as the access token could be stolen via XSS. Please read Comparison with auth0-react, as auth0-react might be a better fit for your project if that's the primary way of fetching data in your application.

mathiasfc commented 3 years ago

Hi everyone, with the new v1.0.0-beta.0 release we have included a way to use an access token from the frontend. However, keep in mind that it is less secure than proxying the requests through API routes, as the access token could be stolen via XSS. Please read Comparison with auth0-react, as auth0-react might be a better fit for your project if that's the primary way of fetching data in your application.

Sorry, I couldn't access the token on the client side, could you provide an example? ty

adamjmcgrath commented 3 years ago

Hi @mathiasfc - we've removed the advice on the recommendation of our security experts, but you can still find it here https://github.com/auth0/nextjs-auth0/pull/245

Also checkout https://github.com/auth0/nextjs-auth0/issues/201#issuecomment-761106265

oliverlevay commented 3 years ago

Hi @mathiasfc - we've removed the advice on the recommendation of our security experts, but you can still find it here #245

Also checkout #201 (comment)

Hey, I built a middleware for our graphql requests that can pickup the token and send it through (found the code when googling but I forget where):

import request from 'request';
import util from 'util';

import { getAccessToken } from '@auth0/nextjs-auth0';

const graphql = async (req, res) => {
  try {
    const { accessToken } = await getAccessToken(req, res, {
      audience: process.env.AUTH0_AUDIENCE,
    });
    const headers = {
      // Attach token to header
      Authorization: accessToken ? `Bearer ${accessToken}` : '',
      // Set content type to JSON
      'Content-Type': 'application/json',
    };
    console.log(headers);
    const asyncReqPost = util.promisify(request.post);
    // Send request
    const graphQLApiResponse = await asyncReqPost({
      url: process.env.GRAPHQL_URL,
      headers,
      json: req.body,
      timeout: 5000, // give queries more time to run
      gzip: true,
    });
    // Set response header
    res.setHeader('Content-Type', 'application/json');
    // Send response
    res.end(JSON.stringify(graphQLApiResponse.body));
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
};

export default graphql;

But we do a lot of requests, and we get a Too Many Requests exception at /userinfo

thatisuday commented 2 years ago

You don't need a complicated config. Just follow this answer: https://github.com/vercel/next.js/discussions/11036#discussioncomment-16147

Also export this from your API handler: https://nextjs.org/docs/api-routes/api-middlewares#custom-config

export const config = {
  api: {
    bodyParser: false,
    externalResolver: true,
  },
};
dariosky commented 2 years ago

I'm adding to this same question - I'm also trying to create a thin proxy, using next-http-proxy-middleware to retrieve the session an add the Authorization header. The issue is that whenever I call getSession (or getAccessToken) something is sent to the client, so setting a header I get:

Cannot set headers after they are sent to the client

I also tried mocking the res parameter sent to getSession but without luck - how can I get the session readonly without interfering with the response?

guven8 commented 1 year ago

Hello @vgrafe I have a pages/api/graphql.js file with something like this:

import proxy from 'http-proxy-middleware';
import express from 'express';
import auth0 from '../../utils/auth0';

const app = express();

app.use('*', async (req, _, next) => {
  const session = = await auth0.getSession(req);

  return proxy({
    target: process.env.GRAPHQL_URL,
    changeOrigin: true,
    proxyTimeout: 5000,
    secure: false,
    headers: {
      Connection: 'keep-alive'
    },
    pathRewrite: {
      '^/api/graphql': ''
    },
    onError: (err, req, res) => {
      console.log('err', err, res.data);
      res.writeHead(500, {
        'Content-Type': 'text/plain'
      });
      res.end(
        'Something went wrong. And we are reporting a custom error message.'
      );
    },
    onProxyReq: async (proxyReq, req, res) => {
      if (session) {
        proxyReq.setHeader('Authorization', `Bearer ${session.idToken}`);
      }

      if (req.body) {
        let bodyData = JSON.stringify(req.body);
        // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json
        proxyReq.setHeader('Content-Type', 'application/json');
        proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
        // stream the content
        proxyReq.write(bodyData);
      }
    }
  })(req, _, next);
});

export default app;

Then you just need to call api/graphql as graphql endpoint without taking care at all of the bearer token.

Something to note is that this proxy will not work with subscription which are using websocket. So to make it works I added an other file pages/api/graphql-ws.js with

import proxy from 'http-proxy-middleware';
import express from 'express';
import auth0 from '../../utils/auth0';

const app = express();

app.use('*', async (req, _, next) => {
  const session = await auth0.getSession(req);

  const wsProxy = proxy({
    target: process.env.GRAPHQL_URL,
    changeOrigin: true,
    logLevel: 'debug',
    ws: true,
    timeout: 30000,
    proxyTimeout: 30000,
    pathRewrite: {
      '^/api/graphql-ws': ''
    },
    onProxyReqWs: proxyReq => {
      if (session) {
        proxyReq.setHeader('Authorization', `Bearer ${session.idToken}`);
      }
    }
  });

  app.on('upgrade', wsProxy.upgrade);

  return wsProxy(req, _, next);
});

export default app;`

thanks for this example, could you show how you are calling your websocket proxy route graphql-ws endpoint client side? I'm using Apollo's GraphQLWSLink which requires a full websocket url, if I pass /api/graphql-ws is throws Failed to construct 'WebSocket': The URL '/api/graphql-ws' is invalid.

my websocket looks like this

new GraphQLWsLink( createClient({ url: '/api/graphql-ws' }) );

liamlows commented 6 months ago

Hello @vgrafe I have a pages/api/graphql.js file with something like this:

import proxy from 'http-proxy-middleware';
import express from 'express';
import auth0 from '../../utils/auth0';

const app = express();

app.use('*', async (req, _, next) => {
  const session = = await auth0.getSession(req);

  return proxy({
    target: process.env.GRAPHQL_URL,
    changeOrigin: true,
    proxyTimeout: 5000,
    secure: false,
    headers: {
      Connection: 'keep-alive'
    },
    pathRewrite: {
      '^/api/graphql': ''
    },
    onError: (err, req, res) => {
      console.log('err', err, res.data);
      res.writeHead(500, {
        'Content-Type': 'text/plain'
      });
      res.end(
        'Something went wrong. And we are reporting a custom error message.'
      );
    },
    onProxyReq: async (proxyReq, req, res) => {
      if (session) {
        proxyReq.setHeader('Authorization', `Bearer ${session.idToken}`);
      }

      if (req.body) {
        let bodyData = JSON.stringify(req.body);
        // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json
        proxyReq.setHeader('Content-Type', 'application/json');
        proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
        // stream the content
        proxyReq.write(bodyData);
      }
    }
  })(req, _, next);
});

export default app;

Then you just need to call api/graphql as graphql endpoint without taking care at all of the bearer token. Something to note is that this proxy will not work with subscription which are using websocket. So to make it works I added an other file pages/api/graphql-ws.js with

import proxy from 'http-proxy-middleware';
import express from 'express';
import auth0 from '../../utils/auth0';

const app = express();

app.use('*', async (req, _, next) => {
  const session = await auth0.getSession(req);

  const wsProxy = proxy({
    target: process.env.GRAPHQL_URL,
    changeOrigin: true,
    logLevel: 'debug',
    ws: true,
    timeout: 30000,
    proxyTimeout: 30000,
    pathRewrite: {
      '^/api/graphql-ws': ''
    },
    onProxyReqWs: proxyReq => {
      if (session) {
        proxyReq.setHeader('Authorization', `Bearer ${session.idToken}`);
      }
    }
  });

  app.on('upgrade', wsProxy.upgrade);

  return wsProxy(req, _, next);
});

export default app;`

thanks for this example, could you show how you are calling your websocket proxy route graphql-ws endpoint client side? I'm using Apollo's GraphQLWSLink which requires a full websocket url, if I pass /api/graphql-ws is throws Failed to construct 'WebSocket': The URL '/api/graphql-ws' is invalid.

my websocket looks like this

new GraphQLWsLink( createClient({ url: '/api/graphql-ws' }) );

Did you ever find a way to do this proxy with the websocket url? We are facing the same issue now and due to the library only accepting websocket URLs we cannot proxy any of the traffic through nextjs api/rewrites/middleware.