Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added /api/auth/tokens endpoint to return oauth tokens #425

Closed
wants to merge 51 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d17da5b
feat(adapter): Add opinionated prisma adapter
Fumler Jun 22, 2020
a04c03b
fix(prisma): Make sure provider id is a string
Fumler Jun 30, 2020
d7771b6
docs(prisma): Add note about model names and set email to optional
Fumler Jul 4, 2020
cbfd8cf
Add support for hitting cancel if using token id
iaincollins Jun 28, 2020
7749759
feat: Added UserData to ProfileData after return from Apple to get us…
Jul 1, 2020
d4ff56b
Fix linter errors and add comments
iaincollins Jul 4, 2020
1dc26a6
Fix for reading private key in Apple provider
iaincollins Jul 4, 2020
327a3f3
Add provider Vercel-style marquee to docs
ndom91 Jul 6, 2020
8a265f7
Add tutorial on how to use custom typeorm models
tmayr Jul 6, 2020
0687c24
Improve CSRF security for all routes
iaincollins Jul 1, 2020
6541cc6
Fix linting errors and bug in getCsrfToken
iaincollins Jul 1, 2020
805c4de
Refactor redirect handling (WIP)
iaincollins Jul 2, 2020
201335f
Improve client state syncing
iaincollins Jul 2, 2020
77be1b0
Improve client event handling
iaincollins Jul 3, 2020
5373da2
Only invoke setTimeout client side
iaincollins Jul 3, 2020
24c9fed
Update events, callbacks & pages to use camelCase
iaincollins Jul 3, 2020
12cdfb1
Fix linter errors
iaincollins Jul 4, 2020
4578ad7
Update version to 3.0.0-beta.8
iaincollins Jul 4, 2020
014f1c9
Update pages documentation
iaincollins Jul 4, 2020
afc2364
Update version to 3.0.0-beta.9
iaincollins Jul 4, 2020
92ef9ea
Respect existing cookies on a request object
iaincollins Jul 6, 2020
9621856
Fix error merging branches for v3
iaincollins Jul 7, 2020
83b2b67
Refactor to simplify site URL configuration
iaincollins Jul 7, 2020
80873c7
Update TypeORM tutorial
iaincollins Jul 7, 2020
d3722d1
Update adapter documentation
iaincollins Jul 7, 2020
468d33c
fix: marquee icons
ndom91 Jul 7, 2020
d45fb43
Fix bug with NEXTAUTH_URL parsing
iaincollins Jul 8, 2020
b158039
Update version to 3.0.0-beta.13
iaincollins Jul 8, 2020
82762ac
Update homepage
iaincollins Jul 8, 2020
f9e9cb7
Refactor and document state provider option
iaincollins Jul 8, 2020
f9c043a
Disable use of state on Apple provider
iaincollins Jul 8, 2020
1e147dc
Improve homepage
iaincollins Jul 8, 2020
5962432
Update adapters documentation
iaincollins Jul 8, 2020
db41ca5
Tweak CSS on homepage
iaincollins Jul 8, 2020
a236da9
Update homepage and refactor CSS
iaincollins Jul 8, 2020
861b019
Add support for passing appContext to getCsrfToken
iaincollins Jul 8, 2020
5b988e6
Update email provider
iaincollins Jul 8, 2020
7dafa67
Apply datetime transforms on properties in custom models
iaincollins Jul 8, 2020
0676b25
Update documentation
iaincollins Jul 8, 2020
5ed775c
Add provider icons to homepage
iaincollins Jul 9, 2020
d933775
Add FAQ
iaincollins Jul 9, 2020
4cea678
Improve docs site on mobile
iaincollins Jul 9, 2020
e1a186f
Update FAQ
iaincollins Jul 9, 2020
5e16fc8
Bump version to 3.0.0-beta.17
iaincollins Jul 10, 2020
19df05a
Refactor JWT support
iaincollins Jul 9, 2020
1e4f6eb
Update JWT and session docs
iaincollins Jul 10, 2020
8dbe4f2
Enforce HMAC-256 on JWT
iaincollins Jul 10, 2020
6835e90
Update version to 3.0.0-beta.18
iaincollins Jul 10, 2020
457b3b6
added /api/auth/accounts endpoint to return oauth tokens
tomvoss Jul 12, 2020
2f046a0
add /api/auth/token/:provider/:type endpoint
tomvoss Jul 14, 2020
a3211b3
rename token endpoint to tokens and remove accounts endpoint
tomvoss Jul 14, 2020
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
Prev Previous commit
Next Next commit
Improve CSRF security for all routes
Includes breaking changes for v3 and updates to documentation.

If using the client, the only required change should be setting the NEXTAUTH_URL environment variable.
  • Loading branch information
iaincollins committed Jul 10, 2020
commit 0687c24e1bab2ca4a8b3c64d8502a950b07aa51b
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "2.2.0",
"version": "3.0.0-beta.0",
"description": "An authentication library for Next.js",
"repository": "https://github.com/iaincollins/next-auth.git",
"author": "Iain Collins <[email protected]>",
Expand Down
130 changes: 78 additions & 52 deletions src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import logger from '../lib/logger'

// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
// 1. An empty value is legitimate when the code is being invoked client side as
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
const __NEXTAUTH = {
site: '',
basePath: '/api/auth',
clientMaxAge: 0 // e.g. 0 == disabled, 60 == 60 seconds
site: (typeof window === 'undefined')
? process.env.NEXTAUTH_URL || process.env.VERCEL_URL || 'http://localhost:3000'
: '',
basePath: (typeof window === 'undefined')
? process.env.NEXTAUTH_BASE_PATH|| '/api/auth'
: '/api/auth',
clientMaxAge: 0, // e.g. 0 == disabled, 60 == 60 seconds
eventListenerAdded: false
}

let __NEXTAUTH_EVENT_LISTENER_ADDED = false

// Method to set options. The documented way is to use the provider, but this
// 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.
Expand All @@ -31,23 +40,24 @@ const getSession = async ({ req, ctx } = {}) => {
}

const baseUrl = _baseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const session = await _fetchData(`${baseUrl}/session`, options)
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
const session = await _fetchData(`${baseUrl}/session`, fetchOptions)
_sendMessage({ event: 'session', data: { trigger: 'getSession' } })
return session
}

// Universal method (client + server)
const getProviders = async () => {
const getCsrfToken = async ({ req }) => {
const baseUrl = _baseUrl()
return _fetchData(`${baseUrl}/providers`)
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
const data = await _fetchData(`${baseUrl}/csrf`. fetchOptions)
return data && data.csrfToken ? data.csrfToken : null
}

// Universal method (client + server)
const getCsrfToken = async () => {
// Universal method (client + server); does not require request headers
const getProviders = async () => {
const baseUrl = _baseUrl()
const data = await _fetchData(`${baseUrl}/csrf`)
return data && data.csrfToken ? data.csrfToken : null
return _fetchData(`${baseUrl}/providers`)
}

// Context to store session data globally
Expand Down Expand Up @@ -80,8 +90,8 @@ const useSessionData = (session) => {
_sendMessage({ event: 'session', data: { trigger: 'useSessionData' } })
}

if (typeof window !== 'undefined' && __NEXTAUTH_EVENT_LISTENER_ADDED === false) {
__NEXTAUTH_EVENT_LISTENER_ADDED = true
if (typeof window !== 'undefined' && __NEXTAUTH.eventListenerAdded === false) {
__NEXTAUTH.eventListenerAdded = true
window.addEventListener('storage', async (event) => {
if (event.key === 'nextauth.message') {
const message = JSON.parse(event.newValue)
Expand All @@ -102,9 +112,7 @@ const useSessionData = (session) => {

// If CLIENT_MAXAGE is greater than zero, trigger auto re-fetching session
if (clientMaxAge > 0) {
setTimeout(async (session) => {
await _getSession()
}, clientMaxAge)
setTimeout(async () => { await _getSession() }, clientMaxAge)
}
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
Expand All @@ -115,44 +123,44 @@ const useSessionData = (session) => {
}

// Client side method
const signin = async (provider, args) => {
const signIn = async (provider, args) => {
const baseUrl = _baseUrl()
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location

if (!provider) {
// Redirect to sign in page if no provider specified
const baseUrl = _baseUrl()
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
return
}

const providers = await getProviders()
if (!providers[provider]) {

// Redirect to sign in page if no valid provider specified
if (!provider || !providers[provider]) {
// If Provider not recognized, redirect to sign in page
const baseUrl = _baseUrl()
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
} else if (providers[provider].type === 'oauth') {
// If is an OAuth provider, redirect to providers[provider].signinUrl
window.location = `${providers[provider].signinUrl}?callbackUrl=${encodeURIComponent(callbackUrl)}`
} else {
// If is any other provider type, POST to providers[provider].signinUrl (with CSRF Token)
// If is any other provider type, POST to provider URL with CSRF Token,
// callback URL and any other parameters supplied.
//
// We pass 'json: true' to request a response in JSON instead of HTTP
// as redirect URLs on other domains are not returned when accessed using
// the fetch API in the browser, and we need to ask the end point to return
// the response as a JSON object (the end point still defaults to returning
// an HTTP response with a redirect for non-JavaScript clients).
const options = {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: _encodedForm({
...args,
csrfToken: await getCsrfToken(),
callbackUrl: callbackUrl,
...args
json: true
})
}
const res = await fetch(providers[provider].signinUrl, options)
window.location = res.url ? res.url : callbackUrl
const res = await fetch(`${baseUrl}/signin/${provider}`, options)
const data = await res.json()
window.location = data.url ? data.url : callbackUrl
}
}

// Client side method
const signout = async (args) => {
const signOut = async (args) => {
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location

const baseUrl = _baseUrl()
Expand Down Expand Up @@ -190,7 +198,24 @@ const _fetchData = async (url, options = {}) => {
}
}

const _baseUrl = () => `${__NEXTAUTH.site}${__NEXTAUTH.basePath}`
const _baseUrl = () => {
// NEXTAUTH_URL should always be set explicitly to support server side calls
if (typeof window === 'undefined' && !process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
}

let site = __NEXTAUTH.site

// If site value exists but does not start with http or https protocol, add it here
if (site.length > 0 && !site.startsWith('https://') && !site.startsWith('http://')) {
site = `https://${site}`
}

// Remove trailing slash from site if there is one
site = site.replace(/\/$/, '')

return `${site}${__NEXTAUTH.basePath}`
}

const _encodedForm = (formData) => {
return Object.keys(formData).map((key) => {
Expand All @@ -205,22 +230,23 @@ const _sendMessage = (message) => {
}

export default {
// Call config() from _app.js to set options globally in the app.
// You need to set at least the site name to use server side calls.
options: setOptions,
setOptions,
// Some methods are exported with more than one name. This provides
// flexibility over how they can be invoked and compatibility with earlier
// releases (going back to v1 and earlier v2 beta releases).
// e.g. NextAuth.session() or const { getSession } from 'next-auth/client'
session: getSession,
providers: getProviders,
csrfToken: getCsrfToken,
getSession,
getProviders,
getCsrfToken,
getProviders,
useSession,
Provider,
signin,
signout
signIn,
signOut,
/* Deprecated / unsupported features below this line */
// Use setOptions() set options globally in the app.
setOptions,
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases.
options: setOptions,
session: getSession,
providers: getProviders,
csrfToken: getCsrfToken,
signin: signIn,
signout: signOut,
}
18 changes: 14 additions & 4 deletions src/lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,26 @@ const logger = {
!text
? console.error(errorCode)
: console.error(
`[next-auth][error][${errorCode}]`,
text,
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
`[next-auth][error][${errorCode.toLowerCase()}]`,
text,
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
)
}
},
warn: (warnCode, ...text) => {
if (console) {
!text
? console.warn(warnCode)
: console.warn(
`[next-auth][warn][${warnCode.toLowerCase()}]`,
text
)
}
},
debug: (debugCode, ...text) => {
if (process && process.env && process.env._NEXTAUTH_DEBUG) {
console.log(
`[next-auth][debug][${debugCode}]`,
`[next-auth][debug][${debugCode.toLowerCase()}]`,
text
)
}
Expand Down
65 changes: 51 additions & 14 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,21 @@ import callback from './routes/callback'
import session from './routes/session'
import pages from './pages'
import adapters from '../adapters'
import logger from '../lib/logger'

const DEFAULT_SITE = 'http://localhost:3000'
const DEFAULT_BASE_PATH = '/api/auth'
// @TODO Refactor internal site / url variable names internally once we have
// tests in place, as the 'site' name feels a bit ambigous.
// I don't want to change it until we have tests as refactoring might
// involve changes in dozen files or so and too easy to break accidentally.
const DEFAULT_SITE = process.env.NEXTAUTH_URL || process.env.VERCEL_URL || 'http://localhost:3000'
const DEFAULT_BASE_PATH = process.env.NEXTAUTH_BASE_PATH || '/api/auth'

// While we can run locally without this value being set, to work properly in
// production with OAuth providers the NEXTAUTH_URL environment variable should
// be set.
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
}

export default async (req, res, userSuppliedOptions) => {
// To the best of my knowledge, we need to return a promise here
Expand All @@ -40,7 +52,16 @@ export default async (req, res, userSuppliedOptions) => {
} = body

// Allow site name, path prefix to be overriden
const site = userSuppliedOptions.site || DEFAULT_SITE
let site = userSuppliedOptions.site || DEFAULT_SITE

// If site value does not start with http or https protocol, add it here
if (!site.startsWith('https://') && !site.startsWith('http://')) {
site = `https://${site}`
}

// Remove trailing slash from site if there is one
site = site.replace(/\/$/, '')

const basePath = userSuppliedOptions.basePath || DEFAULT_BASE_PATH
const baseUrl = `${site}${basePath}`

Expand Down Expand Up @@ -190,7 +211,6 @@ export default async (req, res, userSuppliedOptions) => {
cookies,
secret,
csrfToken,
csrfTokenVerified,
providers: parseProviders(userSuppliedOptions.providers, baseUrl),
session: sessionOption,
jwt: jwtOptions,
Expand All @@ -205,9 +225,16 @@ export default async (req, res, userSuppliedOptions) => {
// Get / Set callback URL based on query param / cookie + validation
options.callbackUrl = await callbackUrlHandler(req, res, options)

// Helper method for handling redirects
const redirect = (redirectUrl) => {
res.status(302).setHeader('Location', redirectUrl)
res.end()
const reponseAsJson = (req.body && req.body.json === 'true') ? true : false

if (reponseAsJson) {
res.json({ url: redirectUrl })
} else {
res.status(302).setHeader('Location', redirectUrl)
res.end()
}
return done()
}

Expand All @@ -223,13 +250,9 @@ export default async (req, res, userSuppliedOptions) => {
res.json({ csrfToken })
return done()
case 'signin':
if (provider && options.providers[provider]) {
signin(req, res, options, done)
} else {
if (options.pages.signin) { return redirect(`${options.pages.signin}${options.pages.signin.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`) }
if (options.pages.signin) { return redirect(`${options.pages.signin}${options.pages.signin.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`) }

pages.render(req, res, 'signin', { site, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
}
pages.render(req, res, 'signin', { site, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
break
case 'signout':
if (options.pages.signout) { return redirect(`${options.pages.signout}${options.pages.signout.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`) }
Expand Down Expand Up @@ -261,17 +284,31 @@ export default async (req, res, userSuppliedOptions) => {
} else if (req.method === 'POST') {
switch (action) {
case 'signin':
// Signin POST requests are used for email sign in
// Verified CSRF Token required for all sign in routes
if (!csrfTokenVerified) {
return redirect(`${baseUrl}/signin?csrf=true`)
}

if (provider && options.providers[provider]) {
signin(req, res, options, done)
break
}
break
case 'signout':
// Verified CSRF Token required for signout
if (!csrfTokenVerified) {
return redirect(`${baseUrl}/signout?csrf=true`)
}

signout(req, res, options, done)
break
case 'callback':
if (provider && options.providers[provider]) {

// Verified CSRF Token required for credentials providers only
if (options.providers[provider].type === 'credentials' && !csrfTokenVerified) {
return redirect(`${baseUrl}/signin?csrf=true`)
}

callback(req, res, options, done)
} else {
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)
Expand Down
16 changes: 15 additions & 1 deletion src/server/lib/oauth/callback.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import oAuthClient from './client'

import { createHash } from 'crypto'
import querystring from 'querystring'
import jwtDecode from 'jwt-decode'
import oAuthClient from './client'
import logger from '../../../lib/logger'

// @TODO Refactor monkey patching in _getOAuthAccessToken() and _get()
Expand All @@ -18,6 +20,18 @@ export default async (req, provider, callback) => {
const client = oAuthClient(provider)

if (provider.version && provider.version.startsWith('2.')) {
// For OAuth 2.0 flows, check state returned and matches expected value
// (a hash of the NextAuth.js CSRF token).
//
// This check can be disabled for providers that do not support it by
// setting 'useState: false' as a provider option (defaults to true).
if (!Object.prototype.hasOwnProperty.call(provider, 'useState') || provider.useState === true) {
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
if (state !== expectedState) {
return callback(new Error("Invalid state returned from oAuth provider"))
}
}

if (req.method === 'POST') {
try {
const body = JSON.parse(JSON.stringify(req.body))
Expand Down
Loading