admin管理员组

文章数量:1399306

I am currently implementing an authentication flow using Nextjs and an api using Expressjs.

I am looking to store a JWT token as an auth token in memory that I can periodically refresh using a refresh token stored in an HTTPOnly cookie.

For my implementation, I took as a reference the nice OSS project here.

My issue here is that when I am storing the auth token in inMemoryToken during login, the value is stored only available client side but still available server side and vice versa.

Another example is when I am disconnecting:

  • inMemoryToken is equal to something server side
  • user click the logout button and logout() is called front side and inMemoryToken = null
  • on page change, getServerSideProps() is called on the server but inMemoryToken on the server is still equal to the previous value and therefore my user still appear connected.

Here is the Nextjs code

//auth.js
import { Component } from 'react';
import Router from 'next/router';
import { serialize } from 'cookie';
import { logout as fetchLogout, refreshToken } from '../services/api';

let inMemoryToken;

export const login = ({ accessToken, accessTokenExpiry }, redirect) => {
    inMemoryToken = {
        token: accessToken,
        expiry: accessTokenExpiry,
    };
    if (redirect) {
        Router.push('/');
    }
};

export const logout = async () => {
    inMemoryToken = null;
    await fetchLogout();
    window.localStorage.setItem('logout', Date.now());
    Router.push('/');
};

const subMinutes = (dt, minutes) => {
    return new Date(dt.getTime() - minutes * 60000);
};

export const withAuth = (WrappedComponent) => {
    return class extends Component {
        static displayName = `withAuth(${Component.name})`;

        state = {
            accessToken: this.props.accessToken,
        };
        async ponentDidMount() {
            this.interval = setInterval(async () => {
                inMemoryToken = null;
                const token = await auth();
                inMemoryToken = token;
                this.setState({ accessToken: token });
            }, 60000);
            window.addEventListener('storage', this.syncLogout);
        }

        ponentWillUnmount() {
            clearInterval(this.interval);
            window.removeEventListener('storage', this.syncLogout);
            window.localStorage.removeItem('logout');
        }

        syncLogout(event) {
            if (event.key === 'logout') {
                Router.push('/');
            }
        }

        render() {
            return (
                <WrappedComponent
                    {...this.props}
                    accessToken={this.state.accessToken}
                />
            );
        }
    };
};

export const auth = async (ctx) => {
    console.log('auth ', inMemoryToken);
    if (!inMemoryToken) {
        inMemoryToken = null;
        const headers =
            ctx && ctx.req
                ? {
                      Cookie: ctx.req.headers.cookie ?? null,
                  }
                : {};
        await refreshToken(headers)
            .then((res) => {
                if (res.status === 200) {
                    const {
                        access_token,
                        access_token_expiry,
                        refresh_token,
                        refresh_token_expiry,
                    } = res.data;
                    if (ctx && ctx.req) {
                        ctx.res.setHeader(
                            'Set-Cookie',
                            serialize('refresh_token', refresh_token, {
                                path: '/',
                                expires: new Date(refresh_token_expiry),
                                httpOnly: true,
                                secure: false,
                            }),
                        );
                    }
                    login({
                        accessToken: access_token,
                        accessTokenExpiry: access_token_expiry,
                    });
                } else {
                    let error = new Error(res.statusText);
                    error.response = res;
                    throw error;
                }
            })
            .catch((e) => {
                console.log(e);
                if (ctx && ctx.req) {
                    ctx.res.writeHead(302, { Location: '/auth' });
                    ctx.res.end();
                } else {
                    Router.push('/auth');
                }
            });
    }
    const accessToken = inMemoryToken;
    if (!accessToken) {
        if (!ctx) {
            Router.push('/auth');
        }
    }
    return accessToken;
};

//page index.js

import Head from 'next/head';
import { Layout } from '../ponents/Layout';
import { Navigation } from '../ponents/Navigation';
import { withAuth, auth } from '../libs/auth';

const Home = ({ accessToken }) => (
    <Layout>
        <Head>
            <title>Home</title>
        </Head>
        <Navigation />
        <div>
            <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
                eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
                enim ad minim veniam, quis nostrud exercitation ullamco laboris
                nisi ut aliquip ex ea modo consequat. Duis aute irure dolor
                in reprehenderit in voluptate velit esse cillum dolore eu fugiat
                nulla pariatur. Excepteur sint occaecat cupidatat non proident,
                sunt in culpa qui officia deserunt mollit anim id est laborum.
            </p>
        </div>
    </Layout>
);

export const getServerSideProps = async (ctx) => {
    const accessToken = await auth(ctx);
    return {
        props: { accessToken: accessToken ?? null },
    };
};

export default withAuth(Home);

Part of the express js code:

app.post('/api/login', (req, res) => {
    const { username, password } = req.body;

    ....

    const refreshToken = uuidv4();
    const refreshTokenExpiry = new Date(new Date().getTime() + 10 * 60 * 1000);

    res.cookie('refresh_token', refreshToken, {
        maxAge: 10 * 60 * 1000,
        httpOnly: true,
        secure: false,
    });

    res.json({
        access_token: accessToken,
        access_token_expiry: accessTokenExpiry,
        refresh_token: refreshToken,
        user,
    });
});

app.post('/api/refresh-token', (req, res) => {
    const refreshToken = req.cookies['refresh_token'];
    
    .....

    const newRefreshToken = uuidv4();
    const newRefreshTokenExpiry = new Date(
        new Date().getTime() + 10 * 60 * 1000,
    );

    res.cookie('refresh_token', newRefreshToken, {
        maxAge: 10 * 60 * 1000,
        httpOnly: true,
        secure: false,
    });

    res.json({
        access_token: accessToken,
        access_token_expiry: accessTokenExpiry,
        refresh_token: newRefreshToken,
        refresh_token_expiry: newRefreshTokenExpiry,
    });
});

app.post('/api/logout', (_, res) => {
    res.clearCookie('refresh_token');
    res.sendStatus(200);
});

What I understand is that even if the let inMemoryToken is declared once, two separate instance of it will be available at runtime, one client side and one server side, and modifying on doesn't affect the other. Am I right?

In this case, how to solved this since the auth method can be called on the server but also on the client?

I am currently implementing an authentication flow using Nextjs and an api using Expressjs.

I am looking to store a JWT token as an auth token in memory that I can periodically refresh using a refresh token stored in an HTTPOnly cookie.

For my implementation, I took as a reference the nice OSS project here.

My issue here is that when I am storing the auth token in inMemoryToken during login, the value is stored only available client side but still available server side and vice versa.

Another example is when I am disconnecting:

  • inMemoryToken is equal to something server side
  • user click the logout button and logout() is called front side and inMemoryToken = null
  • on page change, getServerSideProps() is called on the server but inMemoryToken on the server is still equal to the previous value and therefore my user still appear connected.

Here is the Nextjs code

//auth.js
import { Component } from 'react';
import Router from 'next/router';
import { serialize } from 'cookie';
import { logout as fetchLogout, refreshToken } from '../services/api';

let inMemoryToken;

export const login = ({ accessToken, accessTokenExpiry }, redirect) => {
    inMemoryToken = {
        token: accessToken,
        expiry: accessTokenExpiry,
    };
    if (redirect) {
        Router.push('/');
    }
};

export const logout = async () => {
    inMemoryToken = null;
    await fetchLogout();
    window.localStorage.setItem('logout', Date.now());
    Router.push('/');
};

const subMinutes = (dt, minutes) => {
    return new Date(dt.getTime() - minutes * 60000);
};

export const withAuth = (WrappedComponent) => {
    return class extends Component {
        static displayName = `withAuth(${Component.name})`;

        state = {
            accessToken: this.props.accessToken,
        };
        async ponentDidMount() {
            this.interval = setInterval(async () => {
                inMemoryToken = null;
                const token = await auth();
                inMemoryToken = token;
                this.setState({ accessToken: token });
            }, 60000);
            window.addEventListener('storage', this.syncLogout);
        }

        ponentWillUnmount() {
            clearInterval(this.interval);
            window.removeEventListener('storage', this.syncLogout);
            window.localStorage.removeItem('logout');
        }

        syncLogout(event) {
            if (event.key === 'logout') {
                Router.push('/');
            }
        }

        render() {
            return (
                <WrappedComponent
                    {...this.props}
                    accessToken={this.state.accessToken}
                />
            );
        }
    };
};

export const auth = async (ctx) => {
    console.log('auth ', inMemoryToken);
    if (!inMemoryToken) {
        inMemoryToken = null;
        const headers =
            ctx && ctx.req
                ? {
                      Cookie: ctx.req.headers.cookie ?? null,
                  }
                : {};
        await refreshToken(headers)
            .then((res) => {
                if (res.status === 200) {
                    const {
                        access_token,
                        access_token_expiry,
                        refresh_token,
                        refresh_token_expiry,
                    } = res.data;
                    if (ctx && ctx.req) {
                        ctx.res.setHeader(
                            'Set-Cookie',
                            serialize('refresh_token', refresh_token, {
                                path: '/',
                                expires: new Date(refresh_token_expiry),
                                httpOnly: true,
                                secure: false,
                            }),
                        );
                    }
                    login({
                        accessToken: access_token,
                        accessTokenExpiry: access_token_expiry,
                    });
                } else {
                    let error = new Error(res.statusText);
                    error.response = res;
                    throw error;
                }
            })
            .catch((e) => {
                console.log(e);
                if (ctx && ctx.req) {
                    ctx.res.writeHead(302, { Location: '/auth' });
                    ctx.res.end();
                } else {
                    Router.push('/auth');
                }
            });
    }
    const accessToken = inMemoryToken;
    if (!accessToken) {
        if (!ctx) {
            Router.push('/auth');
        }
    }
    return accessToken;
};

//page index.js

import Head from 'next/head';
import { Layout } from '../ponents/Layout';
import { Navigation } from '../ponents/Navigation';
import { withAuth, auth } from '../libs/auth';

const Home = ({ accessToken }) => (
    <Layout>
        <Head>
            <title>Home</title>
        </Head>
        <Navigation />
        <div>
            <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
                eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
                enim ad minim veniam, quis nostrud exercitation ullamco laboris
                nisi ut aliquip ex ea modo consequat. Duis aute irure dolor
                in reprehenderit in voluptate velit esse cillum dolore eu fugiat
                nulla pariatur. Excepteur sint occaecat cupidatat non proident,
                sunt in culpa qui officia deserunt mollit anim id est laborum.
            </p>
        </div>
    </Layout>
);

export const getServerSideProps = async (ctx) => {
    const accessToken = await auth(ctx);
    return {
        props: { accessToken: accessToken ?? null },
    };
};

export default withAuth(Home);

Part of the express js code:

app.post('/api/login', (req, res) => {
    const { username, password } = req.body;

    ....

    const refreshToken = uuidv4();
    const refreshTokenExpiry = new Date(new Date().getTime() + 10 * 60 * 1000);

    res.cookie('refresh_token', refreshToken, {
        maxAge: 10 * 60 * 1000,
        httpOnly: true,
        secure: false,
    });

    res.json({
        access_token: accessToken,
        access_token_expiry: accessTokenExpiry,
        refresh_token: refreshToken,
        user,
    });
});

app.post('/api/refresh-token', (req, res) => {
    const refreshToken = req.cookies['refresh_token'];
    
    .....

    const newRefreshToken = uuidv4();
    const newRefreshTokenExpiry = new Date(
        new Date().getTime() + 10 * 60 * 1000,
    );

    res.cookie('refresh_token', newRefreshToken, {
        maxAge: 10 * 60 * 1000,
        httpOnly: true,
        secure: false,
    });

    res.json({
        access_token: accessToken,
        access_token_expiry: accessTokenExpiry,
        refresh_token: newRefreshToken,
        refresh_token_expiry: newRefreshTokenExpiry,
    });
});

app.post('/api/logout', (_, res) => {
    res.clearCookie('refresh_token');
    res.sendStatus(200);
});

What I understand is that even if the let inMemoryToken is declared once, two separate instance of it will be available at runtime, one client side and one server side, and modifying on doesn't affect the other. Am I right?

In this case, how to solved this since the auth method can be called on the server but also on the client?

Share Improve this question edited Oct 16, 2020 at 12:24 Florian Ldt asked Oct 15, 2020 at 21:20 Florian LdtFlorian Ldt 1,2353 gold badges17 silver badges33 bronze badges
Add a ment  | 

1 Answer 1

Reset to default 3

I went and created an example that shows how you can use sessions to store information in memory for individual users across requests. You can check out the codesandbox at the bottom if you are just interested in the code.

There are two things to keep in mind:

  1. you can't directly access variables declared on your server on your client
  2. variables declared on the server in the global scope are shared across requests

However, you can use a mon ID stored in a cookie on the client and store data in memory and access it via a session on the request.

When you have a request ining, you can check to see if the cookie exists on request's header, if it exists try to load the session from memory, if it doesn't exists or can't be loaded, create a new session.

| Ining Request
|   |--> Check the cookie header for your session key
|       |--> If cookie exists load cookie
|       |--> Else create session + use 'set-cookie' header to tell the client it's session key
|          |--> Do stuff with the data stored in the session  

To be able to do that, we need to have some way to store the sessions and the data associated with them. You said that you just want to store the data in memory.

const memoryStore = new Map();

Okay now we have are memory store, but how do we make it persist across requests? Let's store it as a global object.

const MEMORY_STORE = Symbol.for('__MEMORY_STORE');
const getMemoryStore = () => {
  if (!global[MEMORY_STORE]) {
    global[MEMORY_STORE] = new Map();
  }
  return global[MEMORY_STORE];
};

Perfect, now we can call getMemoryStore to access the persisted data. Now we want to create a handler that tries to load the session from the request, otherwise creating a new session.

const SESSION_KEY = '__my_session_id';

const loadSession = (req, res) => {
  const memory = getMemoryStore();

  const cookies = parseCookies(req.headers.cookie);
  const cookieSession = cookies[SESSION_KEY];

  // check to make sure that cookieSession is defined and that it exists in the memory store
  if (cookieSession && memory.has(cookieSession)) {
    const session = memory.get(cookieSession);
    req.session = session;
    
    // do something with the session

  } else {
    // okay the session doesn't exists so we need to create one, create the unique session id
    const sessionId = uuid();
    const session = { id: sessionId };
    memory.set(sessionId, session);
   
    // set the set-cookie header on the response with the session ID
    res.setHeader('set-cookie', `${SESSION_KEY}=${sessionId}`);

    req.session = session;
  }  
};

Now we can call that anywhere on the server side and it will load or create the session. For instance, you can call it from getServerSideProps

export const getServerSideProps = ({ req, res }) => {
  loadSession(req, res);

  // our session exists on req.session !! 
  
  return { props: { ... } };
};

I made a codesandbox that has a working example: https://codesandbox.io/s/distracted-water-biicc?file=/utils/app.js

本文标签: javascriptNextjsAuth token stored in memoryrefresh token in HTTP Only cookieStack Overflow