Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 15 additions & 150 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,174 +1,39 @@
# auth-go

A production-minded authentication starter for Go teams who want secure defaults without a giant framework.

Build a REST API with Gin, PostgreSQL, OAuth, email verification, password reset, and a security-focused token lifecycle—ready to extend from a clean hexagonal architecture.
A production-minded Go authentication starter with Gin, PostgreSQL, OAuth, email verification, password reset, and a security-focused token lifecycle. Its hexagonal structure is designed to be extended without a large framework.

## Highlights

- **Argon2id passwords** — memory-hard password hashing with explicit parameters.
- **JWT access tokens** — short-lived tokens for authenticated API requests.
- **Opaque refresh tokens** — cryptographically random values with only HMAC-SHA-256 digests stored in PostgreSQL.
- **Refresh rotation and replay detection** — one-time refresh tokens with family-wide revocation.
- **Secure cookies** — HttpOnly, SameSite=Lax, and Secure in production.
- **OAuth login** — GitHub and Google providers.
- **Email workflows** — verification and password-reset flows.
- **Integration-ready CI** — PostgreSQL-backed tests run separately from the unit suite.
- **Bounded server lifecycle** — explicit HTTP timeouts and graceful SIGINT/SIGTERM shutdown.
- Argon2id passwords and short-lived JWT access tokens.
- Opaque, rotating refresh tokens with replay detection and family-wide revocation.
- GitHub/Google OAuth, email verification, and password recovery.
- Configurable authentication rate limiting, audit logs, and optional Prometheus metrics.

## Quick start

### Prerequisites

- Go 1.25+
- Docker and Docker Compose
- PostgreSQL, or the included Compose database
- `migrate` CLI for integration tests and migrations

### Run locally
Requires Go 1.25+, Docker Compose, and the `migrate` CLI.

```bash
git clone https://github.com/Jonathan0823/auth-go.git
cd auth-go
cp .env.example .env

# Set the secrets and email credentials in .env before running.
go mod download
docker compose up -d db redis
make migrate-up
make run
```

The API starts on `http://localhost:8080` by default.

With `ENABLE_SWAGGER=true` outside production, interactive API documentation is available at [`http://localhost:8080/swagger/index.html`](http://localhost:8080/swagger/index.html). The generated Swagger 2.0 document is served at `/swagger/doc.json`.

## Configuration

The application uses one PostgreSQL connection string everywhere: `DATABASE_URL`.

Start from `.env.example` and set at least:

```dotenv
DATABASE_URL=postgres://postgres:postgres@localhost:5432/auth_go?sslmode=disable
JWT_ACCESS_SECRET=replace-with-a-long-random-secret
REFRESH_TOKEN_HASH_KEY=replace-with-a-separate-long-random-secret
ENABLE_SWAGGER=true
ENABLE_METRICS=false
SESSION_SECRET=replace-with-a-long-random-secret
```

Email credentials are required because registration and recovery send mail. OAuth providers are optional, but each configured client ID must have its matching secret:

```dotenv
EMAIL=your-email@gmail.com
PASSWORD=your-gmail-app-password
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
```

Configuration is validated before the database or HTTP server starts. In production, JWT, refresh-token, session, and rate-limit secrets must each be at least 32 bytes.

Authentication rate limiting supports `memory`, `redis`, and `postgres` backends. Memory is intended for development or an explicitly approved single-instance deployment; production should use Redis or PostgreSQL. Set `RATE_LIMIT_KEY` to a separate secret and configure `TRUSTED_PROXIES` only for known proxy networks.

`make` loads `.env` automatically. Use another file explicitly when needed:

```bash
ENV_FILE=.env.prod make run
```

## Database and tests

```bash
make migrate-up # Apply migrations
make test # Unit tests
make integration # Migrate + PostgreSQL/Redis integration tests
make coverage # Integration tests + coverage/coverage.out
make vet # Static checks
make sqlc # Regenerate PostgreSQL code
make swagger # Regenerate Swagger artifacts
make swagger-validate # Validate the Swagger document
```

Integration tests use the `integration` build tag and require PostgreSQL. Redis-backed tests run when `REDIS_ADDR` is configured:
The API listens at `http://localhost:8080` by default. Swagger UI is available at `/swagger/index.html` when `ENABLE_SWAGGER=true` outside production.

```bash
go test -tags=integration ./...
```

## Observability

- `GET /health/live` reports process liveness without checking PostgreSQL.
- `GET /health/ready` reports database readiness with a bounded PostgreSQL ping.
- `GET /metrics` exposes Prometheus metrics only when `ENABLE_METRICS=true`; it is an operational endpoint and is intentionally excluded from Swagger.
- Security audit events are emitted as structured JSON logs with request IDs and safe categorical context.
- Rate-limit denials and backend failures use low-cardinality audit and Prometheus events without raw identifiers.

Metrics labels use route templates and avoid user-controlled values. Restrict `/metrics` to trusted monitoring systems in production. Grafana, Loki, and tracing infrastructure are intentionally not bundled.

## Authentication model

1. A successful login returns a short-lived JWT access token and an opaque refresh token in secure cookies.
2. Refresh tokens are random bearer values; no user data or JWT claims are embedded in them.
3. Only an HMAC digest of a refresh token is persisted.
4. Every refresh invalidates the previous token and issues a replacement.
5. Reusing a rotated token revokes the entire refresh-token family.
6. Access tokens remain stateless and expire after 15 minutes.

> **Migration note:** The security migration invalidates existing JWT refresh sessions. Legacy bcrypt password hashes are not accepted; affected users must reset their password.

## API documentation

Swagger documentation is generated from Go annotations and committed under `docs/`. To regenerate it after changing a handler contract:

```bash
make swagger
make swagger-validate
```

Swagger UI is opt-in and development-only. Set `ENABLE_SWAGGER=true` locally; it remains disabled whenever `ENVIRONMENT=production`.

## API overview

### Authentication

- `POST /api/auth/register`
- `POST /api/auth/login`
- `POST /api/auth/refresh`
- `POST /api/auth/logout`
- `POST /api/auth/forgot-password`
- `POST /api/auth/reset-password`
- `GET /api/auth/verify/email`
- `POST /api/auth/verify/email/resend`

### OAuth
## Documentation

- `GET /api/oauth/:provider/`
- `GET /api/oauth/:provider/callback`

Supported providers: GitHub and Google.

### Users

- `GET /api/user/me`
- `GET /api/user/:id`
- `GET /api/user/get-all`
- `GET /api/user/email`
- `PATCH /api/user/update`
- `DELETE /api/user/delete/:id`

## Project structure

```text
cmd/ application entrypoint
docs/ generated Swagger specification and registration
internal/
core/ domain models, ports, and services
adapter/ HTTP, PostgreSQL, JWT, password, OAuth, and email adapters
platform/ configuration, database, logging, and server setup
migrations/ PostgreSQL schema migrations
```
- [Setup and operations](docs/setup.md) — configuration, migrations, testing, and observability.
- [Authentication and API](docs/authentication.md) — tokens, cookies, and routes.
- [Rate limiting](docs/rate-limiting.md) — policies, backends, and failure handling.
- [Storage and caching](docs/storage-and-caching.md) — PostgreSQL, Redis, and what is not cached.
- [Architecture](docs/architecture.md) — package boundaries and extension points.
- [Swagger specification](docs/swagger.yaml) — generated HTTP API contract.

## License

Expand Down
4 changes: 2 additions & 2 deletions cmd/auth-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ import (
"github.com/joho/godotenv"

"github.com/Jonathan0823/auth-go/internal/bootstrap"
"github.com/Jonathan0823/auth-go/internal/platform"
"github.com/Jonathan0823/auth-go/internal/config"
)

func main() {
_ = godotenv.Load()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := bootstrap.Run(ctx, platform.LoadConfig()); err != nil {
if err := bootstrap.Run(ctx, config.LoadConfig()); err != nil {
log.Fatal(err)
}
}
25 changes: 25 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Architecture

```text
cmd/ application entrypoint
internal/
bootstrap/ dependency construction and resource lifecycle
config/ environment loading and validation
core/
domain/ business models and errors
port/ application-owned contracts and inputs
ratelimit/ transport-independent rate-limit coordination
service/ authentication and user use cases
adapter/
inbound/http/ Gin handlers, routing, DTOs, and HTTP middleware
outbound/ PostgreSQL, rate-limit stores, JWT, OAuth, password, and email
observability/ logging, metrics, and audit implementations
docs/ generated Swagger files and hand-written guides
migrations/ PostgreSQL schema migrations
```

Dependencies point inward: core code does not import HTTP, configuration, observability, or infrastructure adapters. `bootstrap` is the composition root that wires application-owned ports to concrete adapters and closes resources. The application decides *when* to apply account/email/token policies; HTTP owns client IP extraction and HTTP responses; stores implement persistence. See [Rate limiting](rate-limiting.md).

JWT encoding and token hashing live in the outbound JWT adapter, not in the core service. HTTP request/response DTOs are mapped explicitly rather than exposing domain objects directly. If a future workflow needs a queue, its publisher belongs in an outbound adapter and its worker in an inbound adapter; this template does not implement a queue or outbox.

Go source and test filenames under `cmd/` and `internal/` are lowercase and descriptive, with underscores between words. Test files append `_test.go`; tagged integration tests use `_integration_test.go`. Generator-controlled filenames remain unchanged. The test in `internal/architecture/` checks that core dependencies stay inward.
48 changes: 48 additions & 0 deletions docs/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Authentication and API

## Token lifecycle

1. Registration stores an Argon2id password hash and sends a verification email.
2. Successful password login sets a short-lived JWT access token and a random opaque refresh token in HttpOnly, SameSite=Lax cookies (`Secure` in production).
3. The access token is signed, expires after 15 minutes, and is verified without a database lookup. Refresh tokens contain no JWT claims; only their HMAC-SHA-256 digests are stored in PostgreSQL.
4. Refresh rotates the one-time token. Reusing an already-used token revokes its entire token family. Logout revokes the family and clears the cookies.
5. Password recovery and verification tokens are stored in PostgreSQL; [rate limits](rate-limiting.md) protect these workflows.

```mermaid
sequenceDiagram
participant Client
participant HTTP as HTTP adapter
participant Auth as Auth service
participant Tokens as JWT/token adapter
participant DB as PostgreSQL
Client->>HTTP: Login with credentials
HTTP->>Auth: Login
Auth->>DB: Load user and password hash
Auth->>Tokens: Sign access JWT and generate opaque refresh token plus HMAC
Auth->>DB: Store refresh-token HMAC and family ID
Auth-->>HTTP: Access and refresh tokens
HTTP-->>Client: Set HttpOnly cookies
Client->>HTTP: Refresh with refresh cookie
HTTP->>Auth: RefreshTokens
Auth->>Tokens: HMAC the presented token
Auth->>Tokens: Sign new JWT and generate replacement refresh token plus HMAC
Auth->>DB: Consume old token and create replacement in a transaction
Auth-->>HTTP: New access and refresh tokens
HTTP-->>Client: Replace cookies
Client->>HTTP: Reuse old refresh token
HTTP->>Auth: RefreshTokens
Auth->>DB: Revoke token family in a transaction
HTTP-->>Client: 401 unauthorized
```

Existing JWT refresh sessions are invalidated by the security migration. Legacy bcrypt password hashes are not accepted; affected users must reset their password.

## Routes

| Area | Endpoints |
| --- | --- |
| Authentication | `POST /api/auth/register`, `/login`, `/refresh`, `/logout`, `/forgot-password`, `/reset-password`; `GET /api/auth/verify/email`; `POST /api/auth/verify/email/resend` |
| OAuth | `GET /api/oauth/:provider/`, `/api/oauth/:provider/callback` (`github` and `google`) |
| Users | `GET /api/user/me`, `/api/user/:id`, `/api/user/get-all`, `/api/user/email`; `PATCH /api/user/update`; `DELETE /api/user/delete/:id` |

See the generated [Swagger spec](swagger.yaml) or the development-only Swagger UI for request and response details. Handler routing lives in `internal/adapter/inbound/http/routes.go`.
Loading
Loading