From 2f1a725070c4279479285ceda0462206842e44cd Mon Sep 17 00:00:00 2001 From: Johannes Kettmann Date: Fri, 2 Aug 2019 07:52:24 +0200 Subject: [PATCH] Add graphql-local and facebook strategy --- .gitignore | 7 +++- README.md | 29 ++++++++++++++- api/index.js | 55 ++++++++++++++++++++++++++--- api/resolvers.js | 29 +++++++++++++++ api/typeDefs.js | 6 ++++ package-lock.json | 90 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 9 +++-- 7 files changed, 213 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index f9c6431..892c514 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,9 @@ yarn-debug.log* yarn-error.log* # editor -.vscode \ No newline at end of file +.vscode + +# environment variables +.env + +post.md \ No newline at end of file diff --git a/README.md b/README.md index 236bdeb..cd32e44 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,33 @@ npm start Visit [http://localhost:4000/graphql](http://localhost:4000/graphql). You will see the Apollo playground. There you can run following query and mutation ```graphql +mutation { + login(email: "maurice@moss.com", password: "abcdefg") { + user { + id + firstName + lastName + email + } + } +} + +mutation { + signup( + firstName: "Jen", + lastName: "Barber", + email: "jen@barber.com", + password: "qwerty" + ) { + user { + id + firstName + lastName + email + } + } +} + query { currentUser { id @@ -35,4 +62,4 @@ query { mutation { logout } -``` \ No newline at end of file +``` diff --git a/api/index.js b/api/index.js index a86b505..7a065eb 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,10 @@ +import 'dotenv/config'; import express from 'express'; import session from 'express-session'; import uuid from 'uuid/v4'; import passport from 'passport'; +import FacebookStrategy from 'passport-facebook'; +import { GraphQLLocalStrategy, buildContext } from 'graphql-passport'; import { ApolloServer } from 'apollo-server-express'; import User from './User'; import typeDefs from './typeDefs'; @@ -10,6 +13,47 @@ import resolvers from './resolvers'; const PORT = 4000; const SESSION_SECRECT = 'bad secret'; +const facebookOptions = { + clientID: process.env.FACEBOOK_APP_ID, + clientSecret: process.env.FACEBOOK_APP_SECRET, + callbackURL: 'http://localhost:4000/auth/facebook/callback', + profileFields: ['id', 'email', 'first_name', 'last_name'], +}; + +const facebookCallback = (accessToken, refreshToken, profile, done) => { + const users = User.getUsers(); + const matchingUser = users.find(user => user.facebookId === profile.id); + + if (matchingUser) { + done(null, matchingUser); + return; + } + + const newUser = { + id: uuid(), + facebookId: profile.id, + firstName: profile.name.givenName, + lastName: profile.name.familyName, + email: profile.emails && profile.emails[0] && profile.emails[0].value, + }; + users.push(newUser); + done(null, newUser); +}; + +passport.use(new FacebookStrategy( + facebookOptions, + facebookCallback, +)); + +passport.use( + new GraphQLLocalStrategy((email, password, done) => { + const users = User.getUsers(); + const matchingUser = users.find(user => email === user.email && password === user.password); + const error = matchingUser ? null : new Error('no matching user'); + done(error, matchingUser); + }), +); + passport.serializeUser((user, done) => { done(null, user.id); }); @@ -32,13 +76,16 @@ app.use(session({ app.use(passport.initialize()); app.use(passport.session()); +app.get('/auth/facebook', passport.authenticate('facebook', { scope: ['email'] })); +app.get('/auth/facebook/callback', passport.authenticate('facebook', { + successRedirect: 'http://localhost:4000/graphql', + failureRedirect: 'http://localhost:4000/graphql', +})); + const server = new ApolloServer({ typeDefs, resolvers, - context: ({ req }) => ({ - user: req.user, - logout: () => req.logout(), - }), + context: ({ req, res }) => buildContext({ req, res, User }), playground: { settings: { 'request.credentials': 'same-origin', diff --git a/api/resolvers.js b/api/resolvers.js index b017ea8..8108997 100644 --- a/api/resolvers.js +++ b/api/resolvers.js @@ -1,8 +1,37 @@ +import uuid from 'uuid/v4'; + const resolvers = { Query: { currentUser: (parent, args, context) => context.user, }, Mutation: { + signup: (parent, { firstName, lastName, email, password }, context) => { + const existingUsers = context.User.getUsers(); + const userWithEmailAlreadyExists = !!existingUsers.find(user => user.email === email); + + if (userWithEmailAlreadyExists) { + throw new Error('User with email already exists'); + } + + const newUser = { + id: uuid(), + firstName, + lastName, + email, + password, + }; + + context.User.addUser(newUser); + + context.login(newUser); + + return { user: newUser }; + }, + login: async (parent, { email, password }, context) => { + const { user } = await context.authenticate('graphql-local', { email, password }); + context.login(user); + return { user } + }, logout: (parent, args, context) => context.logout(), }, }; diff --git a/api/typeDefs.js b/api/typeDefs.js index 433df84..cd1cffa 100644 --- a/api/typeDefs.js +++ b/api/typeDefs.js @@ -12,7 +12,13 @@ const typeDefs = gql` currentUser: User } + type AuthPayload { + user: User + } + type Mutation { + signup(firstName: String!, lastName: String!, email: String!, password: String!): AuthPayload + login(email: String!, password: String!): AuthPayload logout: Boolean } `; diff --git a/package-lock.json b/package-lock.json index 71adfbc..f2e33d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2720,6 +2720,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -4229,9 +4234,9 @@ } }, "dotenv": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", - "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", + "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==" }, "dotenv-expand": { "version": "4.2.0", @@ -6072,6 +6077,29 @@ "@apollographql/apollo-tools": "^0.3.6" } }, + "graphql-passport": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/graphql-passport/-/graphql-passport-0.1.5.tgz", + "integrity": "sha512-iwR7eb+Z+XwJVgokF+DKcyLhxF5Py0QMd8XfR3IkN+6f4vHEQ12Uaw88KAJ7gGd+rc2FJXcEmCj/Ji7Z7+xoug==", + "requires": { + "passport-strategy": "^1.0.0", + "util": "^0.12.0" + }, + "dependencies": { + "util": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.0.tgz", + "integrity": "sha512-pPSOFl7VLhZ7LO/SFABPraZEEurkJUWSMn3MuA/r3WQZc+Z1fqou2JqLSOZbCLl73EUIxuUVX8X4jkX2vfJeAA==", + "requires": { + "inherits": "2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "object.entries": "^1.1.0", + "safe-buffer": "^5.1.2" + } + } + } + }, "graphql-subscriptions": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz", @@ -6687,6 +6715,11 @@ "kind-of": "^3.0.2" } }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -6786,6 +6819,11 @@ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -8540,6 +8578,11 @@ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==" }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -8604,6 +8647,17 @@ "object-keys": "^1.0.11" } }, + "object.entries": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", + "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, "object.fromentries": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.0.tgz", @@ -8911,6 +8965,26 @@ "pause": "0.0.1" } }, + "passport-facebook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-3.0.0.tgz", + "integrity": "sha512-K/qNzuFsFISYAyC1Nma4qgY/12V3RSLFdFVsPKXiKZt434wOvthFW1p7zKa1iQihQMRhaWorVE1o3Vi1o+ZgeQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-oauth2": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz", + "integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -10405,6 +10479,11 @@ "workbox-webpack-plugin": "4.2.0" }, "dependencies": { + "dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" + }, "fsevents": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.0.6.tgz", @@ -12173,6 +12252,11 @@ "random-bytes": "~1.0.0" } }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, "undefsafe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", diff --git a/package.json b/package.json index 4953edc..7ffd042 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "authentication-with-graphql-passport-and-react-starter", + "name": "frontend-for-authentication-with-graphql-and-passport", "version": "0.1.0", - "description": "Starter project accompanying a series of articles on authentication with GraphQL", + "description": "Frontend for tutorial about authentication with GraphQL and passport", "keywords": [ "graphql", "auth", @@ -15,16 +15,19 @@ }, "repository": { "type": "git", - "url": "git://github.com/jkettmann/authentication-with-graphql-passport-and-react-starter.git" + "url": "git@github.com:jkettmann/frontend-for-authentication-with-graphql-and-passport.git" }, "license": "MIT", "dependencies": { "apollo-server-express": "^2.6.3", + "dotenv": "^8.0.0", "express": "^4.17.1", "express-session": "^1.16.2", "graphql": "^14.3.1", + "graphql-passport": "^0.1.5", "nodemon": "^1.19.1", "passport": "^0.4.0", + "passport-facebook": "^3.0.0", "react": "^16.8.6", "react-dom": "^16.8.6", "react-scripts": "3.0.1",