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 all commits
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ yarn-debug.log*
yarn-error.log*

# Docusaurus
www/build
www/build

#VS
/.vs/slnx.sqlite-journal
/.vs/slnx.sqlite
/.vs
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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.18",
"description": "An authentication library for Next.js",
"repository": "https://github.com/iaincollins/next-auth.git",
"author": "Iain Collins <[email protected]>",
Expand Down
4 changes: 3 additions & 1 deletion src/adapters/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import TypeORM from './typeorm'
import Prisma from './prisma'

export default {
Default: TypeORM.Adapter,
TypeORM
TypeORM,
Prisma
}
331 changes: 331 additions & 0 deletions src/adapters/prisma/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
import { createHash } from 'crypto'

import { CreateUserError } from '../../lib/errors'
import logger from '../../lib/logger'

const Adapter = (prismaConfig, options = {}) => {
const {
prisma, modelMapping = {
User: 'user',
Account: 'account',
Session: 'session',
VerificationRequest: 'verificationRequest'
}
} = prismaConfig

const { User, Account, Session, VerificationRequest } = modelMapping

async function getAdapter (appOptions) {
function debugMessage (debugCode, ...args) {
if (appOptions && appOptions.debug) {
logger.debug(`PRISMA_${debugCode}`, ...args)
}
}

if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
debugMessage('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
}
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
? appOptions.session.maxAge * 1000
: defaultSessionMaxAge
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
? appOptions.session.updateAge * 1000
: 0

async function createUser (profile) {
debugMessage('CREATE_USER', profile)
try {
return prisma[User].create({
data: {
name: profile.name,
email: profile.email,
image: profile.image,
emailVerified: profile.emailVerified &&
Object.prototype.toString.call(profile.emailVerified) === '[object Date]'
? profile.emailVerified.toISOString()
: null
}
})
} catch (error) {
logger.error('CREATE_USER_ERROR', error)
return Promise.reject(new CreateUserError(error))
}
}

async function getUser (id) {
debugMessage('GET_USER', id)
try {
return prisma[User].findOne({ where: { id } })
} catch (error) {
logger.error('GET_USER_BY_ID_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
}
}

async function getUserByEmail (email) {
debugMessage('GET_USER_BY_EMAIL', email)
try {
if (!email) { return Promise.resolve(null) }
return prisma[User].findOne({ where: { email } })
} catch (error) {
logger.error('GET_USER_BY_EMAIL_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
}
}

async function getUserByProviderAccountId (providerId, providerAccountId) {
debugMessage('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
try {
return prisma[Account].findOne({ where: { providerAccountId: `${providerAccountId}` } })[User]()
} catch (error) {
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
}
}

async function updateUser (user) {
debugMessage('UPDATE_USER', user)
try {
const { id, ...rest } = user
return prisma[User].update({ where: { id }, data: rest })
} catch (error) {
logger.error('UPDATE_USER_ERROR', error)
return Promise.reject(new Error('UPDATE_USER_ERROR', error))
}
}

async function deleteUser (userId) {
debugMessage('DELETE_USER', userId)
try {
return prisma[User].delete({ where: { id: userId } })
} catch (error) {
logger.error('DELETE_USER_ERROR', error)
return Promise.reject(new Error('DELETE_USER_ERROR', error))
}
}

async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
debugMessage('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
try {
return prisma[Account].create({
data: {
accessToken,
refreshToken,
compoundId: createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex'),
providerAccountId: `${providerAccountId}`,
providerId,
providerType,
accessTokenExpires,
[User]: {
connect: {
id: userId
}
}
}
})
} catch (error) {
logger.error('LINK_ACCOUNT_ERROR', error)
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
}
}

async function unlinkAccount (userId, providerId, providerAccountId) {
debugMessage('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
try {
return prisma[Account].delete({ where: { providerAccountId: `${providerAccountId}` } })
} catch (error) {
logger.error('UNLINK_ACCOUNT_ERROR', error)
return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR', error))
}
}

async function createSession (user) {
debugMessage('CREATE_SESSION', user)
try {
let expires = null
if (sessionMaxAge) {
const dateExpires = new Date()
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
expires = dateExpires.toISOString()
}

return prisma[Session].create({
data: {
expires,
[User]: {
connect: {
id: user.id
}
}
}
})
} catch (error) {
logger.error('CREATE_SESSION_ERROR', error)
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
}
}

async function getSession (sessionToken) {
debugMessage('GET_SESSION', sessionToken)
try {
const session = await prisma[Session].findOne({ where: { sessionToken } })

// Check session has not expired (do not return it if it has)
if (session && session.expires && new Date() > session.expires) {
await prisma[Session].delete({ where: { sessionToken } })
return null
}

return session
} catch (error) {
logger.error('GET_SESSION_ERROR', error)
return Promise.reject(new Error('GET_SESSION_ERROR', error))
}
}

async function updateSession (session, force) {
debugMessage('UPDATE_SESSION', session)
try {
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
// Calculate last updated date, to throttle write updates to database
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
// e.g. ({expiry date} - 30 days) + 1 hour
//
// Default for sessionMaxAge is 30 days.
// Default for sessionUpdateAge is 1 hour.
const dateSessionIsDueToBeUpdated = new Date(session.expires)
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)

// Trigger update of session expiry date and write to database, only
// if the session was last updated more than {sessionUpdateAge} ago
if (new Date() > dateSessionIsDueToBeUpdated) {
const newExpiryDate = new Date()
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
session.expires = newExpiryDate
} else if (!force) {
return null
}
} else {
// If session MaxAge, session UpdateAge or session.expires are
// missing then don't even try to save changes, unless force is set.
if (!force) { return null }
}

const { id, ...rest } = session
return prisma[Session].update({ where: { id }, data: rest })
} catch (error) {
logger.error('UPDATE_SESSION_ERROR', error)
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
}
}

async function deleteSession (sessionToken) {
debugMessage('DELETE_SESSION', sessionToken)
try {
return prisma[Session].delete({ where: { sessionToken } })
} catch (error) {
logger.error('DELETE_SESSION_ERROR', error)
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
}
}

async function createVerificationRequest (identifier, url, token, secret, provider) {
debugMessage('CREATE_VERIFICATION_REQUEST', identifier)
try {
const { baseUrl } = appOptions
const { sendVerificationRequest, maxAge } = provider

// Store hashed token (using secret as salt) so that tokens cannot be exploited
// even if the contents of the database is compromised.
// @TODO Use bcrypt function here instead of simple salted hash
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')

let expires = null
if (maxAge) {
const dateExpires = new Date()
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
expires = dateExpires.toISOString()
}

// Save to database
const verificationRequest = await prisma[VerificationRequest].create({
data: {
identifier,
token: hashedToken,
expires
}
})

// With the verificationCallback on a provider, you can send an email, or queue
// an email to be sent, or perform some other action (e.g. send a text message)
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })

return verificationRequest
} catch (error) {
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
}
}

async function getVerificationRequest (identifier, token, secret, provider) {
debugMessage('GET_VERIFICATION_REQUEST', identifier, token)
try {
// Hash token provided with secret before trying to match it with database
// @TODO Use bcrypt instead of salted SHA-256 hash for token
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
const verificationRequest = await prisma[VerificationRequest].findOne({ where: { token: hashedToken } })

if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
// Delete verification entry so it cannot be used again
await prisma[VerificationRequest].delete({ where: { token: hashedToken } })
return null
}

return verificationRequest
} catch (error) {
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
}
}

async function deleteVerificationRequest (identifier, token, secret, provider) {
debugMessage('DELETE_VERIFICATION', identifier, token)
try {
// Delete verification entry so it cannot be used again
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
await prisma[VerificationRequest].delete({ where: { token: hashedToken } })
} catch (error) {
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
}
}

return Promise.resolve({
createUser,
getUser,
getUserByEmail,
getUserByProviderAccountId,
updateUser,
deleteUser,
linkAccount,
unlinkAccount,
createSession,
getSession,
updateSession,
deleteSession,
createVerificationRequest,
getVerificationRequest,
deleteVerificationRequest
})
}

return {
getAdapter
}
}

export default {
Adapter
}
Loading