goten

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 21, 2026 License: MIT Imports: 16 Imported by: 0

README

Goten

Go Language Otentikasi — composable authentication for Go, inspired by better-auth, Limen, and Go-better-auth.

CI Go Reference

Goten is a modular authentication library for Go with a multi-module plugin architecture — install only what you need, no unused code in your binary.

Status: 🚧 v0.1.0 — early release, API may change before v1.0

Features

✅ Email/password sign-up & sign-in ✅ Plugin system with capability interfaces
✅ Opaque session tokens (g10_ prefix) ✅ Username plugin
✅ Cookie + Bearer auth ✅ CLI migration tool
✅ GORM adapter (Postgres) ✅ CSRF origin check
✅ Anti-enumeration on sign-in 🔜 OAuth (Google, GitHub, …)
✅ Session list/revoke 🔜 Magic link, 2FA, JWT plugin

Quick Start

go get github.com/dnahilman/goten
go get github.com/dnahilman/goten/adapters/gorm
package main

import (
    "log"
    "net/http"

    goten "github.com/dnahilman/goten"
    gormadapter "github.com/dnahilman/goten/adapters/gorm"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func main() {
    db, _ := gorm.Open(postgres.Open("postgres://..."), &gorm.Config{})

    auth, err := goten.New(goten.Config{
        BaseURL: "http://localhost:8080",
        Secret:  "your-32-byte-secret-key-here!!!!",
        Adapter: gormadapter.New(db),
    })
    if err != nil {
        log.Fatal(err)
    }

    // Mount auth endpoints at /api/auth/*
    http.Handle("/api/auth/", auth.Handler())

    // Protect your own endpoints
    http.Handle("/api/me", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, _ := goten.UserFromContext(r.Context())
        // user.ID, user.Email, user.Name ...
    })))

    http.ListenAndServe(":8080", nil)
}

See examples/basic/ for a full runnable example with Docker Compose.

Endpoints

Method Path Description
POST /api/auth/sign-up/email Register with email + password
POST /api/auth/sign-in/email Login with email + password
POST /api/auth/sign-out Revoke current session
GET /api/auth/get-session Get current session + user
GET /api/auth/list-sessions List all sessions for user
POST /api/auth/revoke-session Revoke a specific session
POST /api/auth/revoke-other-sessions Revoke all other sessions

CLI

go install github.com/dnahilman/goten/cmd/goten@latest

# Apply migrations (core + plugins)
goten migrate up

# Status
goten migrate status

# Roll back last migration
goten migrate down

# Generate new migration file
goten migrate generate add_phone_number

Config file goten.config.yaml:

database:
  url: postgres://user:pass@localhost:5432/mydb?sslmode=disable

migrations:
  core_dir: ./migrations
  plugins:
    - ./plugins/username/migrations
  table: goten_migrations

Plugins

Username Plugin

Login via username instead of (or alongside) email:

go get github.com/dnahilman/goten/plugins/username
import usernameplugin "github.com/dnahilman/goten/plugins/username"

auth, _ := goten.New(goten.Config{
    // ...
    Plugins: []goten.Plugin{
        usernameplugin.New(usernameplugin.Options{}),
    },
})

Adds endpoints: POST /api/auth/sign-up/username, POST /api/auth/sign-in/username.

Building Your Own Plugin
type MyPlugin struct{ auth *goten.Auth }

func (p *MyPlugin) ID() string          { return "my-plugin" }
func (p *MyPlugin) SetAuth(a *goten.Auth) { p.auth = a }
func (p *MyPlugin) Endpoints() []goten.Endpoint {
    return []goten.Endpoint{
        {Method: "GET", Path: "/my-endpoint", Handler: p.handle},
    }
}

Optional interfaces: Initializer, EndpointProvider, SchemaProvider, MigrationProvider, UserCreateHookProvider, SessionCreateHookProvider.

Architecture

github.com/dnahilman/goten              ← core (Auth, session, crypto, plugin system)
github.com/dnahilman/goten/adapters/gorm  ← GORM adapter (separate module)
github.com/dnahilman/goten/plugins/username ← username plugin (separate module)
github.com/dnahilman/goten/cmd/goten    ← CLI tool (separate module)

Each module is independently versioned — go get only what you use.

ID & Token Format

All IDs and tokens carry a g10_ prefix for easy identification in logs and secret scanning:

  • User/Session ID: g10_018f4a23-1234-7890-abcd-ef1234567890 (UUID v7, time-sortable)
  • Session token: g10_<base64url-32-bytes> (256-bit entropy)

CSRF Protection

Goten applies CSRF origin checking to all non-safe methods (POST, PUT, DELETE, etc.):

  • Bearer token present → bypass (mobile/API clients)
  • TrustedOrigins empty → allow requests without Origin (dev-friendly)
  • TrustedOrigins set → require Origin to match; reject others with 403
auth, _ := goten.New(goten.Config{
    BaseURL:        "https://myapp.com",
    TrustedOrigins: []string{"https://myapp.com", "https://www.myapp.com"},
    // ...
})

Comparison

Goten better-auth (TS) Limen Go-better-auth
Multi-module plugins ✅ (subpath)
CLI migration tool
Map-based adapter
OAuth providers 🔜
Language Go TypeScript Go Go

Contributing

See CONTRIBUTING.md. Security issues: SECURITY.md.

License

MIT — see LICENSE.

Documentation

Overview

Package goten provides composable, self-hosted authentication for Go.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidToken    = &APIError{Code: "INVALID_TOKEN", Message: "invalid session token", Status: 401}
	ErrInvalidEmail    = &APIError{Code: "INVALID_EMAIL", Message: "invalid email", Status: 400}
	ErrInvalidPassword = &APIError{Code: "INVALID_CREDENTIALS", Message: "invalid email or password", Status: 400}
	ErrEmailExists     = &APIError{Code: "EMAIL_EXISTS", Message: "email already exists", Status: 409}
	ErrUserNotFound    = &APIError{Code: "USER_NOT_FOUND", Message: "user not found", Status: 404}
	ErrSessionExpired  = &APIError{Code: "SESSION_EXPIRED", Message: "session expired", Status: 401}
	ErrSessionNotFound = &APIError{Code: "SESSION_NOT_FOUND", Message: "session not found", Status: 401}
	ErrUnauthorized    = &APIError{Code: "UNAUTHORIZED", Message: "unauthorized", Status: 401}
	ErrInternal        = &APIError{Code: "INTERNAL", Message: "internal server error", Status: 500}
)
View Source
var EQ = adp.EQ

EQ constructs an equality Where clause.

View Source
var ErrHookHandled = errors.New("hook handled the response")

ErrHookHandled signals that a hook already wrote the HTTP response. Callers must not write again; just return.

Functions

func DecodeJSON

func DecodeJSON(r *http.Request, dst any) error

func GetClientIP

func GetClientIP(r *http.Request, ipHeader string) string

func SessionFromContext

func SessionFromContext(ctx context.Context) (*models.Session, bool)

func UserFromContext

func UserFromContext(ctx context.Context) (*models.User, bool)

func WithSession

func WithSession(ctx context.Context, sess *models.Session, user *models.User) context.Context

func WriteError

func WriteError(w http.ResponseWriter, status int, code, message string)

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v any)

Types

type APIError

type APIError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Status  int    `json:"status"`
}

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) WriteJSON

func (e *APIError) WriteJSON(w http.ResponseWriter)

type Account

type Account = models.Account

type Adapter

type Adapter = adp.Adapter

Type aliases so callers can use goten.Adapter, goten.Query, etc.

type Auth

type Auth struct {
	// contains filtered or unexported fields
}

Auth is the composition root. Create with New() and mount Handler() on your server.

func New

func New(cfg Config) (*Auth, error)

New creates an Auth instance. Returns error for invalid config — never panics.

func (*Auth) Adapter

func (a *Auth) Adapter() Adapter

func (*Auth) Config

func (a *Auth) Config() Config

func (*Auth) Handler

func (a *Auth) Handler() http.Handler

func (*Auth) InternalAdapter

func (a *Auth) InternalAdapter() *InternalAdapter

func (*Auth) Plugins

func (a *Auth) Plugins() []Plugin

func (*Auth) RequireAuth

func (a *Auth) RequireAuth(next http.Handler) http.Handler

RequireAuth middleware validates the session and injects user+session into context. Use this to protect your own application endpoints.

func (*Auth) RunSessionCreateHooks

func (a *Auth) RunSessionCreateHooks(w http.ResponseWriter, r *http.Request, userID string) error

RunSessionCreateHooks runs all session-create veto hooks. Returns ErrHookHandled if a hook already wrote the response (caller must not write again).

func (*Auth) RunUserCreateHooks

func (a *Auth) RunUserCreateHooks(data map[string]any) map[string]any

RunUserCreateHooks applies all user-create hooks to data in registration order.

func (*Auth) Sessions

func (a *Auth) Sessions() *session.Manager

func (*Auth) SetSessionCookie

func (a *Auth) SetSessionCookie(w http.ResponseWriter, sess *models.Session)

SetSessionCookie is a plugin-friendly helper that sets the session cookie using Auth's configured cookie settings. Plugins call this instead of accessing the internal cookieConfig directly.

type AuthAware

type AuthAware interface {
	SetAuth(a *Auth)
}

AuthAware — plugin receives a reference to *Auth after New() sets up core state.

type Config

type Config struct {
	AppName        string
	BaseURL        string
	BasePath       string
	Secret         string
	Adapter        Adapter
	Plugins        []Plugin
	Session        SessionConfig
	Cookie         CookieConfig
	EmailPassword  EmailPasswordConfig
	TrustedOrigins []string
}

type CookieConfig

type CookieConfig struct {
	Name     string
	Domain   string
	Path     string
	Secure   *bool
	HTTPOnly *bool
	SameSite http.SameSite
}

type EmailPasswordConfig

type EmailPasswordConfig struct {
	Enabled           bool
	AutoSignIn        bool
	MinPasswordLength int
	MaxPasswordLength int
}

type Endpoint

type Endpoint struct {
	Method  string // "GET", "POST", etc.
	Path    string // relative path prefixed with BasePath in the router
	Handler http.HandlerFunc
}

Endpoint describes a single HTTP endpoint a plugin registers.

type EndpointProvider

type EndpointProvider interface {
	Endpoints() []Endpoint
}

EndpointProvider — plugin registers additional HTTP endpoints.

type FieldDef

type FieldDef struct {
	Name     string
	Type     string // "text", "boolean", "integer", "timestamp"
	Required bool
	Unique   bool
	Ref      string // FK reference, e.g. "users.id"
}

FieldDef describes a single column.

type Initializer

type Initializer interface {
	Init() error
}

Initializer — plugin needs a one-time init step (validate config, connect to external service, etc.).

type InternalAdapter

type InternalAdapter struct {
	// contains filtered or unexported fields
}

InternalAdapter provides typed CRUD methods on top of the raw Adapter interface.

func NewInternalAdapter

func NewInternalAdapter(a adp.Adapter) *InternalAdapter

func (*InternalAdapter) Adapter

func (ia *InternalAdapter) Adapter() adp.Adapter

Adapter returns the raw underlying adapter for plugins that need direct access.

func (*InternalAdapter) CreateAccount

func (ia *InternalAdapter) CreateAccount(ctx context.Context, userID, accountID, providerID string, extra map[string]any) (*models.Account, error)

func (*InternalAdapter) CreateUser

func (ia *InternalAdapter) CreateUser(ctx context.Context, email, name string, emailVerified bool) (*models.User, error)

func (*InternalAdapter) CreateUserWithExtra

func (ia *InternalAdapter) CreateUserWithExtra(ctx context.Context, email, name string, emailVerified bool, extra map[string]any) (*models.User, error)

func (*InternalAdapter) DeleteUser

func (ia *InternalAdapter) DeleteUser(ctx context.Context, id string) error

func (*InternalAdapter) FindAccountByProviderAndID

func (ia *InternalAdapter) FindAccountByProviderAndID(ctx context.Context, providerID, accountID string) (*models.Account, error)

func (*InternalAdapter) FindAccountsByUserID

func (ia *InternalAdapter) FindAccountsByUserID(ctx context.Context, userID string) ([]*models.Account, error)

func (*InternalAdapter) FindUserByEmail

func (ia *InternalAdapter) FindUserByEmail(ctx context.Context, email string) (*models.User, error)

func (*InternalAdapter) FindUserByID

func (ia *InternalAdapter) FindUserByID(ctx context.Context, id string) (*models.User, error)

func (*InternalAdapter) UpdatePassword

func (ia *InternalAdapter) UpdatePassword(ctx context.Context, userID, hashedPassword string) error

func (*InternalAdapter) UpdateUser

func (ia *InternalAdapter) UpdateUser(ctx context.Context, id string, data map[string]any) (*models.User, error)

type MigrationProvider

type MigrationProvider interface {
	Migrations() fs.FS
}

MigrationProvider — plugin has embedded SQL migration files. The CLI (Issue 006) collects these alongside core migrations, ordered by timestamp.

type Plugin

type Plugin interface {
	ID() string
}

Plugin is the base interface — all plugins must implement at minimum ID().

type Query

type Query = adp.Query

type SchemaProvider

type SchemaProvider interface {
	Schema() map[string]TableSchema
}

SchemaProvider — plugin declares the DB columns it adds (for CLI introspection).

type Session

type Session = models.Session

type SessionConfig

type SessionConfig struct {
	ExpiresIn time.Duration
	UpdateAge time.Duration
}

type SessionCreateContext

type SessionCreateContext struct {
	UserID  string
	Request *http.Request
	Writer  http.ResponseWriter
}

SessionCreateContext carries request context for session-create hooks.

type SessionCreateHookFn

type SessionCreateHookFn func(ctx SessionCreateContext) error

SessionCreateHookFn is called before inserting a new session. Return a non-nil error to abort. Return ErrHookHandled if the hook already wrote the response.

type SessionCreateHookProvider

type SessionCreateHookProvider interface {
	SessionCreateHooks() []SessionCreateHookFn
}

SessionCreateHookProvider — plugin hooks into session creation.

type TableSchema

type TableSchema struct {
	Fields []FieldDef
}

TableSchema describes columns a plugin adds to a table.

type User

type User = models.User

Type aliases so plugin authors only need to import the root package.

type UserCreateHookFn

type UserCreateHookFn func(data map[string]any) map[string]any

UserCreateHookFn is called before inserting a new user. Receives and returns the data map so plugins can add or transform fields.

type UserCreateHookProvider

type UserCreateHookProvider interface {
	UserCreateHooks() []UserCreateHookFn
}

UserCreateHookProvider — plugin hooks into user creation.

type Where

type Where = adp.Where

Directories

Path Synopsis
Package adapter defines the database adapter interface used by goten core and plugins.
Package adapter defines the database adapter interface used by goten core and plugins.
adapters
gorm module
cmd
goten module
plugins
admin module
oauth module
username module
Package session provides session lifecycle management and cookie utilities for goten.
Package session provides session lifecycle management and cookie utilities for goten.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL