Go Language Otentikasi — composable authentication for Go, inspired by better-auth, Limen, and Go-better-auth.
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.2.0 — early release, API may change before v1.0
| ✅ 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 plugin — Sign in with Google (PKCE + OIDC) |
| ✅ Session list/revoke | ✅ Admin plugin — roles, ban, impersonation (RBAC) |
| ✅ CSRF origin check | 🔜 Magic link, 2FA, JWT plugin |
go get github.com/dnahilman/goten
go get github.com/dnahilman/goten/adapters/gormFor a full walkthrough — database setup, migrations, runnable example, and end-to-end testing with curl — see the Quick Start guide on the Wiki.
Runnable examples in the repo:
examples/basic/— minimalnet/httpserver.examples/layered-gin/— Gin + GORM + layered architecture (handler → service → repository).examples/oauth-google/— Sign in with Google (OAuth plugin).
| 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 |
With the OAuth plugin enabled:
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/sign-in/social |
Start social sign-in → {redirect, url} (or sign in with an idToken) |
GET |
/api/auth/callback/{provider} |
OAuth redirect callback |
GET |
/api/auth/list-accounts |
List the user's linked accounts |
POST |
/api/auth/link-social |
Link a provider to the current user |
POST |
/api/auth/unlink-account |
Unlink a provider |
POST |
/api/auth/get-access-token |
Get a valid provider access token |
POST |
/api/auth/refresh-token |
Refresh the stored provider tokens |
Goten uses an HttpOnly cookie session (goten_session, SameSite=Lax). The
browser sends it automatically — your JS never reads the token. Two rules:
- Every request must send the cookie:
fetch(..., { credentials: "include" }). - The SPA origin must be allowed. Easiest is same origin (serve the SPA and
API from one host). For a separate dev origin (e.g. Vite on
:5173), either proxy/apito the backend, or setTrustedOriginson the server and enable CORS with credentials. Goten's CSRF check validates theOriginheader on unsafe methods againstTrustedOrigins.
// auth.ts — a tiny typed wrapper over goten's endpoints.
const BASE = import.meta.env.VITE_API_URL ?? ""; // "" = same origin
export interface User {
id: string;
email: string;
name: string;
emailVerified: boolean;
image?: string;
createdAt: string;
updatedAt: string;
}
export interface Session {
id: string;
userId: string;
expiresAt: string;
ipAddress?: string;
userAgent?: string;
createdAt: string;
updatedAt: string;
}
export interface SessionResponse {
user: User;
session: Session;
}
// goten errors: { code, message } with a non-2xx status.
export class AuthError extends Error {
constructor(public code: string, message: string, public status: number) {
super(message);
}
}
async function api<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}/api/auth${path}`, {
...init,
credentials: "include",
headers: { "Content-Type": "application/json", ...init?.headers },
});
const body = res.status === 204 ? null : await res.json().catch(() => null);
if (!res.ok) {
const e = (body ?? {}) as { code?: string; message?: string };
throw new AuthError(e.code ?? "ERROR", e.message ?? res.statusText, res.status);
}
return body as T;
}
export const auth = {
signUp: (data: { email: string; password: string; name: string }) =>
api<SessionResponse>("/sign-up/email", { method: "POST", body: JSON.stringify(data) }),
signIn: (data: { email: string; password: string }) =>
api<SessionResponse>("/sign-in/email", { method: "POST", body: JSON.stringify(data) }),
signOut: () => api<{ success: boolean }>("/sign-out", { method: "POST" }),
// Returns null when there is no active session (goten replies 401).
getSession: async (): Promise<SessionResponse | null> => {
try {
return await api<SessionResponse>("/get-session");
} catch (e) {
if (e instanceof AuthError && e.status === 401) return null;
throw e;
}
},
// OAuth plugin: redirect the browser to the provider.
signInSocial: async (provider: string, callbackURL: string) => {
const { url } = await api<{ url: string }>("/sign-in/social", {
method: "POST",
body: JSON.stringify({ provider, callbackURL }),
});
window.location.href = url;
},
};// useSession.tsx
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import { auth, type User } from "./auth";
interface AuthState {
user: User | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
const Ctx = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
auth.getSession().then((s) => setUser(s?.user ?? null)).finally(() => setLoading(false));
}, []);
const signIn = async (email: string, password: string) => {
const s = await auth.signIn({ email, password });
setUser(s.user);
};
const signOut = async () => {
await auth.signOut();
setUser(null);
};
return <Ctx.Provider value={{ user, loading, signIn, signOut }}>{children}</Ctx.Provider>;
}
export function useSession() {
const ctx = useContext(Ctx);
if (!ctx) throw new Error("useSession must be used within <AuthProvider>");
return ctx;
}// Usage
function Profile() {
const { user, loading, signOut } = useSession();
if (loading) return <p>Loading…</p>;
if (!user) return <p>Not signed in</p>;
return (
<div>
<p>Hi, {user.name} ({user.email})</p>
<button onClick={signOut}>Sign out</button>
</div>
);
}Cross-origin dev (Vite on
:5173). Simplest is a dev proxy so the browser stays same-origin — add tovite.config.ts:server: { proxy: { "/api": "http://localhost:8080" } }Then leave
VITE_API_URLunset. Otherwise setTrustedOrigins: ["http://localhost:5173"]on the server, serve CORS withAccess-Control-Allow-Credentials: true, and pointVITE_API_URLat the API.
The CLI is generate-only (like better-auth's generate): it emits ORM model
definitions from the core schema plus the active plugins' schema. You apply them with
your ORM — for GORM, db.AutoMigrate(authmodels.AllModels()...).
go install github.com/dnahilman/goten/cmd/goten@latest
# Generate models into <generate.output_dir>/auth_models.go
goten generateConfig file goten.config.yaml:
plugins:
- username
- oauth # adds the verification table + account token columns to the models
generate:
output_dir: ./internal/auth
output_file: auth_models.go
package: authmodels
orm: gormThen wire it into your app:
import authmodels "yourapp/internal/auth"
db.AutoMigrate(authmodels.AllModels()...) // create/upgrade goten's tables
AutoMigrateis additive (creates tables, adds columns/indexes/constraints). It does not drop/rename columns, change types destructively, or roll back. Destructive changes and data migrations are handled with your own SQL tooling.
Editor autocomplete: add # yaml-language-server: $schema=https://raw.githubusercontent.com/dnahilman/goten/main/goten.config.schema.json as the first line of your goten.config.yaml for autocomplete + inline validation in VS Code (Red Hat YAML extension), JetBrains, and other editors. See examples/basic/goten.config.yaml.
Login via username instead of (or alongside) email:
go get github.com/dnahilman/goten/plugins/usernameimport 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.
Social sign-in via OAuth 2.0 Authorization Code + PKCE + OIDC, modeled after better-auth.
Providers are registered in a map (like better-auth's socialProviders); Google is built in.
go get github.com/dnahilman/goten/plugins/oauthimport (
oauthplugin "github.com/dnahilman/goten/plugins/oauth"
"github.com/dnahilman/goten/plugins/oauth/providers"
)
auth, _ := goten.New(goten.Config{
BaseURL: "http://localhost:8080",
TrustedOrigins: []string{"http://localhost:3000"}, // allowed callbackURL origins
// ...
Plugins: []goten.Plugin{
oauthplugin.New(oauthplugin.Options{
Providers: map[string]oauthplugin.Provider{
"google": providers.Google(providers.GoogleOptions{
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
AccessType: "offline", // request a refresh token
}),
},
// EncryptOAuthTokens: true, // opt-in AES-256-GCM token encryption (default off)
}),
},
})Then add oauth to plugins in goten.config.yaml, run goten generate, and AutoMigrate
the result. The generated models gain a core verification table (which stores the OAuth
sign-in state) plus token columns on accounts.
Security: PKCE (S256), CSRF state (verification row + signed cookie), redirect-origin
validation, and anti account-takeover linking (an existing user is auto-linked only when the
provider's email is verified and, by default, the local account is verified too). Full
walkthrough — including Google Cloud Console setup — in examples/oauth-google/.
Administrative user management with a reusable RBAC model: role management, ban/unban, admin-side user CRUD, session control, and impersonation.
go get github.com/dnahilman/goten/plugins/adminimport (
adminplugin "github.com/dnahilman/goten/plugins/admin"
"github.com/dnahilman/goten/plugins/admin/access"
)
auth, _ := goten.New(goten.Config{
// ...
Plugins: []goten.Plugin{
adminplugin.New(adminplugin.Options{
// Roles: map[string]*access.Role{...}, // optional custom RBAC
}),
},
})Add admin to goten.config.yaml, run goten generate, and apply the models.
The plugin adds role/banned/ban_reason/ban_expires to users and
impersonated_by to sessions. Routes are mounted under /api/auth/admin/*
(e.g. set-role, ban-user, impersonate-user, list-users), each gated by a
permission like user:ban. Bans revoke the user's sessions and block sign-in.
Full options, the endpoint table, the RBAC statement/role model, and the
impersonation design note are in plugins/admin/README.md.
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 (declares the columns the plugin adds, consumed by goten generate), UserCreateHookProvider, SessionCreateHookProvider.
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/plugins/oauth ← OAuth plugin + Google provider (separate module)
github.com/dnahilman/goten/cmd/goten ← CLI tool (separate module)
Each module is independently versioned — go get only what you use.
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)
Goten applies CSRF origin checking to all non-safe methods (POST, PUT, DELETE, etc.):
- Bearer token present → bypass (mobile/API clients)
TrustedOriginsempty → allow requests withoutOrigin(dev-friendly)TrustedOriginsset → requireOriginto match; reject others with403
auth, _ := goten.New(goten.Config{
BaseURL: "https://myapp.com",
TrustedOrigins: []string{"https://myapp.com", "https://www.myapp.com"},
// ...
})| Goten | better-auth (TS) | Limen | Go-better-auth | |
|---|---|---|---|---|
| Multi-module plugins | ✅ | ✅ (subpath) | ✅ | ❌ |
| CLI migration tool | ✅ | ✅ | ✅ | ❌ |
| Map-based adapter | ✅ | — | ✅ | ✅ |
| OAuth providers | ✅ (Google) | ✅ | ✅ | ✅ |
| Language | Go | TypeScript | Go | Go |
See CONTRIBUTING.md. Security issues: SECURITY.md.
MIT — see LICENSE.