Created
February 11, 2020 13:53
-
-
Save khaledosman/bae4fb146d97da5696d9012255f12fc5 to your computer and use it in GitHub Desktop.
google/facebook/apple login
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>') | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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