Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ const defaultOptions = {
}

module.exports = function NuxtOAuth (moduleOptions) {
const options = Object.assign(defaultOptions, moduleOptions, this.options.oauth)
// NOTE: moduleOptions should be last according to the Nuxt documentation
// @see https://nuxtjs.org/docs/directory-structure/modules/
const options = Object.assign({}, defaultOptions, moduleOptions, this.options.oauth)

// Check for required options types
if (typeof options.onLogout !== 'function') throw new Error('options.onLogout must be a function')
if (typeof options.fetchUser !== 'function') throw new Error('options.fetchUser must be a function')
if (options.scopes && !Array.isArray(options.scopes)) throw new Error('options.scopes must be an array')
Expand All @@ -23,7 +26,7 @@ module.exports = function NuxtOAuth (moduleOptions) {
src: resolve(__dirname, 'lib/plugin.js'),
fileName: 'nuxt-oauth.plugin.js',
options: {
moduleName: options.moduleName
moduleName: options.moduleName,
}
})

Expand All @@ -32,7 +35,7 @@ module.exports = function NuxtOAuth (moduleOptions) {
this.options.router.middleware = this.options.router.middleware || []
this.options.router.middleware.push('auth')

// Setup te /auth/login route
// Setup the /auth/login route
this.extendRoutes((routes, res) => {
routes.push({
name: 'oauth-login',
Expand Down
111 changes: 88 additions & 23 deletions lib/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ const { atob, btoa } = require('Base64')
const { parse } = require('qs')
const join = require('url-join')

const pick = (items) => (obj) => items
.reduce((acc, key) => {
obj[key] && (acc[key] = obj[key])
return acc
}, {})
const pickTokenProps = pick(['accessToken', 'refreshToken', 'expires'])

const DEFAULT_TOKEN_DURATION = 60 * 30 // NOTE: Half an hour

function Handler (opts) {
Expand All @@ -20,8 +27,13 @@ Handler.prototype.init = function init ({
this.auth = this.createAuth()
}

// Logs errors to the console if application is running in development mode
const errorLog = e => process.env.NODE_ENV === 'development' && console.error(e)

/**
*
* @returns { Object } An OAuth2 Instance
*/
Handler.prototype.createAuth = function createAuth () {
const {
oauthHost,
Expand Down Expand Up @@ -53,6 +65,13 @@ Handler.prototype.redirect = function redirect (path) {
this.res.end()
}

/**
* Ensures an encrypted session is attached to the request object
* TODO: The session instance could be added to Nuxt serverMiddleware
* Connect handler, ensuring that it was always available on each request
*
* @returns { Promise } An encrypted sessionToken instance
*/
Handler.prototype.createSession = function createSession () {
if (this.req[this.opts.sessionName]) return Promise.resolve()
const session = sessions({
Expand All @@ -63,19 +82,32 @@ Handler.prototype.createSession = function createSession () {
return new Promise(resolve => session(this.req, this.res, resolve))
}

/**
* Returns the accessToken appended to the sessionToken instance or an emopty object
* TODO: Rename getAccessToken?
*
* @returns { [string|Object] } The accessToken if it has been added to the sessionToken
*/
Handler.prototype.getSessionToken = function getSessionToken () {
const { token } = this.req[this.opts.sessionName] || {}

return token || {}
}

/**
* Checks that an accessToken is available before authenticating
* TODO: Replace check for Bearer token to support other systems
* progamatically logging in in this way
*
* @returns { [boolean|Promise|undefined] }
*/
Handler.prototype.checkRequestAuthorization = async function checkRequestAuthorization () {
const existingToken = this.extractToken()

await this.createSession()
const sessionToken = this.req[this.opts.sessionName].token

try {
// when an existing token exists try to authenticate the session
if (existingToken) {
await this.saveData({ accessToken: existingToken })
// when a token exists update the session token
if (sessionToken.accessToken) {
await this.saveData(sessionToken)
}
} catch (e) {
// saveData failed, clear the session
Expand All @@ -85,6 +117,13 @@ Handler.prototype.checkRequestAuthorization = async function checkRequestAuthori
return false
}

/**
* Checks that a valid token was passed to the callback url and stores
* that on the session and adds a client side available accessToken to
* the Node HTTP Request object
*
* @returns { Promise }
*/
Handler.prototype.authenticateCallbackToken = async function authenticateCallbackToken () {
let redirectUrl
try {
Expand All @@ -105,13 +144,22 @@ Handler.prototype.authenticateCallbackToken = async function authenticateCallbac
}
}

/**
* Appends the token to the session variable and appends the accessToken to the
* request which makes it available to the plugin initStore method when passing
* the authenticate middleware method
*
* @param { Object } token
* @returns
*/
Handler.prototype.saveData = async function saveData (token) {
await this.createSession()
if (!token) return this.req[this.opts.sessionName].reset()

const updatedToken = pickTokenProps(token)

const { accessToken, refreshToken, expires } = token
this.req[this.opts.sessionName].token = { accessToken, refreshToken, expires }
this.req.accessToken = accessToken
this.req[this.opts.sessionName].token = updatedToken
this.req.accessToken = updatedToken.accessToken

const fetchUser = async () => {
try {
Expand All @@ -133,10 +181,18 @@ Handler.prototype.updateToken = function updateToken (...args) {
return this.authenticate(...args)
}

/**
* Makes a request to OAuth to check token is valid, if it isn't and has expired
* it uses the refreshToken to fetch a new token and appends this to the sessionToken
* TODO: This method of refreshing the token appears like it may be incorrect
*
* @returns { [Object|null] }
*/
Handler.prototype.authenticate = async function authenticate () {
await this.createSession()

const { accessToken, refreshToken, expires } = this.getSessionToken()

const { token = {} } = this.req[this.opts.sessionName]
const { accessToken, refreshToken, expires } = token
const data = { expires_in: DEFAULT_TOKEN_DURATION }

if (!accessToken) {
Expand Down Expand Up @@ -175,10 +231,15 @@ Handler.prototype.authenticate = async function authenticate () {
}
}

/**
* Handles the `auth/refresh` route
*
* @returns { [Object|null] } An OAuth2 token or null
*/
Handler.prototype.useRefreshToken = async function useRefreshToken () {
await this.createSession()

const { accessToken, refreshToken } = this.getSessionToken()
const { accessToken, refreshToken } = this.req[this.opts.sessionName].token

if (!accessToken || !refreshToken) {
return null
Expand Down Expand Up @@ -223,6 +284,13 @@ Handler.prototype.logout = async function logout () {
this.redirect(redirectUrl)
}

/**
* Requests an OAuth2 Token instance using the accessToken and refreshToken
*
* @param { string } accessToken
* @param { string } refreshToken
* @returns { [boolean|null]} true on success null on error
*/
Handler.prototype.setTokens = async function setTokens (accessToken, refreshToken) {
await this.createSession()

Expand All @@ -247,21 +315,18 @@ Handler.routes = {
refresh: '/auth/refresh'
}

/**
* Checks if a route path matches the Node HTTP request url path
* TODO: This could be handled by utilising the builtin Nuxt
* serverMiddleware Connect handler
*
* @param { string } route - A Handler.routes route key
* @returns { boolean }
*/
Handler.prototype.isRoute = function isRoute (route) {
const path = this.constructor.routes[route]

return this.req.url.startsWith(path)
}

Handler.prototype.extractToken = function extractToken () {
const { headers: { authorization } } = this.req

if (!authorization) return null

// Take the second split, handles all token types
const [, token] = authorization.split(' ')

return token
}

module.exports = Handler
74 changes: 66 additions & 8 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,28 @@ import middleware from '@@/.nuxt/middleware'

const moduleName = '<%= options.moduleName %>'

/**
* Initializes the <%= options.moduleName %> module store
*
* @param { Object } context - The Nuxt context instance
* @returns { undefined }
*/
const initStore = async context => {
if (!context.store) {
context.error('nuxt-oauth requires a Vuex store!')
return
}

const accessToken = context.req && context.req.accessToken
const user = context.req && context.req.user

context.store.registerModule(
moduleName,
{
namespaced: true,
state: {
accessToken: (context.req && context.req.accessToken),
user: (context.req && context.req.user)
accessToken: accessToken,
user: user,
},
mutations: {
setAccessToken (state, accessToken) {
Expand All @@ -26,15 +35,51 @@ const initStore = async context => {
)
}

const isAuthenticatedRoute = component => typeof component.options.authenticated === 'function' ? component.options.authenticated(component) : component.options.authenticated
/**
* Returns true if the component has a valid authenticated property
*
* @param { Object } component A Nuxt component instance
* @returns { boolean }
*/
const isAuthenticatedRoute = component => {
// return result(component)('options.authenticated', component)
return typeof component.options.authenticated === 'function'
? component.options.authenticated(component)
: component.options.authenticated
}

const checkAuthenticatedRoute = ({ route: { matched } }) => process.client
? matched.some(({ components }) => Object.values(components).some(c => isAuthenticatedRoute(c)))
: matched.some(({ components }) => components && Object.values(components).some(({ _Ctor }) => _Ctor && Object.values(_Ctor).some(c => c && c.options && isAuthenticatedRoute(c))))
/**
* Return true if any of the nested components requested has a
* valid authenticated property
*
* @param { Object } context - The Nuxt context instance
* @returns { boolean }
*/
const checkAuthenticatedRoute = ({ route: { matched } }) => {
return process.client
? matched.some(({ components }) => {
return Object.values(components).some(isAuthenticatedRoute)
})
: matched.some(({ components }) => {
return components && Object.values(components).some(({ _Ctor }) => {
return _Ctor && Object.values(_Ctor).some(c => {
return c && c.options && isAuthenticatedRoute(c)
})
})
})
}

/**
* Handler for constructing redirect urls for actions
* Handles redirects on both client and server side
*
* @param { Object } context - The Nuxt context instance
* @param { string } action - The path of the desired endpoint minus the `auth/` prefix
* @param { string } [redirectUrl = ''] - The url of the page to be returned to once the redirect is completed. Defaults to home
* @returns { undefined } redirectUrl
*/
const redirectToOAuth = ({ redirect }, action, redirectUrl = '') => {
const encodedRedirectUrl = `/auth/${action}?redirect-url=${encodeURIComponent(redirectUrl)}`

if (process.client) {
window.location.assign(encodedRedirectUrl)
} else {
Expand All @@ -43,7 +88,16 @@ const redirectToOAuth = ({ redirect }, action, redirectUrl = '') => {
}
}

/**
* Registers the auth middleware. Checks whether an accessToken exists in
* the Oauth module store and whether any nested component in the route
* requires authentication before begining the Oauth flow
*
* @param { Object } context - The Nuxt context instance
* @returns { undefined }
*/
middleware.auth = context => {
// isAuthorizationRequiredForRoute
const isAuthenticated = checkAuthenticatedRoute(context)
const { accessToken = null } = context.store.state[moduleName]

Expand All @@ -54,7 +108,11 @@ middleware.auth = context => {
export default async (context, inject) => {
await initStore(context)

const createAuth = action => (redirectUrl = context.route.fullPath) => redirectToOAuth(context, action, redirectUrl)
const createAuth = action => {
return (redirectUrl = context.route.fullPath) => {
return redirectToOAuth(context, action, redirectUrl)
}
}

inject('login', createAuth('login'))
inject('logout', createAuth('logout'))
Expand Down
16 changes: 11 additions & 5 deletions lib/server-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ const setCustomValues = (options, req) => async key => {
options[key] = await options[key](req)
}

/**
* Returns the serverMiddleware with options applied to the OAuth2 Handler instance
*
* @param { Object } options - The combined moduleOptions, Nuxt oauth options, and defaultOptions
* @returns { Function } Nuxt Connect instance handler
*/
module.exports = options => async (req, res, next) => {

let optionsObj = { ...options }
Expand All @@ -32,9 +38,9 @@ module.exports = options => async (req, res, next) => {
const token = await handler.useRefreshToken()

if (token) {
const { accessToken, expires } = token
const { accessToken, refreshToken, expires } = token
res.writeHead(200, { 'Content-Type': 'application/json' })
const body = JSON.stringify({ accessToken, expires })
const body = JSON.stringify({ accessToken, refreshToken, expires })
return res.end(body)
}

Expand All @@ -59,10 +65,10 @@ module.exports = options => async (req, res, next) => {
return handler.logout()
}

// Check to see if the request has a valid bearer token
await handler.checkRequestAuthorization()
// // Check to see if the request has a bearer token
// await handler.checkRequestAuthorization()

// On any other route, authenticate
// // On any other route, authenticate
await handler.authenticate()

return next()
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nuxt-oauth",
"version": "2.10.0",
"version": "2.10.1",
"description": "OAuth module for your Nuxt applications",
"main": "index.js",
"repository": "https://github.com/SohoHouse/nuxt-oauth",
Expand Down