Backend Authentication
JWT dual-token auth, middleware, NIK encryption, and password reset.
Dual-token JWT
Secrets from env: JWT_ACCESS_SECRET, JWT_REFRESH_SECRET.
| Token | Algorithm | Claims | Expiry | Storage |
|---|---|---|---|---|
| Access | HS256 | {user_id, exp, iat} | 60 minutes | client localStorage |
| Refresh | HS256 | {user_id, exp, iat} | 60 days | users.refresh_token column |
| Super-admin | HS256 | {username, role:"superadmin", exp, iat} | 1 hour | client localStorage |
Refresh validates both the signed token and that it matches the value stored on the user row — so a revoked/rotated refresh token is rejected even if the signature is valid.
Middleware (middleware/auth.go)
Handler identity extraction
Every handler that needs the caller identity repeats:
claims := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)
userID := uint(claims["user_id"].(float64))Password hashing
utils/hash.go — bcrypt (GenerateFromPassword / CompareHashAndPassword).
NIK encryption (PII)
The 16-digit NIK (national ID) is AES-GCM encrypted before storage (utils/AESencrypt.go, key from AES_ENCRYPTION_KEY — 32 bytes):
- Encrypted at registration and admin-create.
- Decrypted on read in
GetProfile,GetUserByID,GetAllAdmins. - There are (commented-out) admin endpoints
EncryptAllNIK/EncryptNIKByIdto migrate legacy plaintext NIKs.
Password reset flow
Registration
POST /api/register accepts {username, email?, password, nik?, nama_ktp, nama_samaran, nomor_hp, role?, domicile?, preferences?, cf-turnstile-response?}:
- Cloudflare Turnstile token verified via siteverify before any registration logic (403 on failure) — see below
- NIK encrypted with AES-GCM
- Phone normalized to
62-prefix - Returns
{access_token, refresh_token}
POST /api/auth/check-username checks availability before submission.
Cloudflare Turnstile (registration captcha)
Registration is protected by Turnstile. The flow:
public/index.htmlloads the Turnstile script (challenges.cloudflare.com/turnstile/v0/api.js).Register.jsrenders the widget explicitly into#turnstile-registerwithREACT_APP_TURNSTILE_SITE_KEY(managed widget ID in a ref; removed on unmount so SPA navigation gets a fresh token).- The token travels in sessionStorage (
register_form_data.turnstileToken) and is sent as thecf-turnstile-responseJSON field. - Backend
verifyTurnstile(internal/auth/turnstile.go) posts it to the siteverify endpoint withTURNSTILE_SECRET, then checks the response hostname against theTURNSTILE_HOSTNAMESallowlist.
Tokens are single-use: once siteverify redeems one, a retry with the same token always fails. On a 403 from registration, ChoosePreference.js strips the stale token from sessionStorage and sends the user back to /register (form data persists) to get a fresh one.
Fail closed
Verification fails closed: an empty TURNSTILE_SECRET or empty TURNSTILE_HOSTNAMES rejects every registration ("verifikasi captcha belum dikonfigurasi di server"). Both must be set in every deployment. Also enforces the widget action (register) and hostname allowlist. Frontend env: REACT_APP_TURNSTILE_SITE_KEY.
POST /api/auth/check-username checks availability before submission.
Required env vars
config.GetEnv(key) fatals on missing variables — the app refuses to start without all required secrets (JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, AES_ENCRYPTION_KEY, etc.).