Skip to content
Add accounts with make:auth

Add accounts with make:auth

Give your app user accounts in one command: registration, login with “remember me” and throttling, sign-in with Google and GitHub, logout, email verification, password reset and API tokens, with their pages, emails, routes, migration and tests. The code is written into your app, where you change it as you like; the parts that must be right (password hashing, tokens, sessions, throttling, the OAuth flow) stay in the auth and social packages, so fixes reach you with go get -u.

Before you start

You have a project made with anetos new (it has sessions, the cache, the queue and the mailer that accounts use), and no User model yet.

Steps

1. Generate the accounts

From the project’s directory:

go tool anetos make:auth
created app/models/user.go
created app/handlers/auth.go
created app/mailers/auth.go
created views/auth.templ
created views/auth_mail.templ
created routes/auth.go
created auth.go
created auth_test.go
created database/migrations/2026_10_02_090000_create_users_table.go
updated .env: SOCIAL_* settings
updated .env.example: SOCIAL_* settings
updated main.go: setup calls setupAuth
wrote app/models/models_gen.go

make:auth writes nothing if one of these files exists, or if a name they declare (User, Accounts, Login, …) is taken in its package; if writing fails midway, it removes what it wrote. It runs go mod tidy, anetos gen (the User model’s typed columns) and templ generate (the pages), checks that the project builds, and adds a call to setupAuth to setup in main.go, after routes.Register:

// illustrative: main.go after make:auth
routes.Register(srv.Router(), sessions)
// Accounts (anetos make:auth): registration, login with a password,
// Google or GitHub, email verification, password reset and API tokens.
if _, err := setupAuth(app, srv.Router(), sessions); err != nil {
	return nil, err
}

If your setup no longer has that statement, make:auth says so and you add the call yourself.

2. Migrate and try it

go run . migrate     # the users, api_tokens and social_accounts tables
go tool anetos dev

Open /register. In development, emails go to the log (MAIL_DRIVER=log), with their links; with QUEUE_DRIVER=database, the app’s workers send them, so they appear a moment later.

3. Turn on sign-in with Google or GitHub

Each provider is on once its client ID and secret are set; until then its button doesn’t show and its routes answer 404. Create an OAuth app at the provider (the Google Cloud console: “OAuth client ID”, type “Web application”; GitHub: “New OAuth App”), register APP_URL/auth/google/callback (or /auth/github/callback) as its callback URL, and fill in the settings make:auth added to .env:

APP_URL=http://localhost:8080
SOCIAL_GOOGLE_CLIENT_ID=1234-abc.apps.googleusercontent.com
SOCIAL_GOOGLE_CLIENT_SECRET=…

The login and registration pages then show “Sign in with Google”. A first sign-in creates a user, with the address verified and no password (they can set one with “Forgot your password?”); an account whose address both the provider and your app have verified is linked to the existing user. Social login explains the rules and how to add another provider (Okta, Auth0, any OpenID Connect provider).

4. Run the tests

auth_test.go registers, verifies the address by following the emailed link, logs in and out, resets the password, uses an API token, and signs in with Google and GitHub through a stand-in provider (anetostest.FakeSocial):

go test ./...

Keep these tests as you change the code: they cover the flows that matter.

5. Make it yours

FileHolds
app/models/user.goUser, and models.Users: how auth finds users and stores their tokens. Add columns here and in a migration
app/handlers/auth.gohandlers.Accounts: each page and form. Validation messages, redirects and what happens after registration are here; SocialUser finds or creates the user of a Google or GitHub account
views/auth.templThe pages, inside your Layout
app/mailers/auth.go, views/auth_mail.templThe verification and reset emails
routes/auth.goThe routes and their names (login, register, dashboard, …), and the rate limits of the forgotten-password and verification forms
auth.gosetupAuth: the api_tokens and social_accounts migrations, auth.ForApp, social.ForApp with the providers, and the routes
database/migrations/…_create_users_table.goThe users table

The routes:

RouteFor
GET, POST /registerGuests: create an account, email the verification link, sign in (10 posts a minute per client IP address)
GET, POST /loginGuests: sign in (AUTH_THROTTLE limits failures)
GET, POST /forgot-passwordGuests: email a reset link (5 posts a minute per client IP address; 3 links an hour per address, the same answer after)
GET, POST /reset-passwordGuests: choose a new password with the link
GET /auth/{provider}/redirect, GET /auth/{provider}/callbackGuests: sign in with Google or GitHub (404 for a provider whose settings aren’t set)
GET /verify-emailAnyone with the link: verify the address (400 if the link is bad, expired, or for an address the user has since changed)
GET /dashboardSigned-in users: the account and its API tokens
POST /logoutSigned-in users
POST /email/verification-notificationSigned-in users: email the link again (3 a minute per client IP address, 6 an hour per account)
POST /tokens, POST /tokens/{id}/deleteSigned-in users: create (shown once) and revoke API tokens
GET /api/meAPI clients, with Authorization: Bearer <token>

Signed-in users who open a guest page go to AUTH_HOME_URL (default /; set AUTH_HOME_URL=/dashboard to send them to the dashboard, where logging in, registering and signing in with a provider lead); guests who open a member page go to AUTH_LOGIN_URL and come back after logging in. To protect your own pages, put them in a group with a.Middleware and a.Require, as routes/auth.go does: setupAuth returns the *auth.Auth.

To do more when someone signs up (a welcome email, a trial), change Register and SocialUser in app/handlers/auth.go, which create users. examples/saas, a make:auth app, queues a welcome job in the transaction that creates the user, so the job runs only if the user is committed:

// welcome queues the welcome email of a new user, once the transaction
// that creates them commits: a worker sends it (jobs.SendWelcome).
func welcome(ctx context.Context, u *models.User) error {
	return queue.Dispatch(ctx, jobs.SendWelcome{UserID: u.ID}, queue.AfterCommit())
}

(Copied from examples/saas/app/handlers/auth.go, region dispatch.)

// SendWelcome emails a new user the welcome email. Registration and the
// first sign-in with Google or GitHub dispatch it once the user is
// committed; a worker runs it, with the queue's retries.
type SendWelcome struct {
	UserID int64 `json:"user_id"`
}

// Handle sends the email.
func (j SendWelcome) Handle(ctx context.Context) error {
	u, err := db.Find[models.User](ctx, j.UserID)
	if errors.Is(err, db.ErrNotFound) {
		return queue.Permanent(err) // deleted since: retrying won't help
	}
	if err != nil {
		return err
	}
	url, err := mailer.URL(ctx, "/dashboard")
	if err != nil {
		return err
	}
	m := mailers.Welcome{Name: u.Name, Email: u.Email, URL: url}
	if u.TrialEndsAt != nil {
		m.TrialEnds = *u.TrialEndsAt
	}
	return mailer.Send(ctx, m)
}

(Copied from examples/saas/app/jobs/welcome.go, region job.)

setup registers the job type, queue.Register[jobs.SendWelcome](q, queue.Tries(5)), where anetos new left a comment for it; see Queues.

How it works

The generated code is the same kind of code as examples/auth, which the Authentication guide walks through, adapted to a anetos new project: templ pages in views, typed columns, the mailer for the links (mailer.Queue, so a slow mail server doesn’t slow the request), and route names. Links in emails are absolute, on APP_URL. Reset and verification tokens are signed, not stored: a reset link works for AUTH_RESET_TTL and once, a verification link for AUTH_VERIFY_TTL. Registration creates the user and renders the email in one transaction, so an email that can’t be rendered leaves no account behind; the queue sends it after the commit. Names are kept on one line (control characters dropped), and emails go to the address alone, the name only in their body.

The forgotten-password form answers the same whether or not the address has an account, but takes a little longer when it does (it renders and queues the email): someone measuring response times could tell. The registration form says when an address is taken, as most sites do.

Signing in with a provider follows the rules of Social login: accounts are linked to users by the provider’s account ID, not by email, and an address finds an existing user only when both the provider and your app have verified it, and the user has no other account of that provider linked.

make:auth doesn’t add authorization policies: see Authorization.

Common problems

ProblemCauseFix
…_create_users_table.go exists or app/models/user.go existsThe app has users already, or make:auth ran beforeRename yours, or add accounts by hand from Authentication
app/models already declares User (or another name)A name the generated files declare is taken in that packageRename yours
the project doesn't build after the files are writtenYour code and the generated code clash, or a tool failedFix it, then run the commands it prints
no app/models directoryNot an anetos new projectStart from examples/auth
main.go doesn't have the routes.Register callsetup was changedAdd the setupAuth call it prints
no such table: usersThe migrations haven’t rungo run . migrate
URL needs the app's public URL when registeringAPP_URL isn’t setSet it in .env
No email in the logQUEUE_DRIVER=database and the workers aren’t runningRun the app with go tool anetos dev or go run ., or use QUEUE_DRIVER=sync
No “Sign in with Google” buttonIts SOCIAL_GOOGLE_* settings are emptySet both in .env
redirect_uri_mismatch at GoogleThe callback URL registered there isn’t APP_URL/auth/google/callbackFix APP_URL or the URL at Google

Next steps

Coming from Laravel? anetos make:auth is Breeze: the controllers, views and routes are published into your app. Hashing, remember-me, throttling and tokens (Fortify and Sanctum’s parts) stay in the library, and so does the OAuth flow (Socialite’s part).