Skip to content

Instantly share code, notes, and snippets.

@parris
Last active January 15, 2018 23:55
Show Gist options
  • Save parris/3f0b23796b7d8490613c0cef6dfd8c48 to your computer and use it in GitHub Desktop.
Save parris/3f0b23796b7d8490613c0cef6dfd8c48 to your computer and use it in GitHub Desktop.
Fervor + PostgraphQL/Postgraphile + Knex Authentication Example
exports.up = (db, Promise) => (
Promise.all([
db.schema.raw('ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM public;'),
db.schema.raw("CREATE TYPE public.user_role AS ENUM ('hatch_anonymous', 'hatch_contributor', 'hatch_admin')"),
db.schema.raw('CREATE ROLE hatch_anonymous;'),
db.schema.raw('CREATE ROLE hatch_contributor;'),
db.schema.raw('CREATE ROLE hatch_admin;'),
db.schema.raw('GRANT USAGE ON SCHEMA public to hatch_anonymous, hatch_contributor, hatch_admin;'),
db.schema.raw('CREATE EXTENSION IF NOT EXISTS "pgcrypto";'),
db.schema.createTable('public.person', (table) => {
table.increments();
table.string('email').notNull().defaultTo('').unique();
table.string('password_hash').notNull().defaultTo('');
table.string('password_salt').notNull().defaultTo('');
table.specificType('role', 'user_role').notNull().defaultTo('hatch_contributor');
table.boolean('verified').notNull().defaultTo(false);
table.string('display_name').nullable();
table.timestamps(true, true);
}),
db.schema.raw(`
CREATE TYPE public.jwt_token AS (
role text,
person_id integer
);
`),
]).then((() => (Promise.all([
db.schema.raw(`
CREATE FUNCTION public.register_person(
display_name TEXT,
email TEXT,
password TEXT
) RETURNS public.person AS $$
DECLARE
p public.person;
salt TEXT;
passhash TEXT;
BEGIN
salt := gen_salt('bf');
passhash := crypt(password, salt);
INSERT INTO public.person (display_name, email, password_hash, password_salt) VALUES
(display_name, email, passhash, salt)
RETURNING * INTO p;
RETURN p;
END;
$$ LANGUAGE plpgsql STRICT SECURITY DEFINER;
comment on function register_person(TEXT, TEXT, TEXT) is 'Registers a single user.';
`),
db.schema.raw(`
CREATE FUNCTION public.authenticate(
email TEXT,
password TEXT
) RETURNS public.jwt_token AS $$
DECLARE
account public.person;
BEGIN
SELECT p.*
INTO account
FROM public.person AS p
WHERE p.email = $1;
IF account.password_hash = crypt(password, account.password_salt)
THEN
RETURN (account.role, account.id) :: public.jwt_token;
ELSE
RETURN NULL;
END IF;
END;
$$ LANGUAGE plpgsql STRICT SECURITY DEFINER;
comment on function public.authenticate(TEXT, TEXT) is 'Authenticates a user and returns a JWT';
`),
db.schema.raw(`
CREATE FUNCTION public.current_person() RETURNS public.person AS $$
SELECT * FROM public.person
WHERE id = current_setting('jwt.claims.person_id')::integer
$$ language sql stable;
comment on function public.current_person() is 'Gets the person who was identified by our JWT.';
`),
db.schema.raw('GRANT SELECT ON TABLE public.person TO hatch_anonymous, hatch_contributor, hatch_admin;'),
db.schema.raw('GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE public.person TO hatch_admin;'),
db.schema.raw('GRANT USAGE ON SEQUENCE public.person_id_seq TO hatch_anonymous, hatch_contributor, hatch_admin'),
db.schema.raw('GRANT EXECUTE ON FUNCTION public.authenticate(TEXT, TEXT) TO hatch_anonymous, hatch_contributor, hatch_admin'),
db.schema.raw('GRANT EXECUTE ON FUNCTION public.current_person() TO hatch_contributor, hatch_admin'),
db.schema.raw('GRANT EXECUTE ON FUNCTION public.register_person(TEXT, TEXT, TEXT) TO hatch_anonymous, hatch_contributor, hatch_admin'),
db.schema.raw('CREATE POLICY select_person ON public.person FOR SELECT TO hatch_anonymous, hatch_contributor, hatch_admin USING (true);'),
db.schema.raw('ALTER TABLE public.person ENABLE ROW LEVEL SECURITY;'),
db.schema.raw('CREATE POLICY insert_person ON public.person FOR INSERT WITH CHECK (true);'),
db.schema.raw("CREATE POLICY update_person ON public.person FOR UPDATE USING (id = current_setting('jwt.claims.person_id')::integer) WITH CHECK (id = current_setting('jwt.claims.person_id')::integer);"),
db.schema.raw('CREATE POLICY delete_person ON public.person FOR DELETE TO hatch_admin;'),
]))))
);
exports.down = (db, Promise) => (
Promise.all([
db.schema.raw('DROP FUNCTION IF EXISTS public.register_person(TEXT, TEXT, TEXT)'),
db.schema.raw('DROP FUNCTION IF EXISTS public.authenticate(TEXT, TEXT)'),
db.schema.raw('DROP FUNCTION IF EXISTS public.current_person()'),
db.schema.dropTableIfExists('public.person'),
db.schema.raw('DROP TYPE IF EXISTS public.jwt_token'),
db.schema.raw('DROP TYPE IF EXISTS public.user_role'),
]).then(() => Promise.all([
db.schema.raw('REVOKE ALL PRIVILEGES ON SCHEMA public FROM hatch_anonymous;'),
db.schema.raw('REVOKE ALL PRIVILEGES ON SCHEMA public FROM hatch_contributor;'),
db.schema.raw('REVOKE ALL PRIVILEGES ON SCHEMA public FROM hatch_admin;'),
])).then(() => Promise.all([
db.schema.raw('DROP ROLE hatch_anonymous;'),
db.schema.raw('DROP ROLE hatch_contributor;'),
db.schema.raw('DROP ROLE hatch_admin;'),
]))
);
// src/graph/index.js
function removePrivatePersonFields(builder) {
builder.hook('GraphQLObjectType:fields', (fields, _, { GraphQLObjectType }) => {
if (GraphQLObjectType.name === 'Person') {
delete fields.email;
delete fields.passwordHash;
delete fields.passwordSalt;
delete fields.verified;
}
return fields;
});
builder.hook('GraphQLInputObjectType:fields', (fields, _, { GraphQLInputObjectType }) => {
if (GraphQLInputObjectType.name === 'PersonPatch') {
delete fields.email;
delete fields.passwordHash;
delete fields.passwordSalt;
delete fields.verified;
}
return fields;
});
}
function removePrivateQueries(builder) {
builder.hook('GraphQLObjectType:fields', (fields, _, { scope: { isRootQuery } }) => {
if (!isRootQuery) { return fields; }
delete fields.allKnexMigrations;
delete fields.allKnexMigrationsLocks;
delete fields.knexMigrationById;
delete fields.knexMigration;
delete fields.allPeople;
delete fields.crypt;
delete fields.dearmor;
delete fields.decrypt;
delete fields.decryptIv;
delete fields.encrypt;
delete fields.encryptIv;
delete fields.pgpKeyId;
return fields;
});
}
function removePrivateMutations(builder) {
builder.hook('GraphQLObjectType:fields', (spec) => {
delete spec.createKnexMigration;
delete spec.createKnexMigrationsLock;
delete spec.updateKnexMigration;
delete spec.updateKnexMigrationById;
delete spec.deleteKnexMigration;
delete spec.deleteKnexMigrationById;
delete spec.createPerson;
delete spec.deletePerson;
delete spec.deletePersonById;
delete spec.genRandomBytes;
delete spec.genRandomUuid;
return spec;
});
}
export default () => ({
graphiql: true,
graphiqlRoute: '/admin/graphiql',
jwtSecret: process.env.JWT_SECRET,
jwtPgTypeIdentifier: 'public.jwt_token',
appendPlugins: [
removePrivatePersonFields,
removePrivateQueries,
removePrivateMutations,
],
});
// src/apps/Login.js
import { React, Form, clientCookies, gql } from 'fervor/lib';
import Template from '../components/Template';
import styles from './styles/about.scss';
const authenticate = gql`
mutation Authenticate($login: AuthenticateInput!) {
authenticate(input: $login) {
jwtToken
}
}`;
const onSuccess = (res) => {
clientCookies.set('authJWT', res.data.authenticate.jwtToken);
};
export default () => (
<Template title="Login">
<article className={styles.article}>
<Form mutation={authenticate} onSuccess={onSuccess}>
<input name="login[email]" placeholder="Email" />
<input name="login[password]" type="password" placeholder="password" />
<button type="submit">Submit</button>
</Form>
</article>
</Template>
);
// src/apps/Register.js
import { React, Form, gql } from 'fervor/lib';
import Template from '../components/Template';
import styles from './styles/about.scss';
const authenticate = gql`
mutation Register($register: RegisterPersonInput!) {
registerPerson(input: $register) {
person {
id
role
}
}
}`;
export default () => (
<Template title="hatch">
<article className={styles.article}>
<Form mutation={authenticate} redirectTo="/login">
<input name="register[givenName]" placeholder="First Name" />
<input name="register[familyName]" placeholder="Last Name" />
<input name="register[email]" placeholder="Email" />
<input name="register[password]" type="password" placeholder="password" />
<button type="submit">Submit</button>
</Form>
</article>
</Template>
);
@gcoda
Copy link

gcoda commented Jan 15, 2018

knex migrate:latest says it should be:
(db, Promise) => ( return Promise.all([

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment