Skip to content

Instantly share code, notes, and snippets.

@khaledosman
Created February 11, 2020 13:53
Show Gist options
  • Save khaledosman/bae4fb146d97da5696d9012255f12fc5 to your computer and use it in GitHub Desktop.
Save khaledosman/bae4fb146d97da5696d9012255f12fc5 to your computer and use it in GitHub Desktop.
google/facebook/apple login
import axios from 'axios'
import * as jwt from 'jsonwebtoken'
import NodeRSA from 'node-rsa'
const clientID = process.env.APPLE_CLIENT_ID
const ENDPOINT_URL = 'https://appleid.apple.com'
const TOKEN_ISSUER = 'https://appleid.apple.com'
async function getApplePublicKey (): Promise<any> {
const url = `${ENDPOINT_URL}/auth/keys`
const data = await axios.get(url).then(res => res.data)
const key = data.keys[0]
const pubKey = new NodeRSA()
pubKey.importKey({ n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') }, 'components-public')
return pubKey.exportKey(['public'])
}
export const verifyAppleToken = async (idToken: string): Promise<any> => {
const applePublicKey = await getApplePublicKey()
const jwtClaims: any = jwt.verify(idToken, applePublicKey, { algorithms: ['RS256'] })
if (jwtClaims.iss !== TOKEN_ISSUER) {
throw new Error(
'id token not issued by correct OpenID provider - expected: ' + TOKEN_ISSUER + ' | from: ' + jwtClaims.iss
)
}
if (clientID !== undefined && jwtClaims.aud !== clientID) {
throw new Error('aud parameter does not include this client - is: ' + jwtClaims.aud + '| expected: ' + clientID)
}
if (jwtClaims.exp < Date.now() / 1000) throw new Error('id token has expired')
return jwtClaims
}
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
import { OvsToken } from '../interfaces/OvsToken'
const saltRounds = 10
export function signToken ({ _id }, { expiresIn = '10d' } = { expiresIn: '10d' }): string {
return jwt.sign({ _id, createdAt: Date.now() }, process.env.AUTH_SECRET, { expiresIn })
}
export function verifyToken (token: string): Promise<OvsToken> {
return new Promise((resolve, reject) => {
jwt.verify(token, process.env.AUTH_SECRET, function (err, decoded) {
if (err) reject(err)
resolve(decoded as OvsToken)
})
})
}
export function decodeToken (token: string): string | { [key: string]: any } {
return jwt.decode(token, {
json: true
})
}
export async function hashPassword (password: string): Promise<string> {
const salt = await bcrypt.genSalt(saltRounds)
return bcrypt.hash(password, salt)
}
export function comparePassword (password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash)
}
export function validateAuthHeader (authHeader: string): Promise<OvsToken> {
if (!authHeader) {
throw new Error('No auth header was sent')
}
const parts = authHeader.split(' ')
if (parts.length !== 2) {
throw new Error('Authentication header is invalid, did you forget the Bearer prefix?')
}
if (parts[0] === 'Bearer') {
const token = parts[1]
return verifyToken(token)
}
throw new Error('Unknown auth scheme, please send your header as Bearer <token>')
}
// https://medium.com/@byn9826/verify-facebook-login-by-python-e02ac1e23e37
import axios from 'axios'
const clientId = process.env.FACEBOOK_CLIENT_ID
const clientSecret = process.env.FACEBOOK_CLIENT_SECRET
export async function verifyFacebookToken (userId: string, userToken: string): Promise<any> {
// copy clientId, clientSecret from MY APP Page
// From appLink, retrieve the second accessToken: app access_token
const data = await axios
.get(
`https://graph.facebook.com/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`
)
.then(res => res.data)
const appToken = data.access_token
const tokenData = await Promise.all([
axios
.get(`https://graph.facebook.com/debug_token?input_token=${userToken}&access_token=${appToken}`)
.then(res => res.data),
axios
.get(`https://graph.facebook.com/${userId}?fields=id,name,email&access_token=${appToken}`)
.then(res => res.data)
])
return tokenData
}
import { OAuth2Client } from 'google-auth-library'
// const androidClientId = process.env.GOOGLE_CLIENT_ID_ANDROID
// const iosClientId = process.env.GOOGLE_CLIENT_ID_IOS
const googleClientId = process.env.GOOGLE_CLIENT_ID
export async function verifyGoogleToken (token: string): Promise<any> {
const client = new OAuth2Client(googleClientId)
const ticket = await client.verifyIdToken({
idToken: token,
audience: googleClientId // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
// [CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
})
const payload = ticket.getPayload()
return payload
}
import { AuthenticationError, ValidationError } from 'apollo-server-lambda'
import { comparePassword, signToken, hashPassword } from '../../helpers/authentication'
import { verifyAppleToken } from '../../helpers/apple-login-helpers'
import { verifyFacebookToken } from '../../helpers/facebook-login-helpers'
import { verifyGoogleToken } from '../../helpers/google-login-helpers'
import { IUser, LoggedInUser } from '../../interfaces/User'
import { Query } from 'mongoose'
import { OvsContext } from '../../interfaces/OvsContext'
import { getMongooseSelectionFromSelectedFields } from '../../helpers/get-mongoose-selection-from-selected-fields'
function verifyExternalToken (provider, id, token): Promise<any> {
const providerVerificationMap = {
FACEBOOK: (): Promise<any> => {
return verifyFacebookToken(id, token)
},
GOOGLE: (): Promise<any> => {
return verifyGoogleToken(token)
},
APPLE: (): Promise<any> => {
return verifyAppleToken(token)
}
}
return providerVerificationMap[provider]()
}
export const getCurrentUser = (_parentObj, _args, { user, mongooseConnection, err }: OvsContext, info): Query<IUser> => {
if (!user) {
throw new AuthenticationError(err.message)
} else {
const mongooseSelection = getMongooseSelectionFromSelectedFields(info)
return mongooseConnection
.model('User')
.findById(user._id)
.select(mongooseSelection)
.lean()
}
}
export async function login (_parentObj, args, { user, mongooseConnection }: OvsContext): Promise<LoggedInUser> {
if (user) {
throw new ValidationError('User is already logged in')
}
const { email, password } = args.input
const User = mongooseConnection.model('User')
const existingUser: IUser = await User.findOne({ email }).lean()
if (!existingUser) {
throw new ValidationError('No user found with the given email')
}
const isPasswordCorrect = await comparePassword(password, existingUser.password)
const jwtToken = signToken(existingUser)
if (isPasswordCorrect) {
return { ...existingUser, jwtToken }
} else {
throw new ValidationError('Incorrect password')
}
}
export async function externalLogin (_parentObj, args, { user, mongooseConnection }: OvsContext, info): Promise<LoggedInUser & {isSignup: boolean}> {
if (user) {
throw new ValidationError('User is already logged in')
}
let isSignup
const { email, id, token, provider, firstName, lastName } = args.input
const mongooseSelection = getMongooseSelectionFromSelectedFields(info)
const User = mongooseConnection.model('User')
const providerKey = `${provider.toLowerCase()}Provider`
console.log({ input: args.input })
const tokenData = await verifyExternalToken(provider, id, token).catch(err => {
console.error(err.response && err.response.data ? err.response.data : err)
throw new ValidationError(err.response && err.response.data ? err.response.data.error.message : err.message)
})
console.log({ tokenData })
const query = email ? { $or: [{ email }, { [`${providerKey}.id`]: id }] } : { [`${providerKey}.id`]: id }
let existingUser: IUser = await User.findOne(query).select(mongooseSelection).lean()
if (!existingUser) {
console.log('user doesnt exist, registering user')
isSignup = true
existingUser = await new User({
email: email || tokenData.email,
firstName,
lastName,
[providerKey]: {
id,
token
},
password: await hashPassword('' + Math.random())
}).save().then(doc => doc.toObject())
} else {
isSignup = false
existingUser = await User.findOneAndUpdate(
query,
{
[providerKey]: {
id,
token
}
}
).lean()
console.log('user already exists, signing in', existingUser)
}
const jwtToken = signToken(existingUser)
return { ...existingUser, jwtToken, isSignup }
}
export async function register (_parentObj, args, { user, mongooseConnection }): Promise<LoggedInUser> {
const { email, password, firstName, lastName } = args.input
if (user) {
throw new ValidationError('User is already logged in')
}
const User = mongooseConnection.model('User')
const existingUser = await User.findOne({ email }).lean()
if (existingUser) {
throw new ValidationError("There's already a user registered with the given email")
}
const hashedPassword = await hashPassword(password)
const newUser = await User.create({
email,
password: hashedPassword,
firstName,
lastName
})
const leanUser = newUser.toObject()
const jwtToken = signToken(leanUser)
return { ...leanUser, jwtToken }
}
import { OvsContext } from '../../interfaces/OvsContext'
import { AuthenticationError, ValidationError } from 'apollo-server-lambda'
import { getMongooseSelectionFromSelectedFields } from '../../helpers/get-mongoose-selection-from-selected-fields'
import { hashPassword } from '../../helpers/authentication'
import { IUser } from '../../interfaces/User'
export async function updateUser (_parent, args, context: OvsContext, info): Promise<IUser> {
const { err, mongooseConnection, user } = context
const UserModel = mongooseConnection.model('User')
if (!user) {
throw new AuthenticationError(err.message)
}
const {
password,
passwordConfirmation,
...rest
} = args.input
const mongooseSelection = getMongooseSelectionFromSelectedFields(info)
const updateBody = { ...rest }
if (password && passwordConfirmation) {
if (password !== passwordConfirmation) {
throw new ValidationError('passwords don\'t match')
} else {
const hashedPassword = await hashPassword(password)
updateBody.password = hashedPassword
}
}
if (updateBody.pushToken && updateBody.pushToken !== user.pushToken) {
await UserModel.findOneAndUpdate({ pushToken: user.pushToken }, { pushToken: null }).select({ _id: 1 }).lean()
}
return UserModel.findByIdAndUpdate(user._id, updateBody, { new: true }).select(mongooseSelection).lean()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment