Giter Site home page Giter Site logo

tobymurray / postgraphile-login Goto Github PK

View Code? Open in Web Editor NEW
57.0 3.0 8.0 182 KB

Auth enabled Express Server with PostGraphile and PostgreSQL

JavaScript 23.55% PLpgSQL 70.27% Shell 6.18%
postgresql postgraphql express authentication authorization jwt postgraphile

postgraphile-login's Introduction

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: "[email protected]"
    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: "[email protected]"
    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: "[email protected]"
    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: "[email protected]"
    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: "[email protected]",
    activationCode: "00000000-0000-0000-0000-000000000000"
  }) {
    boolean
  }
}

Observe the response:

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

Activate with the right code

mutation {
  activateUser (input:{
    email: "[email protected]",
    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.

postgraphile-login's People

Contributors

dependabot[bot] avatar tobymurray avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

postgraphile-login's Issues

Parameterize SQL to make naming easier

Presently auth... is everywhere directly in the SQL. It'd be nice to generate the appropriate SQL provided e.g. a database name, schema names and user names.

Could potentially make a tool out of this to set up the database instead of a seed project.

Use indirection in RLS policies for figuring out user_id

Currently you use current_setting('jwt.claims.user_id') directly in your RLS policies. I recommend instead using a level of indirection, such as:

create or replace function auth_public.current_user_id() returns integer as $$
  select current_setting('jwt.claims.user_id', true)::integer;
$$ language sql stable;

CREATE POLICY update_user ON auth_public.user FOR UPDATE TO auth_authenticated 
  using (id = current_user_id());

This means that instead of having to update all your policies to make a change you only have to replace one function. Reasons you might make a change include:

  • accounting for new functionality, e.g. adding the missing_ok field as I have done above
  • allowed other methods of indicating the current user_id (e.g. for microservices that perform tasks against the db); this might just involve changing the function to use a coalesce(current_setting(...), current_setting(...)) call
  • if you decide to replace the current solution with a more secure one, such as one that looks up the current user ID based on a secure session ID hash (preventing the risk SQL injection allowing the user to switch to a different user ID via set local jwt.claims.user_id to 7)

I hope this is helpful ๐Ÿ‘

Turn on CI

Pretty much anything that validates the project still works as expected would be nice. Ideally:

  • Run script to insert into database
  • Start server
  • Execute a few workflows
  • Stop server
  • Run script to insert into database again, to ensure repeatability

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.