Hallo Stroke

Backend Authentication

JWT dual-token auth, middleware, NIK encryption, and password reset.

Dual-token JWT

Secrets from env: JWT_ACCESS_SECRET, JWT_REFRESH_SECRET.

TokenAlgorithmClaimsExpiryStorage
AccessHS256{user_id, exp, iat}60 minutesclient localStorage
RefreshHS256{user_id, exp, iat}60 daysusers.refresh_token column
Super-adminHS256{username, role:"superadmin", exp, iat}1 hourclient 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)

Loading diagram...

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 / EncryptNIKById to migrate legacy plaintext NIKs.

Password reset flow

Loading diagram...

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:

  1. public/index.html loads the Turnstile script (challenges.cloudflare.com/turnstile/v0/api.js).
  2. Register.js renders the widget explicitly into #turnstile-register with REACT_APP_TURNSTILE_SITE_KEY (managed widget ID in a ref; removed on unmount so SPA navigation gets a fresh token).
  3. The token travels in sessionStorage (register_form_data.turnstileToken) and is sent as the cf-turnstile-response JSON field.
  4. Backend verifyTurnstile (internal/auth/turnstile.go) posts it to the siteverify endpoint with TURNSTILE_SECRET, then checks the response hostname against the TURNSTILE_HOSTNAMES allowlist.

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.).

On this page