Note: This project is mostly taken from the wonderful PostGraphile tutorial.
A minimal authentication and authorization enabled Express server with PostGraphile middleware creating a GraphQL server from a PostgreSQL schema.
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.
- Clone this repository
git clone https://github.com/tobymurray/postgraphile-login.git
- Install dependencies
yarn
ornpm install
- 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
- E.g.:
- Ensure you have a PostgreSQL client available. If you don't, install one.
- E.g.:
sudo apt install postgresql-client
- E.g.:
- Fill out the
.env
file with the relevant connection details- Note that if you change values, you may have to update
provision.sql
- Note that if you change values, you may have to update
- 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)
- E.g.:
- Start the server
npm start
- Navigate to GraphiQL the port you've configured (3000 by default)
- 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
}
}
}
- Observe the response
- e.g.
{
"data": {
"registerUser": {
"user": {
"id": 2,
"firstName": "Genghis",
"lastName": "Khan",
"createdAt": "2017-06-11T06:17:39.084578"
}
}
}
}
- Try authenticating with a different GraphQL mutation
- e.g.
mutation {
authenticate(input: {
email: "Genghis@khan.mn"
password: "Genghis1162"
}) {
jwt
}
}
- Observe the response
- e.g.:
{
"data": {
"authenticate": {
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXV0aF9hdXRoZW50aWNhdGVkIiwidXNlcl9pZCI6MiwiaWF0IjoxNDk3MTYyMTIyLCJleHAiOjE0OTcyNDg1MjIsImF1ZCI6InBvc3RncmFwaHFsIiwiaXNzIjoicG9zdGdyYXBocWwifQ.hLZ7p3vJs3UYW9IKB7u8tbXONUl_tZoWhiAAD1-OPQg"
}
}
}
currentUser
is protected, so query that
query {
currentUser{
id
firstName
lastName
createdAt
}
}
- 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
}
}
- 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
- Set an authorization header by copy/pasting the value out of the
jwt
field in theauthenticate
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 seeAuthorization header is not of the correct bearer scheme format.
- Submit the query with the authorization header attached
query {
currentUser{
nodeId
id
firstName
lastName
createdAt
}
}
- Observe your now successful response
{
"data": {
"currentUser": {
"nodeId": "WyJ1c2VycyIsMl0=",
"id": 2,
"firstName": "Genghis",
"lastName": "Khan",
"createdAt": "2017-06-11T06:17:39.084578"
}
}
}
- With the authorization header set, try updating Genghis
mutation {
updateUser(input: {
nodeId: "WyJ1c2VycyIsMl0="
userPatch: {
lastName: "NotKhan"
}
}) {
user {
nodeId
id
firstName
lastName
createdAt
}
}
}
- Observe that it works:
{
"data": {
"updateUser": {
"user": {
"nodeId": "WyJ1c2VycyIsMl0=",
"id": 2,
"firstName": "Ghengis",
"lastName": "NotKhan",
"createdAt": "2017-06-11T06:17:39.084578"
}
}
}
}
- Add a friend
mutation {
registerUser(input: {
firstName: "Serena"
lastName: "Williams"
email: "Serena@Williams.ca"
password: "NotGhengis"
}) {
user {
nodeId
id
firstName
lastName
createdAt
}
}
}
- Keeping Genghis' JWT, try modifying your friend
- Note this is Serena's
nodeId
- Note this is Serena's
mutation {
updateUser(input: {
nodeId: "WyJ1c2VycyIsM10="
userPatch: {
lastName: "KhanMaybe?"
}
}) {
user {
nodeId
id
firstName
lastName
createdAt
}
}
}
- 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
}
}
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.
mutation {
activateUser(input: {
email: "realemail@gmail.com",
activationCode: "00000000-0000-0000-0000-000000000000"
}) {
boolean
}
}
Observe the response:
{
"data": {
"activateUser": {
"boolean": false
}
}
}
mutation {
activateUser (input:{
email: "realemail@gmail.com",
activationCode: "e0df9b6b-ef0f-417c-823a-6e871f5c7d43"
}) {
boolean
}
}
And observe the successful activation!
{
"data": {
"activateUser": {
"boolean": true
}
}
}
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.
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.