Skip to content

Instantly share code, notes, and snippets.

@cannikin
Last active June 9, 2023 22:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cannikin/ef206cd25e2c2bddb69935a568074f3e to your computer and use it in GitHub Desktop.
Save cannikin/ef206cd25e2c2bddb69935a568074f3e to your computer and use it in GitHub Desktop.
Oauth implementation for RedwoodJS
import CryptoJS from 'crypto-js'
import fetch from 'node-fetch'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
import { createUser } from 'src/services/users'
export const handler = async (event, _context) => {
switch (event.path) {
case '/oauth/callback':
return await callback(event)
default:
return notFound(event)
}
}
const callback = async (event) => {
const { code, state } = event.queryStringParameters
const response = await fetch(`https://github.com/login/oauth/access_token`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: process.env.GITHUB_OAUTH_CLIENT_ID,
client_secret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
code: code,
redirect_uri: process.env.GITHUB_OAUTH_REDIRECT_URI,
}),
})
// eslint-disable-next-line camelcase
const { access_token, scope, error } = JSON.parse(await response.text())
if (error) {
return errorStatus(error)
}
try {
const providerUser = await getProviderUser(access_token)
logger.info(`[OAUTH] User ${providerUser.login} attempting login`)
const user = await login(providerUser, access_token, scope, { state })
return redirectToSite(secureCookie(user))
} catch (e) {
return errorStatus(e.message)
}
}
// creates the cookie a logged in user should have as if they used dbAuth
const secureCookie = (user) => {
const expires = new Date()
expires.setFullYear(expires.getFullYear() + 1)
const cookieAttrs = [
`Expires=${expires.toUTCString()}`,
'HttpOnly=true',
'Path=/',
'SameSite=Strict',
`Secure=${process.env.NODE_ENV !== 'development'}`,
]
const data = JSON.stringify({ id: user.id })
const encrypted = CryptoJS.AES.encrypt(
data,
process.env.SESSION_SECRET
).toString()
return [`session=${encrypted}`, ...cookieAttrs].join('; ')
}
// create user first, then log them in
const login = async (providerUser, accessToken, scope, options) => {
const { user, identity } = await getLocalUser(providerUser, options)
await db.identity.update({
where: { id: identity.id },
data: { accessToken, scope, lastLoginAt: new Date() },
})
return user
}
const getProviderUser = async (token) => {
const response = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${token}` },
})
const body = JSON.parse(await response.text())
return body
}
const getLocalUser = async (providerUser, options) => {
let user, identity
// user has been here before and has an identity
identity = await db.identity.findFirst({
where: { provider: 'github', uid: providerUser.id.toString() },
include: { user: true },
})
if (identity) {
logger.info('[OAUTH] Identity found')
return { user: identity.user, identity }
}
// existing users that aren't already linked to GitHub
user = await db.user.findFirst({
where: { username: providerUser.login },
})
if (user) {
identity = await createIdentity(user, providerUser)
logger.info('[OAUTH] User found, creating identity')
return { user, identity }
} else {
// new user, create account
const created = await createLocalUser(providerUser, options)
logger.info('[OAUTH] New user, creating')
return { user: created.user, identity: created.identity }
}
}
const createLocalUser = async (providerUser, options) => {
const [firstName, lastName] = providerUser.name?.split(' ') || [null, null]
// createUser() will validate invite code, just need to include it in the payload
const user = await createUser({
input: {
username: providerUser.login,
email: providerUser.email || `${providerUser.login}@spoke.run`,
firstName: firstName,
lastName: lastName,
avatar: providerUser.avatar_url,
bio: providerUser.bio,
url: providerUser.blog,
invite: options?.state,
},
})
const identity = await createIdentity(user, providerUser)
return { user, identity }
}
const createIdentity = async (user, providerUser) => {
return await db.identity.create({
data: {
provider: 'github',
uid: providerUser.id.toString(),
userId: user.id,
},
})
}
const redirectToSite = (cookie) => {
return {
statusCode: 302,
headers: {
'Set-Cookie': cookie,
Location: process.env.OAUTH_REDIRECT,
},
}
}
const errorStatus = (message) => {
return {
statusCode: 302,
headers: {
Location: `/login?error=${message}`,
},
}
}
const notFound = () => {
return {
statusCode: 404,
}
}
model Identity {
id Int @id @default(autoincrement())
provider String
uid String
userId Int
user User @relation(fields: [userId], references: [id])
accessToken String?
scope String?
lastLoginAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([provider, uid])
@@index(userId)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment