Skip to content

izetmolla/goauth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

goauth

A runtime-agnostic authentication library for Go, modeled closely on the architecture of Auth.js. If you know Auth.js, you already know goauth: the same concepts (providers, callbacks, events, adapters, JWT and database sessions) mapped onto idiomatic Go and the standard net/http handler interface.

import "github.com/izetmolla/goauth"

Why it mirrors Auth.js

Auth.js goauth
Auth(request, config) goauth.New(cfg) returns an http.Handler
providers/* goauth/providers (GitHub, Google, OAuth, OIDC, Credentials, Email)
Adapters (@auth/*-adapter) goauth.Adapter interface + goauth/adapters/memory
callbacks (signIn/jwt/session/...) goauth.Config.Callbacks
events goauth.Config.Events
Encrypted JWT sessions (JWE) goauth/jwt (wire-compatible dir + A256CBC-HS512)
Routes: session/csrf/providers/... The same routes under BasePath (default /auth)

Core routes

The handler serves the same fixed action table as Auth.js core:

Method Path Purpose
GET /auth/session Current session JSON ({} if none)
GET /auth/csrf Signed double-submit CSRF token
GET /auth/providers Configured providers
POST /auth/signin/{provider} Begin sign-in (OAuth redirect, etc.)
GET /auth/callback/{provider} OAuth/OIDC and magic-link callback
POST /auth/callback/{provider} Credentials sign-in
POST /auth/signout Clear the session
POST /auth/mfa/verify Submit OTP code (when MFA enabled)
GET /auth/mfa/device Check trusted device (userId, deviceId)
GET /auth/sessions List active sessions
DELETE /auth/sessions?token=… Revoke a specific session

Quick start

package main

import (
	"log"
	"net/http"

	"github.com/izetmolla/goauth"
	"github.com/izetmolla/goauth/providers/github"
)

func main() {
	auth, err := goauth.New(goauth.Config{
		Secret:    []string{"a-32+-byte-random-secret-please-change"},
		TrustHost: true, // trust the request Host (set false + URL in prod behind no proxy)
		Providers: []goauth.Provider{
			github.New("CLIENT_ID", "CLIENT_SECRET"),
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	mux.Handle("/auth/", auth) // mount the action handlers

	// Protect routes with the session helper.
	mux.HandleFunc("/me", func(w http.ResponseWriter, r *http.Request) {
		session, _ := auth.GetSession(w, r)
		if session == nil {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		w.Write([]byte("hello " + session.User.Email))
	})

	log.Fatal(http.ListenAndServe(":3000", mux))
}

Session strategies

Like Auth.js, the strategy defaults intelligently:

  • No adapter → JWT (stateless, encrypted cookie).
  • Adapter configured → database (sessions persisted via the adapter).
  • A Credentials provider forces JWT (database sessions are not supported for credentials, matching Auth.js).

Override explicitly with Config.Session.Strategy.

Providers

Each provider lives in its own subpackage under providers/, so you import only the ones you use:

import (
	"github.com/izetmolla/goauth/providers/github"
	"github.com/izetmolla/goauth/providers/google"
	"github.com/izetmolla/goauth/providers/apple"
	"github.com/izetmolla/goauth/providers/oauth"
	"github.com/izetmolla/goauth/providers/oidc"
	"github.com/izetmolla/goauth/providers/credentials"
	"github.com/izetmolla/goauth/providers/email"
	"github.com/izetmolla/goauth/providers/otp"
)

github.New(id, secret)
google.New(id, secret)              // OIDC discovery
apple.New(clientID, secret)         // ES256 client secret via apple.ClientSecret
oauth.New(oauth.Options{ /* generic OAuth 2.0 */ })
oidc.New(oidc.Options{ Issuer: "https://issuer" })
credentials.New(credentials.Options{ Authorize: ... })
email.New(email.Options{ SendVerificationRequest: ... }) // magic link; needs adapter
otp.New(otp.Options{ SendCode: ... })                    // email one-time code; needs adapter

See providers/README.md for full examples, including Sign in with Apple (form_post + JWT client secret) and the email OTP flow.

Callbacks

goauth.Config{
	Callbacks: goauth.Callbacks{
		SignIn: func(ctx context.Context, p goauth.SignInCallbackParams) (bool, error) {
			return strings.HasSuffix(p.User.Email, "@trusted.com"), nil
		},
		JWT: func(ctx context.Context, p goauth.JWTCallbackParams) (goauth.JWT, error) {
			if p.User != nil {
				p.Token["role"] = "member"
			}
			return p.Token, nil
		},
		Session: func(ctx context.Context, p goauth.SessionCallbackParams) (*goauth.Session, error) {
			return p.Session, nil
		},
	},
}

MFA — post-login OTP verification

Enable a second factor for credentials sign-ins. After a successful password check, goauth sends a one-time code and returns a challenge token. The client submits the code to complete sign-in. Users can opt to trust the device so future logins skip the OTP step.

goauth.Config{
	MFA: goauth.MFAConfig{
		Enabled:           true,
		CodeLength:        6,               // default
		MaxAge:            10 * time.Minute, // default
		TrustDeviceMaxAge: 90 * 24 * time.Hour, // default; 0 disables
		SendCode: func(ctx context.Context, p goauth.MFASendCodeParams) error {
			return sendEmail(p.Email, "Your code: "+p.Code)
		},
	},
}

Flow

  1. POST /auth/signin/credentials with email + password → if MFA is enabled and the device is not trusted, returns {"challenge":"…","expiresIn":600} instead of a session.
  2. The user receives the code via email/SMS.
  3. POST /auth/mfa/verify with challenge=…&code=123456 (optionally trustDevice=true) → completes the sign-in (returns session cookie or tokens).

Trusting a device

When the user sets trustDevice=true in the verify request, a long-lived encrypted cookie (goauth.trusted-device) is set. On subsequent logins from that browser the OTP step is skipped. The cookie lifetime is configurable via MFA.TrustDeviceMaxAge (default 90 days). Set it to a negative value to disable device trust entirely.

Pass a stable deviceId (form field, query, or X-Device-Id header) on credentials sign-in and MFA verify. Check trust before prompting for OTP:

GET /auth/mfa/device?userId=<id>&deviceId=<device>
→ { "trusted": true, "skipMfa": true }

For mobile or server-side trust stores, implement MFA.IsDeviceTrusted and MFA.TrustDevice with goauth.MFADeviceTrustParams{UserID, DeviceID}.

Session management

Authenticated users can list and revoke their active sessions via the /auth/sessions endpoint. This requires an adapter that implements the SessionLister interface (all bundled adapters do).

Method Path Purpose
GET /auth/sessions List active sessions
DELETE /auth/sessions?token=… Revoke a specific session
// List sessions
GET /auth/sessions
Authorization: Bearer <accessToken>

// Response: [{"sessionToken":"…","userId":"…","expires":"…"}, …]

// Revoke a session
DELETE /auth/sessions?token=<sessionToken>
Authorization: Bearer <accessToken>

This lets users see all devices where they're signed in and revoke any session they no longer trust — similar to "Sign out of all devices" in Google/GitHub.

API / mobile clients (React Native) — bearer tokens

Browsers use cookies, but mobile apps want tokens they can store and send. Enable the token flow and goauth will, on request, return an access token and refresh token as JSON instead of setting a cookie and redirecting.

goauth.Config{
	Tokens: goauth.TokensConfig{
		Enabled:            true,            // serve /auth/token + accept Bearer auth
		AccessTokenMaxAge:  15 * time.Minute, // default
		RefreshTokenMaxAge: 30 * 24 * time.Hour,
		// AlwaysReturn: true,               // make this a pure API (never cookies)
	},
}

A client opts in per request with the X-Auth-Flow: token header (or ?flow=token). Sign-in then returns:

{
  "sessionId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", // UUID v4 — stable session identifier
  "user": { "id": "1", "name": "Demo", "email": "demo@example.com" },
  "accessToken": "eyJhbGc…",   // short-lived JWE; send as Authorization: Bearer
  "refreshToken": "eyJhbGc…",  // long-lived; exchange at POST /auth/token
  "tokenType": "Bearer",
  "expiresIn": 900,
  "expiresAt": 1780478790
}
  • Authenticate requests: send Authorization: Bearer <accessToken>. GetSession validates it automatically (no cookie needed).
  • Refresh: POST /auth/token with refresh_token=<token> (form) or {"refreshToken":"…"} (JSON) → a fresh access/refresh pair.
  • CSRF is skipped for token-flow sign-in (no ambient cookie credentials).

React Native sketch:

const r = await fetch(`${API}/auth/callback/credentials`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Auth-Flow": "token" },
  body: new URLSearchParams({ email, password }),
});
const { accessToken, refreshToken } = await r.json();
// store securely, then: fetch(`${API}/me`, { headers: { Authorization: `Bearer ${accessToken}` } })

Fiber integration (v3)

github.com/izetmolla/fiberauth is a separate module that adapts the handler to Fiber v3, like the framework packages in Auth.js. It lives outside this module so the goauth core never pulls in Fiber; add it only if you use Fiber:

go get github.com/izetmolla/fiberauth
import (
	"github.com/gofiber/fiber/v3"
	"github.com/izetmolla/fiberauth"
)

app := fiber.New()
app.All("/auth/*", fiberauth.Handler(auth))                // mount actions
app.Get("/me", fiberauth.Protect(auth), func(c fiber.Ctx) error {
	return c.JSON(fiberauth.SessionFrom(c))                 // requires a session
})
// or non-blocking: app.Use(fiberauth.SessionLoader(auth)) then fiberauth.SessionFrom(c)

Authorization middleware (roles & conditions)

Guard requires a session and that every Authorizer passes — 401 when there's no session, 403 when an authorizer rejects. Compose the built-in authorizers or write your own:

// Require a role (reads the "roles" array or "role" string claim).
app.Get("/admin", fiberauth.Guard(auth, fiberauth.HasRole("admin")), adminHandler)

// Multiple conditions (all must pass): a role AND a claim AND a custom rule.
// HasRole accepts several roles and passes if the session has any of them.
app.Get("/billing", fiberauth.Guard(auth,
	fiberauth.HasRole("admin", "billing"),
	fiberauth.HasClaim("plan", "pro"),
	fiberauth.Condition(func(c fiber.Ctx, s *goauth.Session) bool {
		return c.Params("orgId") == s.StringClaim("org")
	}),
), billingHandler)

// Custom responses:
app.Get("/x", fiberauth.GuardWithConfig(auth, fiberauth.GuardConfig{
	Authorizers:  []fiberauth.Authorizer{fiberauth.HasRole("admin")},
	Unauthorized: func(c fiber.Ctx) error { return c.Redirect().To("/login") },
	Forbidden:    func(c fiber.Ctx) error { return c.Status(403).JSON(fiber.Map{"error": "nope"}) },
}))

Roles and custom fields come from the JWT — populate them in the JWT callback (e.g. p.Token["roles"] = []string{"admin"}); they surface on the session via session.Roles(), session.HasRole(), session.Claim() and StringClaim().

Adapters

Implement goauth.Adapter to back the library with any database. Bundled backends (each with its own README):

Package Backend Docs
adapters/memory in-memory (tests/prototypes) memory
adapters/postgres PostgreSQL postgres
adapters/mysql MySQL mysql
adapters/mariadb MariaDB mariadb
adapters/redis Redis & Redis Cluster redis
adapters/mongodb MongoDB mongodb
db, _ := sql.Open("pgx", dsn)
adapter := postgres.New(db)

// Tables are created automatically when the adapter is passed to goauth.New.
auth, _ := goauth.New(goauth.Config{ Adapter: adapter, /* … */ })

All adapters that implement the Migrator interface (postgres, mysql, mariadb) run their schema migration automatically during goauth.New() — no separate Migrate call is needed. You can still call adapter.Migrate(ctx) yourself if you prefer explicit control.

Every adapter namespaces its tables/keys/collections with a prefix that defaults to goauth_; override it with WithPrefix("…"). All generated IDs (users, accounts, sessions) use UUID v4 (stdlib crypto/rand). See adapters/README.md for the overview and each adapter's README for setup details (drivers, migrations, Redis Cluster, Mongo indexes, …).

CSRF

Unsafe actions (POST /signout, credentials and email sign-in) require the signed double-submit token from GET /auth/csrf, submitted as the csrfToken form field — identical to the Auth.js client contract.

Status

This is a from-scratch Go port focused on the core architecture: OAuth 2.0/OIDC, credentials and email providers; JWT and database sessions; CSRF, PKCE, state and nonce checks; callbacks, events and pluggable adapters. See .cursor/rules/ for conventions when extending it.

Author

Izet Molla

About

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors