Skip to content
Authentication

Authentication

The auth package adds JWT-based authentication on top of a User model. It ships with all the routes and handlers for user creation, login, password resets, email verification, and token refresh.

Setting it up

Register[T] registers the DI providers that resolve the current *Claims and the logged-in T user from the request context. It is a bootstrap step in the generated kernel:

kernel.Register(func(ctx context.Context, c *config.Config) {
	auth.Register[*models.User](ctx)
})

The HTTP middleware is applied in routes.go:

r.Use(auth.AttachUser())

AttachUser parses the Authorization header and stores the claims in the request context. It is permissive — requests without a header pass through with no claims.

The User model

A User is a model with an ID, a password hash, and one or more username columns, plus methods for salting passwords:

type User interface {
	model.Model
	GetID() string
	GetPasswordHash() []byte
	SetPasswordHash([]byte)
	SaltedPassword(password string) []byte
	UsernameColumns() []string
}

Two ready-made users are provided: UsernameUser (a username column) and EmailVerifiedUser (an email column with a validated flag, a lookup_token for email verification, and a GetEmail interface). Both embed BaseModel, use a uuid.UUID primary key, and salt the password hash with the user ID before hashing with bcrypt.

Auth routes

RegisterRoutes adds the auth endpoints to a router group:

auth.RegisterRoutes(r, auth.NewBasicAuthController[*models.User](
	auth.CreateUser(func(r *auth.EmailVerifiedUserCreateRequest, c *auth.BasicAuthController[*models.User]) (*auth.UserCreateResponse[*models.User], error) {
		return c.RunUserCreate(&models.User{
			EmailVerifiedUser: auth.EmailVerifiedUser{
				ID:           uuid.New(),
				Email:        r.Email,
				PasswordHash: []byte{},
			},
		}, &r.UserCreateRequest)
	}),
	auth.ResetPasswordName("reset-password"),
))

This registers:

routehandlerpurpose
POST /loginLoginverify credentials, return access and refresh tokens
POST /userUserCreatecreate a user and optionally send a verification email
GET /user/verifyVerifyEmailmark the user verified from a token
POST /user/password/forgotForgotPasswordemail a password reset token
POST /user/password/resetResetPasswordset a new password with the token
POST /login/refreshRefreshmint a new access token from a refresh token
POST /user/password/changeChangePasswordchange the password for the logged-in user (authed)

The last route is protected: RegisterRoutes applies AttachUser and LoggedIn to it.

Middleware

LoggedIn() rejects requests without claims with a 401 and can be scoped to a route. It also acts as an openapidoc.OperationMiddleware, tagging the operation as requiring the default security definition. HasClaim(cb) rejects requests whose claims fail a predicate:

r.Group("/admin", func(r *router.Router) {
	r.Use(auth.AttachUser())
	r.Use(auth.HasClaim(func(c *auth.Claims) bool {
		return slices.Contains(c.Scope, "admin")
	}))
	r.Get("/dashboard", handlers.Dashboard)
})

Injecting the user

Inside a handler, the current user is filled from the DI container. Register loads the user from the database by the subject of the claims:

type Request struct {
	User *models.User `inject:""`
}

request.Handler(func(r *Request) (*UserResponse, error) {
	return &UserResponse{User: r.User}, nil
})

If there is no logged-in user this injection returns Err401Unauthorized.

Claims and tokens

Claims embeds the JWT registered claims, carries a space-separated scope list, and is built fluently:

claims := auth.NewClaims().
	WithSubject(user.GetID()).
	WithLifetime(time.Hour).
	WithIssuer("example.com").
	WithScopes(auth.ScopeAccess)

GenerateToken(claims) signs a token with HS512 using the app key, and Parse/ParseOf[T] verify it. Set a stable key with SetAppKey so tokens survive restarts; without one a random key is generated and a warning logged.

ScopeAccess and ScopeRefresh are the two standard scopes. Access tokens are checked for ScopeAccess by AttachUser and refresh tokens must carry ScopeRefresh.