0
votes

I'm working on a Nuxt project (for the first time) where the API that I'm consuming requires us to authenticate via client oAuth2. The application holds the client ID & client secret but I don't want to expose this data to the user.

My plan is to retrieve the generated bearer token on the server side using the oAuth token endpoint. Then I can locally store the generated token on the filesystem and push it through to the user.

The problem I'm having is that I can't work out how best to store the bearer token. I've created a serverMiddleware that I'm trying to use FS to write the generated token to file. The problem with this is that I can't actually seem to use FS as it throws an error when being required in. I thought the idea of serverMiddleware is that it always runs on the server side?

I'm getting -

[HMR] bundle 'client' has 1 warnings
client.js?1b93:196 ./middleware/server/bearerToken.tsModule not found: Error: Can't resolve 'fs' in '/middleware/server'

Here's the middleware I've created -

const myServerMiddleware = (req, res, next) => {

    try {
        const fs = require('fs');

        // ToDo: read and/or generate bearer token

    } catch (e) {
        console.log('No FS available', e);
    }

    next();
};

export default {
    path: '/',
    handler: myServerMiddleware,
    mode: 'server'
};

And this is being loaded within my configuration using -

{
  serverMiddleware: [
    '@/middleware/server/bearerToken.ts'
  ],
}

I hope someone can point me in the right direction here!

1

1 Answers

1
votes

For anyone in the same boat...

Server middleware didn't seem to cut it but normal middleware did. Thought this could be useful to someone...

Within my nuxt.config.js I've defined the API's authentication parameters and set the middleware that the router should use. These actually load from .env file at runtime:

    // Public variables
    publicRuntimeConfig: {
        apiUrl: process.env.API_URL,
        name: process.env.APP_NAME
    },

    // Private variables
    privateRuntimeConfig: {
        apiClientId: process.env.API_CLIENT,
        apiClientSecret: process.env.API_SECRET,
    },

    // Apply global middleware to our routing.
    router: {
        middleware: [
            'bearer-token'
        ],
    },

Now for the middleware. I've created a file called bearer-token.ts within the middleware folder which automatically enables it using the config above. It turns out at this stage of execution you can actually check whether we are at the server or client level by checking that process.server is truthy. As long as we are at the server level, we can manipulate the filesystem using node's fs package.

Here is my 'completed' middleware (in typescript)... Notice that I'm using the Vuex to store my generated key, this allows me to push the generated bearer token through to the user. I'm also updating the modification time of the generated file based on the token expiry so I can easily check when the token needs regenerating, saving excessive calls to the oAuth server.

import {Context} from '@nuxt/types';

const getToken = (context: Context) => {
    const axios = require('axios');

    return axios.post('/oauth/token', {
        grant_type: 'client_credentials',
        client_id: context.$config.apiClientId,
        client_secret: context.$config.apiClientSecret,
        scope: ''
    }, {
        baseURL: context.$config.apiUrl,
        headers: {'Accept': 'application/json'}
    });
}

export default function (context: Context) {

    // only perform this at the server level.
    if (process.server) {

        // set parameters needed.
        const fileName = './.apiToken',
            timestamp = Date.now(),
            fs = require('fs'),
            promise = new Promise(async (resolve, reject) => {


                try {
                    // get our stored files timestamps.
                    const data = fs.statSync(fileName);

                    // is our file timestamp in the future?
                    if (Date.now() < data.mtimeMs) {

                        // resolve our token from our file.
                        resolve(fs.readFileSync(fileName).toString());
                        return;
                    }
                } catch (err) {
                    // ignore this error (file not found)
                }

                try {
                    // grab our token from the oAuth server,
                    // we'll reduce the expiry timestamp by 10 mins
                    // to reduce near expiring tokens.
                    const res = await getToken(context),
                        expiryTime = timestamp + (res.data.expires_in - (60 * 10));

                    // write our file and sync the times.
                    fs.writeFileSync(fileName, res.data.access_token);
                    fs.utimesSync(fileName, new Date(expiryTime), new Date(expiryTime));

                    // resolve our promise with our generated token.
                    resolve(res.data.access_token);

                } catch (err) {

                    // ToDo: handle token retrieval failure...
                    console.log(err);
                }

            });

        // once our promise is resolved, set it against our store.
        promise.then(
            (token) => context.store.commit('bearerToken/setToken', token),
            // ToDo: better handling of errors...
            (err) => console.log(err)
        );
    }
}

This allows me to generate a bearer token at the server level and cache it, which I can then use with SSR or on the client-side.