From aa61276ce3d727f9e75b87decba2e9bc481e8f5d Mon Sep 17 00:00:00 2001 From: allpwrfulroot Date: Sat, 22 Jul 2017 21:01:40 -0400 Subject: [PATCH 1/2] phoneNumber and deviceId alternative to email and password authentication --- phone-user-management/README.md | 39 +++++++++++ .../functions/authenticate/README.md | 36 ++++++++++ .../functions/authenticate/authenticate.js | 53 ++++++++++++++ .../authenticate/schema-extension.graphql | 7 ++ .../functions/signup/README.md | 37 ++++++++++ .../functions/signup/schema-extension.graphql | 7 ++ .../functions/signup/signup.js | 62 +++++++++++++++++ .../functions/update-device/README.md | 36 ++++++++++ .../update-device/schema-extension.graphql | 7 ++ .../functions/update-device/update-device.js | 66 ++++++++++++++++++ .../functions/update-phone/README.md | 38 ++++++++++ .../update-phone/schema-extension.graphql | 8 +++ .../functions/update-phone/update-phone.js | 69 +++++++++++++++++++ .../phone-user-management.graphql | 10 +++ 14 files changed, 475 insertions(+) create mode 100644 phone-user-management/README.md create mode 100644 phone-user-management/functions/authenticate/README.md create mode 100644 phone-user-management/functions/authenticate/authenticate.js create mode 100644 phone-user-management/functions/authenticate/schema-extension.graphql create mode 100644 phone-user-management/functions/signup/README.md create mode 100644 phone-user-management/functions/signup/schema-extension.graphql create mode 100644 phone-user-management/functions/signup/signup.js create mode 100644 phone-user-management/functions/update-device/README.md create mode 100644 phone-user-management/functions/update-device/schema-extension.graphql create mode 100644 phone-user-management/functions/update-device/update-device.js create mode 100644 phone-user-management/functions/update-phone/README.md create mode 100644 phone-user-management/functions/update-phone/schema-extension.graphql create mode 100644 phone-user-management/functions/update-phone/update-phone.js create mode 100644 phone-user-management/phone-user-management.graphql diff --git a/phone-user-management/README.md b/phone-user-management/README.md new file mode 100644 index 0000000..7869bbc --- /dev/null +++ b/phone-user-management/README.md @@ -0,0 +1,39 @@ +# phone-user-management + +Functions for phone and deviceID login using Schema Extensions and Graphcool Functions ⚡️ + +> Note: Schema Extensions are currently only available in the Beta Program. + +## Getting Started + +```sh +npm -g install graphcool +graphcool init --schema phone-user-management.graphql +``` + +## Usage + +1. Your app calls the Graphcool mutation `signupPhoneUser(phoneNumber: String!, deviceId: String!)` to signup Users +2. Your app calls the Graphcool mutation `authenticatePhoneUser(phoneNumber: String!, deviceId: String!)` to authenticate Users +3. Your app calls the Graphcool mutation `updatePhone(phoneNumber: String!, deviceId: String!, newPhoneNumber: String!)` to change a User's phone number (use case: gets new phone number) +4. Your app calls the Graphcool mutation `updateDevice(phoneNumber: String!, deviceId: String!, newDeviceId: String!)` to change a User's deviceId (use case: gets new phone) + +## Notes + +The function in this example use a Permanent Authentication Token to fetch or create users via the API so the permissions are not needed. Therefore, it's recommended to **remove Create, Update and Read permissions** for the fields `phoneNumber` and `deviceId` on `User`. Those fields are controlled by the custom mutations that are added in this example. + +This also allows you to change the phoneNumber or deviceId fields using a PAT manually as an administrator. For manually updating deviceId, note that the `deviceId` field is hashed and salted using `bcrypt` and for authenticating or updating the user information, the current deviceId is compared to that hashed and salted version. + +## Test the Code + +First, follow the READMEs to setup all the functions in the functions folder. Then go to the Graphcool Playground: + +```sh +graphcool playground +``` + +and follow the instructions in the functions folder to test the code. It makes sense to setup the functions in the above order. + +## Contributions + +A straightforward adaptation of [@stevewpatterson](https://github.com/stevewpatterson)'s email auth example diff --git a/phone-user-management/functions/authenticate/README.md b/phone-user-management/functions/authenticate/README.md new file mode 100644 index 0000000..051f5dd --- /dev/null +++ b/phone-user-management/functions/authenticate/README.md @@ -0,0 +1,36 @@ +# authenticate + +Authenticate users in your app using phoneNumber and deviceId and receive a token in return. + +## Authentication flow in app + +1. Your app calls the Graphcool mutation `authenticatePhoneUser(phoneNumber: String!, deviceId: String!)` +2. If no user exists yet that corresponds to the passed `phoneNumber`, or the `deviceId` does not match, an error will be returned +3. If a user with the passed `phoneNumber` exists and the `deviceId` matches, the mutation returns a valid token for the user +4. Your app stores the token and uses it in its `Authorization` header for all further requests to Graphcool + +## Setup the Authentication Function + +* Create a new Schema Extension Function and paste the schema from `schema-extension.graphql` and code from `authenticate.js`. +* add a PAT to the project *called the same as your function*. The token can be obtained from the Authentication tab in the project settings. + +## Test the Code + +First, you need to create a new user with the `signup` function. Then, go to the Graphcool Playground: + +```sh +graphcool playground +``` + +and run this mutation to authenticate as that user: + +```graphql +mutation { + # replace __PHONE_NUMBER__ and __DEVICE_ID__ + authenticatePhoneUser(phoneNumber: "__PHONE_NUMBER__", deviceId: "__DEVICE_ID__") { + token + } +} +``` + +If the phoneNumber/deviceId combo are valid you should see that it returns a token. The returned token can be used to authenticate requests to your Graphcool API as that user. diff --git a/phone-user-management/functions/authenticate/authenticate.js b/phone-user-management/functions/authenticate/authenticate.js new file mode 100644 index 0000000..5bc663f --- /dev/null +++ b/phone-user-management/functions/authenticate/authenticate.js @@ -0,0 +1,53 @@ +const fromEvent = require('graphcool-lib').fromEvent +const bcrypt = require('bcrypt') + +module.exports = function(event) { + const phoneNumber = event.data.phoneNumber + const deviceId = event.data.deviceId + const graphcool = fromEvent(event) + const api = graphcool.api('simple/v1') + + function getGraphcoolUser(phoneNumber) { + return api.request(` + query { + User(phoneNumber: "${phoneNumber}"){ + id + deviceId + } + }`) + .then((userQueryResult) => { + if (userQueryResult.error) { + return Promise.reject(userQueryResult.error) + } else { + return userQueryResult.User + } + }) + } + + function generateGraphcoolToken(graphcoolUserId) { + return graphcool.generateAuthToken(graphcoolUserId, 'User') + } + + return getGraphcoolUser(phoneNumber) + .then((graphcoolUser) => { + if (graphcoolUser === null) { + return Promise.reject("Invalid Credentials") //returning same generic error so user can't find out what phoneNumbers are registered. + } else { + return bcrypt.compare(deviceId, graphcoolUser.deviceId) + .then((res) => { + if (res === true) { + return graphcoolUser.id + } else { + return Promise.reject("Invalid Credentials") + } + }) + } + }) + .then(generateGraphcoolToken) + .then((token) => { + return { data: { token } } + }) + .catch((error) => { + return { error: error.toString() } + }) +} diff --git a/phone-user-management/functions/authenticate/schema-extension.graphql b/phone-user-management/functions/authenticate/schema-extension.graphql new file mode 100644 index 0000000..ef3af59 --- /dev/null +++ b/phone-user-management/functions/authenticate/schema-extension.graphql @@ -0,0 +1,7 @@ +type AuthenticatePhoneUserPayload { + token: String! +} + +extend type Mutation { + authenticatePhoneUser(phoneNumber: String!, deviceId: String!): AuthenticatePhoneUserPayload +} diff --git a/phone-user-management/functions/signup/README.md b/phone-user-management/functions/signup/README.md new file mode 100644 index 0000000..9b557a9 --- /dev/null +++ b/phone-user-management/functions/signup/README.md @@ -0,0 +1,37 @@ +# phone-user-creation + +Signup new users in your app using phone number and device ID and receive a token in return. + +## Signup flow in app + +1. Your app calls the Graphcool mutation `signupPhoneUser(phoneNumber: String!, deviceId: String!)` +2. If no user exists yet that corresponds to the passed `phoneNumber`, a new `User` node will be created with the deviceId (after being hashed and salted) +3. If a user with the passed `phoneNumber` exists, a `User` node is not created and an error is returned +4. If a user is created, then the `signupPhoneUser(phoneNumber: String!, deviceId: String!)` mutation returns the id for the new user + +## Setup the Create User Function + +* Create a new Schema Extension Function and paste the schema from `schema-extension.graphql` and code from `signup.js`. +* add a PAT to the project *called the same as your function*. The token can be obtained from the Authentication tab in the project settings. +* Remove all Create permissions for the `User` type. The function uses a Permanent Access Token to create users via the API so the permissions are not needed. + +## Test the Code + +Go to the Graphcool Playground: + +```sh +graphcool playground +``` + +Run this mutation to create a user: + +```graphql +mutation { + # replace __PHONE_NUMBER__ and __DEVICE_ID__ + signupPhoneUser(phoneNumber: "__PHONE_NUMBER__", deviceId: "__DEVICE_ID__") { + id + } +} +``` + +You should see that a new user has been created. The returned id can be used to query your Graphcool API for that user. Note that running the mutation again with the same phoneNumber will return an error stating that the phoneNumber is already in use. diff --git a/phone-user-management/functions/signup/schema-extension.graphql b/phone-user-management/functions/signup/schema-extension.graphql new file mode 100644 index 0000000..0c6ee58 --- /dev/null +++ b/phone-user-management/functions/signup/schema-extension.graphql @@ -0,0 +1,7 @@ +type SignupPhoneUserPayload { + id: ID! +} + +extend type Mutation { + signupPhoneUser(phoneNumber: String!, deviceId: String!): SignupPhoneUserPayload +} diff --git a/phone-user-management/functions/signup/signup.js b/phone-user-management/functions/signup/signup.js new file mode 100644 index 0000000..f02e3c4 --- /dev/null +++ b/phone-user-management/functions/signup/signup.js @@ -0,0 +1,62 @@ +const fromEvent = require('graphcool-lib').fromEvent +const bcrypt = require('bcrypt') +const validator = require('validator') + +module.exports = function(event) { + const phoneNumber = event.data.phoneNumber + const deviceId = event.data.deviceId + const graphcool = fromEvent(event) + const api = graphcool.api('simple/v1') + const SALT_ROUNDS = 10 + + function getGraphcoolUser(phoneNumber) { + return api.request(` + query { + User(phoneNumber: "${phoneNumber}") { + id + } + }`) + .then((userQueryResult) => { + if (userQueryResult.error) { + return Promise.reject(userQueryResult.error) + } else { + return userQueryResult.User + } + }) + } + + function createGraphcoolUser(phoneNumber, deviceIdHash) { + return api.request(` + mutation { + createUser( + phoneNumber: "${phoneNumber}", + deviceId: "${deviceIdHash}" + ) { + id + } + }`) + .then((userMutationResult) => { + return userMutationResult.createUser.id + }) + } + + if (validator.isMobilePhone(phoneNumber, 'any')) { + return getGraphcoolUser(phoneNumber) + .then((graphcoolUser) => { + if (graphcoolUser === null) { + return bcrypt.hash(deviceId, SALT_ROUNDS) + .then(hash => createGraphcoolUser(phoneNumber, hash)) + } else { + return Promise.reject("phone number already in use") + } + }) + .then((id) => { + return { data: { id } } + }) + .catch((error) => { + return { error: error.toString() } + }) + } else { + return { error: "Not a valid phone number" } + } +} diff --git a/phone-user-management/functions/update-device/README.md b/phone-user-management/functions/update-device/README.md new file mode 100644 index 0000000..ab4a4b0 --- /dev/null +++ b/phone-user-management/functions/update-device/README.md @@ -0,0 +1,36 @@ +# update-device + +Update the deviceId for users in your app who login using phoneNumber and deviceId. + +## Update deviceId flow in app + +1. Your app calls the Graphcool mutation `updateDevice(phoneNumber: String!, deviceId: String!, newDeviceId: String!)` +3. If a user with the phoneNumber and deviceId combination is not found, an error is returned +4. If a user with the passed `phoneNumber` exists and `deviceId` matches, the user's `deviceId` field is updated to the new deviceId. +5. The mutation returns the id of the updated user + +## Setup the deviceId Update Function + +* Create a new Schema Extension Function and paste the schema from `schema-extension.graphql` and code from `update-device.js`. +* add a PAT to the project *called the same as your function*. The token can be obtained from the Authentication tab in the project settings. + +## Test the Code + +First, you need to create a new user with the `signup` function. Then, go to the Graphcool Playground: + +```sh +graphcool playground +``` + +Run this mutation to change a user's deviceId: + +```graphql +mutation { + # replace __PHONE_NUMBER__ , __OLD_DEVICE_ID__ , and __NEW_DEVICE_ID__ + updateDevice(phoneNumber: "__PHONE_NUMBER__", deviceId: "__OLD_DEVICE_ID__", newDeviceId: "__NEW_DEVICE_ID__") { + id + } +} +``` + +If the phoneNumber/newDeviceId combo are valid you should see that it updates the user's deviceId field and returns the user's id. diff --git a/phone-user-management/functions/update-device/schema-extension.graphql b/phone-user-management/functions/update-device/schema-extension.graphql new file mode 100644 index 0000000..8ac565e --- /dev/null +++ b/phone-user-management/functions/update-device/schema-extension.graphql @@ -0,0 +1,7 @@ +type UpdateDevicePayload { + id: ID! +} + +extend type Mutation { + updateDevice(phoneNumber: String!, deviceId: String!, newDeviceId: String!): UpdateDevicePayload +} diff --git a/phone-user-management/functions/update-device/update-device.js b/phone-user-management/functions/update-device/update-device.js new file mode 100644 index 0000000..87f463f --- /dev/null +++ b/phone-user-management/functions/update-device/update-device.js @@ -0,0 +1,66 @@ +const fromEvent = require('graphcool-lib').fromEvent +const bcrypt = require('bcrypt') + +module.exports = function(event) { + const phoneNumber = event.data.phoneNumber + const deviceId = event.data.deviceId + const newDeviceId = event.data.newDeviceId + const graphcool = fromEvent(event) + const api = graphcool.api('simple/v1') + const saltRounds = 10 + + function getGraphcoolUser(phoneNumber) { + return api.request(` + query { + User(phoneNumber: "${phoneNumber}"){ + id + deviceId + } + }`) + .then((userQueryResult) => { + if (userQueryResult.error) { + return Promise.reject(userQueryResult.error) + } else { + return userQueryResult.User + } + }) + } + + function updateGraphcoolUser(id, newDeviceIdHash) { + return api.request(` + mutation { + updateUser( + id:"${id}", + deviceId:"${newDeviceIdHash}" + ){ + id + } + }`) + .then((userMutationResult) => { + return userMutationResult.updateUser.id + }) + } + + return getGraphcoolUser(phoneNumber) + .then((graphcoolUser) => { + if (graphcoolUser === null) { + return Promise.reject("Invalid Credentials") + } else { + return bcrypt.compare(deviceId, graphcoolUser.deviceId) + .then((res) => { + if (res == true) { + return bcrypt.hash(newDeviceId, saltRounds) + .then(hash => updateGraphcoolUser(graphcoolUser.id, hash)) + } else { + return Promise.reject("Invalid Credentials") + } + }) + } + }) + .then((id) => { + return { data: { id } } + }) + .catch((error) => { + return { error: error.toString() } + }) +} diff --git a/phone-user-management/functions/update-phone/README.md b/phone-user-management/functions/update-phone/README.md new file mode 100644 index 0000000..e325ea6 --- /dev/null +++ b/phone-user-management/functions/update-phone/README.md @@ -0,0 +1,38 @@ +# update-phone + +Update the phoneNumber for users in your app who login using phoneNumber and deviceId. + +## Update phoneNumber flow in app + +1. Your app calls the Graphcool mutation `updatePhone(phoneNumber: String!, deviceId: String!, newPhoneNumber: String!)` +2. If `newPhoneNumber` passed is not a valid format, an error is returned +3. If a user with the passed `phoneNumber` and `deviceId` combination is not found, an error is returned +4. If a user with the passed `phoneNumber` exists and the `deviceId` matches, the user's `phoneNumber` field is updated to the new phoneNumber +5. The mutation returns the id and new phoneNumber of the updated user + +## Setup the Email Update Function + +* Create a new Schema Extension Function and paste the schema from `schema-extension.graphql` and code from `update-phone.js`. +* add a PAT to the project *called the same as your function*. The token can be obtained from the Authentication tab in the project settings. + +## Test the Code + +First, you need to create a new user with the `signup` function. Then, go to the Graphcool Playground: + +```sh +graphcool playground +``` + +Run this mutation to change a user's phoneNumber: + +```graphql +mutation { + # replace __PHONE_NUMBER__ , __NEW_PHONE_NUMBER__ , and __DEVICE_ID__ + updatePhone(phoneNumber: "__PHONE_NUMBER__", deviceId: "__DEVICE_ID__", newPhoneNumber: "__NEW_PHONE_NUMBER__") { + id + phoneNumber + } +} +``` + +If the phoneNumber/deviceId combo are valid, and the newPhoneNumber is a valid format, you should see that it updates the user's phoneNumber field and returns the user's id and new phoneNumber. diff --git a/phone-user-management/functions/update-phone/schema-extension.graphql b/phone-user-management/functions/update-phone/schema-extension.graphql new file mode 100644 index 0000000..669acf1 --- /dev/null +++ b/phone-user-management/functions/update-phone/schema-extension.graphql @@ -0,0 +1,8 @@ +type UpdatePhonePayload { + id: ID! + phoneNumber: String! +} + +extend type Mutation { + updatePhone(phoneNumber: String!, deviceId: String!, newPhoneNumber: String!): UpdatePhonePayload +} diff --git a/phone-user-management/functions/update-phone/update-phone.js b/phone-user-management/functions/update-phone/update-phone.js new file mode 100644 index 0000000..ef258f7 --- /dev/null +++ b/phone-user-management/functions/update-phone/update-phone.js @@ -0,0 +1,69 @@ +const fromEvent = require('graphcool-lib').fromEvent +const bcrypt = require('bcrypt') +const validator = require('validator') + +module.exports = function(event) { + const phoneNumber = event.data.phoneNumber + const newPhoneNumber = event.data.newPhoneNumber + const deviceId = event.data.deviceId + const graphcool = fromEvent(event) + const api = graphcool.api('simple/v1') + + function getGraphcoolUser(phoneNumber) { + return api.request(` + query { + User(phoneNumber: "${phoneNumber}") { + id + deviceId + } + }`) + .then((userQueryResult) => { + if (userQueryResult.error) { + return Promise.reject(userQueryResult.error) + } else { + return userQueryResult.User + } + }) + } + + function updateGraphcoolUser(id, newphoneNumber) { + return api.request(` + mutation { + updateUser( + id: "${id}", + phoneNumber: "${newPhoneNumber}" + ) { + id + } + }`) + .then((userMutationResult) => { + return userMutationResult.updateUser.id + }) + } + + if (validator.isMobilePhone(newPhoneNumber, 'any')) { + return getGraphcoolUser(phoneNumber) + .then((graphcoolUser) => { + if (graphcoolUser === null) { + return Promise.reject("Invalid Credentials") + } else { + return bcrypt.compare(deviceId, graphcoolUser.deviceId) + .then((res) => { + if (res == true) { + return updateGraphcoolUser(graphcoolUser.id, newPhoneNumber) + } else { + return Promise.reject("Invalid Credentials") + } + }) + } + }) + .then((id) => { + return { data: { id, phoneNumber: newPhoneNumber } } + }) + .catch((error) => { + return { error: error.toString() } + }) + } else { + return { error: "Not a valid phone number" } + } +} diff --git a/phone-user-management/phone-user-management.graphql b/phone-user-management/phone-user-management.graphql new file mode 100644 index 0000000..ded3835 --- /dev/null +++ b/phone-user-management/phone-user-management.graphql @@ -0,0 +1,10 @@ +# This schema is used for all included functions + +type User { + id: ID! + + # Phone number must be unique + # Both should be required if device authorization is the primary auth for your app + phoneNumber: String @isUnique + deviceId: String +} From 20c5119ba1daa3221c0f15faf6870ed4978416cd Mon Sep 17 00:00:00 2001 From: Nilan Marktanner Date: Mon, 24 Jul 2017 15:03:50 +0200 Subject: [PATCH 2/2] wording and contribution --- README.md | 3 +++ phone-user-management/README.md | 2 +- phone-user-management/functions/authenticate/authenticate.js | 2 +- .../functions/update-device/update-device.js | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1d4ca11..d1dcd01 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ A big thank you to all contributors and supporters of this repository 💚 petrvlcek + + allpwrfulroot + ## Help & Community [![Slack Status](https://slack.graph.cool/badge.svg)](https://slack.graph.cool) diff --git a/phone-user-management/README.md b/phone-user-management/README.md index 7869bbc..1e2ed42 100644 --- a/phone-user-management/README.md +++ b/phone-user-management/README.md @@ -36,4 +36,4 @@ and follow the instructions in the functions folder to test the code. It makes s ## Contributions -A straightforward adaptation of [@stevewpatterson](https://github.com/stevewpatterson)'s email auth example +A straightforward adaptation of [@stevewpatterson](https://github.com/stevewpatterson)'s email auth example by [allpwrfulroot](https://github.com/allpwrfulroot) :tada: diff --git a/phone-user-management/functions/authenticate/authenticate.js b/phone-user-management/functions/authenticate/authenticate.js index 5bc663f..469e0be 100644 --- a/phone-user-management/functions/authenticate/authenticate.js +++ b/phone-user-management/functions/authenticate/authenticate.js @@ -10,7 +10,7 @@ module.exports = function(event) { function getGraphcoolUser(phoneNumber) { return api.request(` query { - User(phoneNumber: "${phoneNumber}"){ + User(phoneNumber: "${phoneNumber}") { id deviceId } diff --git a/phone-user-management/functions/update-device/update-device.js b/phone-user-management/functions/update-device/update-device.js index 87f463f..b36ad97 100644 --- a/phone-user-management/functions/update-device/update-device.js +++ b/phone-user-management/functions/update-device/update-device.js @@ -12,7 +12,7 @@ module.exports = function(event) { function getGraphcoolUser(phoneNumber) { return api.request(` query { - User(phoneNumber: "${phoneNumber}"){ + User(phoneNumber: "${phoneNumber}") { id deviceId } @@ -32,7 +32,7 @@ module.exports = function(event) { updateUser( id:"${id}", deviceId:"${newDeviceIdHash}" - ){ + ) { id } }`)