Skip to content

Auth enabled Express Server with PostGraphile and PostgreSQL

Notifications You must be signed in to change notification settings

tobymurray/postgraphile-login

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Note: This project is mostly taken from the wonderful PostGraphile tutorial.

What is it?

A minimal authentication and authorization enabled Express server with PostGraphile middleware creating a GraphQL server from a PostgreSQL schema.

Email account activation

See the this branch for an integration of email activation. This workflow creates users that are not "activated" until they provide their activation code from their email.

Get it running

  1. Clone this repository
    • git clone https://github.com/tobymurray/postgraphile-login.git
  2. Install dependencies
    • yarn or npm install
  3. Ensure you have a PostgreSQL server running somewhere. If you don't, start one.
    • E.g.: docker run --restart=always -p 5432:5432 --name postgres -e POSTGRES_PASSWORD=password -d postgres:alpine
  4. Ensure you have a PostgreSQL client available. If you don't, install one.
    • E.g.: sudo apt install postgresql-client
  5. Fill out the .env file with the relevant connection details
    • Note that if you change values, you may have to update provision.sql
  6. Load the contents of provision.sql into your PostgreSQL server
    • E.g.: psql -h localhost -U postgres -f provision.sql
    • NOTE: If you're using docker, you need to specify the host explicilty (PSQL tries the socket by default, which fails)
  7. Start the server
    • npm start

Try it out

  1. Navigate to GraphiQL the port you've configured (3000 by default)

Create a user

  1. Register a user via GraphQL mutation
    • e.g.
mutation {
  registerUser(input: {
    firstName: "Genghis"
    lastName: "Khan"
    email: "Genghis@khan.mn"
    password: "Genghis1162"
  }) {
    user {
      id
      firstName
      lastName
      createdAt
    }
  }
}
  1. Observe the response
    • e.g.
{
  "data": {
    "registerUser": {
      "user": {
        "id": 2,
        "firstName": "Genghis",
        "lastName": "Khan",
        "createdAt": "2017-06-11T06:17:39.084578"
      }
    }
  }
}

Observe authentication working

  1. Try authenticating with a different GraphQL mutation
    • e.g.
mutation {
  authenticate(input: {
    email: "Genghis@khan.mn"
    password: "Genghis1162"
  }) {
    jwt 
  }
}
  1. Observe the response
    • e.g.:
{
  "data": {
    "authenticate": {
      "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXV0aF9hdXRoZW50aWNhdGVkIiwidXNlcl9pZCI6MiwiaWF0IjoxNDk3MTYyMTIyLCJleHAiOjE0OTcyNDg1MjIsImF1ZCI6InBvc3RncmFwaHFsIiwiaXNzIjoicG9zdGdyYXBocWwifQ.hLZ7p3vJs3UYW9IKB7u8tbXONUl_tZoWhiAAD1-OPQg"
    }
  }
}

Try making an unauthenticated request when authentication is necessary

  1. currentUser is protected, so query that
query {
  currentUser{
    id
    firstName
    lastName
    createdAt
  }
}
  1. Observe the not-particularly-friendly response
{
  "errors": [
    {
      "message": "unrecognized configuration parameter \"jwt.claims.user_id\"",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "currentUser"
      ]
    }
  ],
  "data": {
    "currentUser": null
  }
}

Try making an authenticated request when authentication is necessary

  1. You'll need the ability to send your JWT to the server, which unfortunately isn't possible with vanilla GraphiQL.
    • If you're in Chrome you can try ModHeader
    • If you're in Firefox you can try Modify Headers
    • If you're in another browser, you can try Chrome or Firefox
  2. Set an authorization header by copy/pasting the value out of the jwt field in the authenticate response in step 5.
    • Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXV0aF9hdXRoZW50aWNhdGVkIiwidXNlcl9pZCI6MSwiaWF0IjoxNDk3MTYwNzA3LCJleHAiOjE0OTcyNDcxMDcsImF1ZCI6InBvc3RncmFwaHFsIiwiaXNzIjoicG9zdGdyYXBocWwifQ.aInZvEVhhDfi9yQDWRzvmSaE7Mk2PufbBrY3rxGlEt8
    • Don't forget the Bearer on the right side of the header, otherwise you'll likely see Authorization header is not of the correct bearer scheme format.
  3. Submit the query with the authorization header attached
query {
  currentUser{
    nodeId
    id
    firstName
    lastName
    createdAt
  }
}
  1. Observe your now successful response
{
  "data": {
    "currentUser": {
      "nodeId": "WyJ1c2VycyIsMl0=",
      "id": 2,
      "firstName": "Genghis",
      "lastName": "Khan",
      "createdAt": "2017-06-11T06:17:39.084578"
    }
  }
}

Observe authorization working

  1. With the authorization header set, try updating Genghis
mutation {
  updateUser(input: {
    nodeId: "WyJ1c2VycyIsMl0="
    userPatch: {
      lastName: "NotKhan"
    }
  }) {
    user {
      nodeId
      id
      firstName
      lastName
      createdAt
    }
  }
}
  1. Observe that it works:
{
  "data": {
    "updateUser": {
      "user": {
        "nodeId": "WyJ1c2VycyIsMl0=",
        "id": 2,
        "firstName": "Ghengis",
        "lastName": "NotKhan",
        "createdAt": "2017-06-11T06:17:39.084578"
      }
    }
  }
}
  1. Add a friend
mutation {
  registerUser(input: {
    firstName: "Serena"
    lastName: "Williams"
    email: "Serena@Williams.ca"
    password: "NotGhengis"
  }) {
    user {
      nodeId
      id
      firstName
      lastName
      createdAt
    }
  }
}
  1. Keeping Genghis' JWT, try modifying your friend
    • Note this is Serena's nodeId
mutation {
  updateUser(input: {
    nodeId: "WyJ1c2VycyIsM10="
    userPatch: {
      lastName: "KhanMaybe?"
    }
  }) {
    user {
      nodeId
      id
      firstName
      lastName
      createdAt
    }
  }
}
  1. Get rejected
{
  "errors": [
    {
      "message": "No values were updated in collection 'users' using key 'id' because no values were found.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "updateUser"
      ]
    }
  ],
  "data": {
    "updateUser": null
  }
}

Activate user

Running the server on this branch for the first time will prompt you to integrate with Gmail. Subsequent times, your client key should be cached. Once Gmail integration is set up, create a user with a real email address you control.

mutation {
  registerUser(input: {
    firstName: "Firstname"
    lastName: "Lastname"
    email: "realemail@gmail.com"
    password: "doesNotMatter"
  }) {
    user {
      id
      firstName
      lastName
      createdAt
    }
  }
}

There's nothing particularly notable about the response here, so you can ignore it.

Activate with the wrong code

mutation {
  activateUser(input: {
    email: "realemail@gmail.com",
    activationCode: "00000000-0000-0000-0000-000000000000"
  }) {
    boolean
  }
}

Observe the response:

{
  "data": {
    "activateUser": {
      "boolean": false
    }
  }
}

Activate with the right code

mutation {
  activateUser (input:{
    email: "realemail@gmail.com",
    activationCode: "e0df9b6b-ef0f-417c-823a-6e871f5c7d43"
  }) {
    boolean
  }
}

And observe the successful activation!

{
  "data": {
    "activateUser": {
      "boolean": true
    }
  }
}

Note if you actually use this

Move or remove the .env file and add .env to the .gitignore, then bring your .env back. This will ensure your environment variables (in particular your application server secret) are not added to version control and ultimately shared.

Why write this up?

I like to build largely disposable web apps in my spare time, and almost every one needs authentication and authorization to be at all usable. Auth is hard and boring and generally not value added, so I plan on using this as something of a seed for weekend projects.