# Easy Email Tokens in Pure Postgres 1. [Generate a secret.](#1) 2. [Add the token function.](#2) 3. [Create/send tokens.](#3) 4. [Verify tokens.](#4) ## 1. Generate a secret. {1} Generate a random string using Bash... ```bash openssl rand -base64 32 ``` ...or Postgres: ```sql select string_agg(substr(c, (random() * length(c) + 1)::integer, 1), '') from (values('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')) as x(c) , generate_series(1, 32) ``` ## 2. Add the token function. {2} ```sql create function email_token (ts timestamptz, email text) returns text language sql immutable returns null on null input return '' || extract(epoch from ts)::bigint || ':' || encode( sha256(( email || 'SECRET' || extract(epoch from ts)::bigint )::bytea), 'base64' ) ; ``` [^note]: Make sure to replace `SECRET` with your secret from step 1! With the code above each token will look something like this: ``` 1695683829:Kr/BYcOyBifA6RvSp+EoK0isN7XkKk/dGkFHr27JKG8= ``` Feel free to use `hex` instead of `base64`, or `sha512` instead of `sha256`. Be wary of `md5`. ## 3. Create/send tokens. {3} Generate email tokens when you register new users... ```sql with u as ( insert into usr (email, password) values ($1, crypt($2, gen_salt('bf', 8))) returning * ) select usr_id, email_token(now(), email) from u; ``` ...or use the function ad-hoc for any flow you need: ```sql select email_token(now(), 'hello@example.com'); ``` You'll need to send your user a link like this at some point: ```html verify email ``` ## 4. Verify tokens. {4} Use the following logic on an endpoint like `/verify-token`: ```ts const token = request.url.searchParams.get("token"); const [usr] = await sql<{ usr_id: string }[]>` update usr set email_verified_at = now() where email_verified_at is null and to_timestamp(split_part(${token},':',1)::bigint) > now() - interval '2 days' and ${email} = email and ${token} = email_token( to_timestamp(split_part(${token},':',1)::bigint), email ) returning usr_id `; ``` There are multiple ways to pass `${email}` into this query: - _Option #1:_ Store the email address in cookie/session. Unfortunately, they won't be able to confirm their email from a different device without logging in again. - _Option #2:_ Remove the `${email} = email` line and Postgres will check all unverified emails. This is easiest to implement, but insecure and inefficient. - _Option #3:_ Pass the email address through the verification link via an additional search param like `&email=EMAIL`.