Skip to main content

User Auth

Category: Security & Compliance

Get this pack →

This page is generated from the Air Pipe marketplace. Browse it live to install into your organization.

Overview

User signup and login with bcrypt password hashing and a signed JWT. Demonstrates bcrypt, bcrypt_verify, add_jwt.

Endpoints

  • POST auth/signup — register (bcrypt-hashed password)
  • POST auth/login — verify password, return JWT

Required variables

  • ap_var::DATABASE_URL — Postgres connection string
  • ap_var::JWT_SECRET — JWT signing secret

Configuration

seed.yml

name: UserAuthSeed
description: Creates the users table used by signup/login. Safe to re-run (starts empty; sign up to create users).

docs: true

# Required managed variables:
# DATABASE_URL — Postgres connection string

global:
databases:
main:
driver: postgres
conn_string: "a|ap_var::DATABASE_URL|"

interfaces:
api/seed:
output: http
method: POST
summary: Create and clear the users table
actions:
- name: CreateUsersTable
database: main
hide_data_on_success: true
query: |
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
- name: TruncateUsers
run_when_succeeded: [CreateUsersTable]
database: main
hide_data_on_success: true
query: TRUNCATE users;
- name: Respond
run_when_succeeded: [TruncateUsers]
json_output: |-
{ "status": "seeded", "users": 0 }

user_auth.yml

name: UserAuth
description: >
Register users with a bcrypt-hashed password and log them in with a signed
JWT. Demonstrates the bcrypt post-transform, the bcrypt_verify assertion and
the add_jwt post-transform.

docs: true

# Required managed variables:
# DATABASE_URL — Postgres connection string
# JWT_SECRET — secret used to sign and verify JWTs

global:
variables:
jwt_secret: a|ap_var::JWT_SECRET|
databases:
main:
driver: postgres
conn_string: "a|ap_var::DATABASE_URL|"
interfaces:
auth/signup:
output: http
method: POST
summary: Register a user, bcrypt-hashing the password before storing
actions:
- name: ReadBody
input: a|body|
assert:
error_message: "email and password are required"
http_code_on_error: 400
tests:
- value: email
is_not_empty: true
- value: password
is_not_empty: true
post_transforms:
- bcrypt:
value: password
cost: 12
- name: CheckExisting
run_when_succeeded: [ReadBody]
database: main
query: |-
SELECT count(*) AS n FROM users WHERE email = a|ReadBody::email->single_quote|
assert:
error_message: "email already registered"
http_code_on_error: 409
tests:
- value: "[0].n"
is_equal_to: 0
- name: InsertUser
run_when_succeeded: [CheckExisting]
database: main
query: |-
INSERT INTO users (id, email, password_hash)
VALUES (
a|uuid->single_quote|,
a|ReadBody::email->single_quote|,
a|ReadBody::password->single_quote|
)
RETURNING id, email
post_transforms:
- extract_value: "[0]"
- name: Respond
run_when_succeeded: [InsertUser]
json_output: |-
{ "id": "a|InsertUser::id|", "email": "a|InsertUser::email|" }
auth/login:
output: http
method: POST
summary: Verify a password and return a signed JWT
actions:
- name: ReadBody
input: a|body|
assert:
error_message: "email and password are required"
http_code_on_error: 400
tests:
- value: email
is_not_empty: true
- value: password
is_not_empty: true
- name: FetchUser
run_when_succeeded: [ReadBody]
database: main
query: |-
SELECT id, email, password_hash
FROM users
WHERE email = a|ReadBody::email->single_quote|
assert:
error_message: "invalid credentials"
http_code_on_error: 401
tests:
- value: "[0].id"
is_not_null: true
post_transforms:
- extract_value: "[0]"
- name: VerifyPassword
run_when_succeeded: [FetchUser]
input: a|body|
assert:
error_message: "invalid credentials"
http_code_on_error: 401
tests:
- value: password
verify_bcrypt: a|FetchUser::password_hash|
- name: IssueToken
run_when_succeeded: [VerifyPassword]
input: a|FetchUser|
post_transforms:
- add_jwt:
key: token
secret: a|var::jwt_secret|
exp: "24h"
data: [id, email]
- name: Respond
run_when_succeeded: [IssueToken]
json_output: |-
{ "token": "a|IssueToken::token|" }