Skip to content

Commit

Permalink
refactor: use native URL instead of string concats
Browse files Browse the repository at this point in the history
  • Loading branch information
balazsorban44 committed Dec 15, 2020
1 parent d04185c commit 690c55b
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 114 deletions.
18 changes: 7 additions & 11 deletions src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/* global fetch:false */
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import logger from '../lib/logger'
import parseUrl from '../lib/parse-url'
import baseUrl from '../lib/baseUrl'

// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
Expand All @@ -22,8 +22,7 @@ import parseUrl from '../lib/parse-url'
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrl: baseUrl(),
keepAlive: 0, // 0 == disabled (don't send); 60 == send every 60 seconds
clientMaxAge: 0, // 0 == disabled (only use cache); 60 == sync if last checked > 60 seconds ago
// Properties starting with _ are used for tracking internal app state
Expand Down Expand Up @@ -77,13 +76,11 @@ if (typeof window !== 'undefined') {
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
const setOptions = ({
baseUrl,
basePath,
baseUrl: _baseUrl,
clientMaxAge,
keepAlive
} = {}) => {
if (baseUrl) { __NEXTAUTH.baseUrl = baseUrl }
if (basePath) { __NEXTAUTH.basePath = basePath }
if (baseUrl) { __NEXTAUTH.baseUrl = baseUrl(_baseUrl) }
if (clientMaxAge) { __NEXTAUTH.clientMaxAge = clientMaxAge }
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
Expand Down Expand Up @@ -306,11 +303,10 @@ const _apiBaseUrl = () => {
if (!process.env.NEXTAUTH_URL) { logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set') }

// Return absolute path when called server side
return `${__NEXTAUTH.baseUrl}${__NEXTAUTH.basePath}`
} else {
// Return relative path when called client side
return __NEXTAUTH.basePath
return __NEXTAUTH.baseUrl.href
}
// Return relative path when called client side
return __NEXTAUTH.baseUrl.pathname
}

const _encodedForm = (formData) => {
Expand Down
30 changes: 30 additions & 0 deletions src/lib/baseUrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import logger from './logger'

/**
* Simple universal (client/server) function to split host and path.
* It can also take a url (either URL or a string) and parses it correctly.
* @returns {URL}
*/
function baseUrl (url) {
let _url = url || process.env.NEXTAUTH_URL || process.env.VERCEL_URL
if (typeof _url !== 'string' && !(_url instanceof URL)) {
throw new Error('baseUrl must be either a valid URL object or a valid string URL')
}
const defaultUrl = 'http://localhost:3000/api/auth'
_url = _url || defaultUrl
try {
const parsedUrl = new URL(_url)
if (parsedUrl.pathname === '/') {
parsedUrl.pathname = '/api/auth'
}
parsedUrl.pathname = parsedUrl.pathname.replace(/\/$/, '')
parsedUrl.href = parsedUrl.href.replace(/\/$/, '')

return parsedUrl
} catch (error) {
logger.error('INVALID_URL', _url, error)
return new URL(defaultUrl)
}
}

export default baseUrl
27 changes: 0 additions & 27 deletions src/lib/parse-url.js

This file was deleted.

36 changes: 15 additions & 21 deletions src/server/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHash, randomBytes } from 'crypto'
import jwt from '../lib/jwt'
import parseUrl from '../lib/parse-url'
import baseUrl from '../lib/baseUrl'
import cookie from './lib/cookie'
import callbackUrlHandler from './lib/callback-url-handler'
import parseProviders from './lib/providers'
Expand Down Expand Up @@ -44,11 +44,6 @@ async function NextAuthHandler (req, res, userSuppliedOptions) {
csrfToken: csrfTokenFromPost
} = body

// @todo refactor all existing references to site, baseUrl and basePath
const parsedUrl = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
const baseUrl = parsedUrl.baseUrl
const basePath = parsedUrl.basePath

// Parse database / adapter
let adapter
if (userSuppliedOptions.adapter) {
Expand All @@ -63,15 +58,17 @@ async function NextAuthHandler (req, res, userSuppliedOptions) {
// If no secret option is specified then it creates one on the fly
// based on options passed here. A options contains unique data, such as
// OAuth provider secrets and database credentials it should be sufficent.
const secret = userSuppliedOptions.secret || createHash('sha256').update(JSON.stringify({ baseUrl, basePath, ...userSuppliedOptions })).digest('hex')
const secret = userSuppliedOptions.secret || createHash('sha256').update(JSON.stringify({
baseUrl: baseUrl(), ...userSuppliedOptions
})).digest('hex')

// Use secure cookies if the site uses HTTPS
// This being conditional allows cookies to work non-HTTPS development URLs
// Honour secure cookie option, which sets 'secure' and also adds '__Secure-'
// prefix, but enable them by default if the site URL is HTTPS; but not for
// non-HTTPS URLs like http://localhost which are used in development).
// For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
const useSecureCookies = userSuppliedOptions.useSecureCookies || baseUrl.startsWith('https://')
const useSecureCookies = userSuppliedOptions.useSecureCookies || baseUrl().protocol.startsWith('https://')
const cookiePrefix = useSecureCookies ? '__Secure-' : ''

// @TODO Review cookie settings (names, options)
Expand Down Expand Up @@ -201,27 +198,24 @@ async function NextAuthHandler (req, res, userSuppliedOptions) {
// These computed settings can values in userSuppliedOptions but override them
// and are request-specific.
adapter,
baseUrl,
basePath,
action,
provider,
cookies,
secret,
csrfToken,
providers: parseProviders(userSuppliedOptions.providers, baseUrl, basePath),
providers: parseProviders(userSuppliedOptions.providers),
session: sessionOptions,
jwt: jwtOptions,
events: eventsOptions,
callbacks: callbacksOptions,
callbackUrl: baseUrl,
redirect
}

// If debug enabled, set ENV VAR so that logger logs debug messages
if (options.debug === true) { process.env._NEXTAUTH_DEBUG = true }

// Get / Set callback URL based on query param / cookie + validation
options.callbackUrl = await callbackUrlHandler(req, res, options)
const callbackUrl = await callbackUrlHandler(req, res, options)

if (req.method === 'GET') {
switch (action) {
Expand All @@ -236,17 +230,17 @@ async function NextAuthHandler (req, res, userSuppliedOptions) {
return done()
case 'signin':
if (options.pages.signIn) {
let redirectUrl = `${options.pages.signIn}${options.pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`
let redirectUrl = `${options.pages.signIn}${options.pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${callbackUrl}`
if (req.query.error) { redirectUrl = `${redirectUrl}&error=${req.query.error}` }
return redirect(redirectUrl)
}

pages.render(req, res, 'signin', { baseUrl, basePath, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
pages.render(req, res, 'signin', { providers: Object.values(options.providers), callbackUrl, csrfToken }, done)
break
case 'signout':
if (options.pages.signOut) { return redirect(`${options.pages.signOut}${options.pages.signOut.includes('?') ? '&' : '?'}error=${error}`) }

pages.render(req, res, 'signout', { baseUrl, basePath, csrfToken, callbackUrl: options.callbackUrl }, done)
pages.render(req, res, 'signout', { csrfToken, callbackUrl }, done)
break
case 'callback':
if (provider && options.providers[provider]) {
Expand All @@ -259,12 +253,12 @@ async function NextAuthHandler (req, res, userSuppliedOptions) {
case 'verify-request':
if (options.pages.verifyRequest) { return redirect(options.pages.verifyRequest) }

pages.render(req, res, 'verify-request', { baseUrl }, done)
pages.render(req, res, 'verify-request', { }, done)
break
case 'error':
if (options.pages.error) { return redirect(`${options.pages.error}${options.pages.error.includes('?') ? '&' : '?'}error=${error}`) }

pages.render(req, res, 'error', { baseUrl, basePath, error }, done)
pages.render(req, res, 'error', { error }, done)
break
default:
res.status(404).end()
Expand All @@ -275,7 +269,7 @@ async function NextAuthHandler (req, res, userSuppliedOptions) {
case 'signin':
// Verified CSRF Token required for all sign in routes
if (!csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
return redirect(`${baseUrl()}/signin?csrf=true`)
}

if (provider && options.providers[provider]) {
Expand All @@ -285,7 +279,7 @@ async function NextAuthHandler (req, res, userSuppliedOptions) {
case 'signout':
// Verified CSRF Token required for signout
if (!csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signout?csrf=true`)
return redirect(`${baseUrl()}/signout?csrf=true`)
}

signout(req, res, options, done)
Expand All @@ -294,7 +288,7 @@ async function NextAuthHandler (req, res, userSuppliedOptions) {
if (provider && options.providers[provider]) {
// Verified CSRF Token required for credentials providers only
if (options.providers[provider].type === 'credentials' && !csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
return redirect(`${baseUrl()}/signin?csrf=true`)
}

callback(req, res, options, done)
Expand Down
11 changes: 6 additions & 5 deletions src/server/lib/callback-url-handler.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import cookie from '../lib/cookie'
import baseUrl from '../../lib/baseUrl'

export default async (req, res, options) => {
const { query } = req
const { body } = req
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = options
const { cookies, defaultCallbackUrl, callbacks } = options
const homepage = baseUrl().origin

// Handle preserving and validating callback URLs
// If no defaultCallbackUrl option specified, default to the homepage for the site
let callbackUrl = defaultCallbackUrl || baseUrl

let callbackUrl = defaultCallbackUrl || homepage
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
if (callbackUrlParamValue) {
// If callbackUrl form field or query parameter is passed try to use it if allowed
callbackUrl = await callbacks.redirect(callbackUrlParamValue, baseUrl)
callbackUrl = await callbacks.redirect(callbackUrlParamValue, homepage)
} else if (callbackUrlCookieValue) {
// If no callbackUrl specified, try using the value from the cookie if allowed
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, baseUrl)
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, homepage)
}

// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
Expand Down
8 changes: 5 additions & 3 deletions src/server/lib/providers.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export default (_providers, baseUrl, basePath) => {
import baseUrl from '../../lib/baseUrl'

export default function parseProviders (_providers) {
const providers = {}

_providers.forEach(provider => {
const providerId = provider.id
providers[providerId] = {
...provider,
signinUrl: `${baseUrl}${basePath}/signin/${providerId}`,
callbackUrl: `${baseUrl}${basePath}/callback/${providerId}`
signinUrl: `${baseUrl()}/signin/${providerId}`,
callbackUrl: `${baseUrl()}/callback/${providerId}`
}
})

Expand Down
5 changes: 3 additions & 2 deletions src/server/lib/signin/email.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { randomBytes } from 'crypto'
import baseUrl from '../../../lib/baseUrl'

export default async (email, provider, options) => {
try {
const { baseUrl, basePath, adapter } = options
const { adapter } = options

const { createVerificationRequest } = await adapter.getAdapter(options)

Expand All @@ -13,7 +14,7 @@ export default async (email, provider, options) => {
const token = randomBytes(32).toString('hex')

// Send email with link containing token (the unhashed version)
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
const url = `${baseUrl()}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`

// @TODO Create invite (send secret so can be hashed)
await createVerificationRequest(email, url, token, secret, provider, options)
Expand Down
7 changes: 4 additions & 3 deletions src/server/pages/error.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
import baseUrl from '../../lib/baseUrl'

export default ({ baseUrl, basePath, error, res }) => {
const signinPageUrl = `${baseUrl}${basePath}/signin`
export default ({ error, res }) => {
const signinPageUrl = `${baseUrl()}/signin`

let statusCode = 200
let heading = <h1>Error</h1>
let message = <p><a className='site' href={baseUrl}>{baseUrl.replace(/^https?:\/\//, '')}</a></p>
let message = <p><a className='site' href={baseUrl().origin}>{baseUrl().host}</a></p>

switch (error) {
case 'Signin':
Expand Down
5 changes: 3 additions & 2 deletions src/server/pages/signout.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
import baseUrl from '../../lib/baseUrl'

export default ({ baseUrl, basePath, csrfToken }) => {
export default ({ csrfToken }) => {
return render(
<div className='signout'>
<h1>Are you sure you want to sign out?</h1>
<form action={`${baseUrl}${basePath}/signout`} method='POST'>
<form action={`${baseUrl()}/signout`} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
<button type='submit'>Sign out</button>
</form>
Expand Down
5 changes: 3 additions & 2 deletions src/server/pages/verify-request.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
import baseUrl from '../../lib/baseUrl'

export default ({ baseUrl }) => {
export default function verifyRequest () {
return render(
<div className='verify-request'>
<h1>Check your email</h1>
<p>A sign in link has been sent to your email address.</p>
<p><a className='site' href={baseUrl}>{baseUrl.replace(/^https?:\/\//, '')}</a></p>
<p><a className='site' href={baseUrl().origin}>{baseUrl().host}</a></p>
</div>
)
}
Loading

0 comments on commit 690c55b

Please sign in to comment.