cryden

package module
v2.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 10 Imported by: 0

README

CrydenSync

Go Reference GitHub Stars GitHub Forks

An embeddable, framework-agnostic authentication engine for Go. Import it, configure it, own your users.

import "github.com/crydensync/cryden/v2"

Why

Every project ends up rewriting auth from scratch, or handing user data to a third-party provider. CrydenSync is a library, not a service — your users, sessions, and audit logs stay in your own database, under your own control.

  • Own your users — no hosted service, no data leaving your infrastructure
  • No vendor lock-in — plain Postgres tables, no proprietary format
  • Framework-agnostic — no request/response objects, no assumptions about your HTTP layer
  • Zero telemetry — the engine never phones home. Logs and audit events go wherever you wire them, never to us

Install

go get github.com/crydensync/cryden/v2

Quickstart

Runs with zero setup using the in-memory store — good for trying it out or writing tests:

package main

import (
	"context"
	"os"

	"github.com/crydensync/cryden/v2"
	"github.com/crydensync/cryden/v2/store/memory"
)

func main() {
	ctx := context.Background()

	engine, err := cryden.New(cryden.Config{
		JWTSecret: os.Getenv("JWT_SECRET"),
		Users:     memory.NewUserStore(),
		Sessions:  memory.NewSessionStore(),
		Audit:     memory.NewAuditStore(),
	})
	if err != nil {
		panic(err)
	}

	user, err := cryden.SignUp(ctx, engine, "proguy@example.com", "Pass@2026", "1.2.3.4")
	if err != nil {
		panic(err)
	}

	tokens, err := cryden.Login(ctx, engine, "proguy@example.com", "Pass@2026", "1.2.3.4", "some-user-agent")
	if err != nil {
		panic(err)
	}

	userID, err := cryden.VerifyToken(engine, tokens.AccessToken)
	_ = user
	_ = userID
}

Running against Postgres

  1. Run the migration in store/postgres/migrations/0001_initial_schema.up.sql against your database.
  2. Requires Postgres 13+ (uses the built-in gen_random_uuid()).
  3. Swap the memory stores for the Postgres ones:
import (
	"database/sql"

	_ "github.com/lib/pq"
	"github.com/crydensync/cryden/v2/store/postgres"
)

db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))

engine, err := cryden.New(cryden.Config{
	JWTSecret: os.Getenv("JWT_SECRET"),
	Users:     postgres.NewUserStore(db),
	Sessions:  postgres.NewSessionStore(db),
	Audit:     postgres.NewAuditStore(db),
})

Works with any standard Postgres — Supabase, Neon, RDS, self-hosted, etc. If your provider offers both a direct and a connection-pooled URL, use the direct (or session-mode pooled) connection string — the engine relies on multi-statement transactions during token rotation, which can misbehave under transaction-mode pgbouncer poolers.

Account lockout

After repeated failed login attempts, an account is locked for a configurable duration — persistent in the database, not in-memory, so it holds even through restarts or multiple running instances. Defaults to 5 attempts / 15 minutes; override via Config.LockoutThreshold and Config.LockoutDuration.

Email verification / email change

RequestEmailChange and ConfirmEmailChange require two additional Config fields that are otherwise optional:

engine, err := cryden.New(cryden.Config{
	// ...required fields...
	Verifications: postgres.NewVerificationStore(db), // or memory.NewVerificationStore()
	EmailSender:   myEmailSenderImpl,                  // you implement notify.EmailSender
})

The engine never sends email itself — implement notify.EmailSender against whatever provider you use (SendGrid, SES, SMTP), and build the actual verification URL yourself; the engine only hands you a raw token, it has no idea what your app's domain or routes look like. Calling RequestEmailChange without these configured returns cryden.ErrEmailChangeNotConfigured rather than panicking.

OAuth (Google, GitHub, or any provider)

The engine never performs an HTTP redirect and never talks to a specific provider — that's inherently HTTP-shaped work that belongs in your API layer. By the time you call into the engine, your app has already completed the provider's redirect/callback flow and confirmed the person's identity:

engine, err := cryden.New(cryden.Config{
	// ...required fields...
	OAuth: postgres.NewOAuthStore(db), // or memory.NewOAuthStore()
})

tokens, err := cryden.LoginWithOAuth(ctx, engine, "google", externalID, email, callerIP, userAgent)

LoginWithOAuth also doubles as signup — if neither an existing link nor an existing account matches, a new user is created automatically. If the email matches an existing password-based account that isn't linked yet, it returns *auth.ErrOAuthEmailConflict (retrievable via errors.As) rather than auto-linking — auto-linking on email match alone is an account-takeover vector if a provider's email verification ever has an edge case. Resolve it by having the person log in with their password first, then call:

err := cryden.LinkOAuthIdentity(ctx, engine, userID, "google", externalID, email, callerIP)

userID must come from an already-verified session — never trust an email alone to authorize a link. Calling either function without Config.OAuth set returns cryden.ErrOAuthNotConfigured.

AI-assisted admin queries (library support only)

The ai subpackage provides the safety machinery for natural-language admin tooling — an allowlisted QueryIntent type, validateIntent, and ExecuteQuery — plus store/postgres.SafeQueryStore, a read-only query executor. This is a foundation for tools like csax's CLI to build on, not a feature you call directly in application code. An LLM's output is treated as untrusted data to validate against a strict allowlist, never as SQL to execute — and the actual DB connection passed to SafeQueryStore must be opened with a read-only Postgres role, since that's the real safety boundary, not just the allowlist check. ai.LLMProvider ships zero implementations; bring your own (OpenAI, Anthropic, OpenRouter, a local model).

What's in v2

  • Signup, login, logout (single device + all devices)
  • OAuth login/signup (Google, GitHub, or any provider) with explicit, non-auto-linking account collision handling — see OAuth
  • JWT access tokens + rotating opaque refresh tokens with theft/reuse detection
  • Session listing and revocation
  • Change password (requires current password, revokes all other sessions)
  • Change email (requires verification of the new address before it takes effect)
  • Delete account (requires current password)
  • Persistent, DB-backed account lockout after repeated failed login attempts — survives restarts, correct across multiple instances
  • Email verification primitives (token issue/confirm) — delivery is pluggable via the notify.EmailSender interface, the engine never sends email itself
  • Rate limiting, bcrypt password hashing, audit logging
  • Pagination and system-wide read facades (ListAll, Count, CountActive, SearchByType, GetUser, ListPublicSessions) for building admin tooling on top of the engine
  • ai subpackage — allowlisted, read-only query safety layer for AI-assisted admin tooling built on top of this engine (see AI-assisted admin queries)
  • One storage backend: Postgres (interface-based, more can be added later)

What's not in v2 (yet)

CLI, HTTP API, and language SDKs are separate repositories that wrap this engine — this repo is the core library only. MFA, magic links, SMS OTP, WebAuthn, SAML, and other advanced auth methods are planned for later releases.

License

MIT — see LICENSE.


Built with ❤️ in Africa · Own your users, not vendor lock-in

Documentation

Overview

Package cryden is an embeddable, framework-agnostic authentication engine. Import this package only — internal packages (auth, token, store, security, session, logger) are implementation detail.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrMissingJWTSecret    = errors.New("cryden: JWTSecret is required")
	ErrMissingUserStore    = errors.New("cryden: Config.Users is required")
	ErrMissingSessionStore = errors.New("cryden: Config.Sessions is required")
	ErrMissingAuditStore   = errors.New("cryden: Config.Audit is required")
)
View Source
var ErrEmailChangeNotConfigured = errors.New("cryden: email change requires Config.Verifications and Config.EmailSender to be set")

ErrEmailChangeNotConfigured is returned by RequestEmailChange if the Engine was built without Config.Verifications and Config.EmailSender set.

View Source
var ErrOAuthNotConfigured = errors.New("cryden: oauth login requires Config.OAuth to be set")

ErrOAuthNotConfigured is returned by LoginWithOAuth if the Engine was built without Config.OAuth set.

Functions

func ChangePassword

func ChangePassword(ctx context.Context, e *Engine, userID, currentPassword, newPassword string) error

ChangePassword requires the caller's current password as re-confirmation, and revokes all sessions on success.

func ConfirmEmailChange

func ConfirmEmailChange(ctx context.Context, e *Engine, rawToken string) error

ConfirmEmailChange completes an email change using the token from the verification link.

func DeleteAccount

func DeleteAccount(ctx context.Context, e *Engine, userID, currentPassword string) error

DeleteAccount requires the caller's current password as re-confirmation before this irreversible action.

func GetUser added in v2.1.0

func GetUser(ctx context.Context, e *Engine, email string) (store.User, error)

GetUser looks up a user by email. Read-only, no side effects — safe to expose as a public facade function, unlike ChangePassword/ DeleteAccount which require self-authentication. Added because admin tooling had no way to do this except reaching past the public facade into the store layer directly.

func LinkOAuthIdentity added in v2.1.0

func LinkOAuthIdentity(ctx context.Context, e *Engine, userID, provider, externalID, email, callerIP string) error

LinkOAuthIdentity attaches a confirmed external identity to an already-authenticated user. userID must come from a verified session/access token — this is the resolution path api should use after a *auth.ErrOAuthEmailConflict, once the caller has logged in with their password to prove ownership of the account.

func ListPublicSessions added in v2.1.0

func ListPublicSessions(ctx context.Context, e *Engine, userID string) ([]store.PublicSession, error)

ListPublicSessions is a redacted alternative to ListSessions, returning store.PublicSession (no TokenHash/FamilyID) instead of the full store.Session. Added alongside ListSessions, not as a replacement for it — existing callers of ListSessions are unaffected. Consumers building an HTTP-facing endpoint should prefer this over ListSessions plus their own hand-rolled DTO.

func ListSessions

func ListSessions(ctx context.Context, e *Engine, userID string) ([]store.Session, error)

ListSessions returns all active sessions for a user.

func Logout

func Logout(ctx context.Context, e *Engine, sessionID, userID string) error

Logout revokes a single session. Verifies ownership before revoking.

func LogoutAll

func LogoutAll(ctx context.Context, e *Engine, userID string) error

LogoutAll revokes every session belonging to userID.

func RequestEmailChange

func RequestEmailChange(ctx context.Context, e *Engine, userID, newEmail string) error

RequestEmailChange starts an email change — sends a verification link to newEmail. The email is not actually changed until ConfirmEmailChange is called with the resulting token.

func RevokeSession

func RevokeSession(ctx context.Context, e *Engine, sessionID, userID string) error

RevokeSession revokes a specific session. Verifies ownership before revoking.

func SignUp

func SignUp(ctx context.Context, e *Engine, email, password, callerIP string) (store.User, error)

SignUp creates a new user. callerIP is required — used only for rate limiting and audit metadata, never inferred by the engine.

func VerifyToken

func VerifyToken(e *Engine, accessToken string) (string, error)

VerifyToken validates an access token and returns the embedded user ID.

Types

type Config

type Config struct {
	// Required — no default exists for any of these.
	JWTSecret string
	Users     store.UserStore
	Sessions  store.SessionStore
	Audit     store.AuditStore

	// Optional — only needed if you use RequestEmailChange /
	// ConfirmEmailChange. Leave nil if you don't need that flow;
	// calling it without these configured returns a clear error
	// rather than a nil-pointer panic.
	Verifications store.VerificationStore
	EmailSender   notify.EmailSender
	// OAuth is optional — only required if LoginWithOAuth is used.
	// Left unset, LoginWithOAuth returns ErrOAuthNotConfigured.
	OAuth store.OAuthStore

	// Optional — sensible defaults applied in New() if zero-valued.
	// These are tuning knobs, not security-critical secrets, so
	// defaulting them (unlike JWTSecret) is safe.
	AccessTokenTTL         time.Duration // default: 15 minutes
	BcryptCost             int           // default: bcrypt.DefaultCost (10)
	RefreshTokenByteLength int           // default: 32
	RateLimitAttempts      int           // default: 10
	RateLimitWindow        time.Duration // default: 1 minute
	LockoutThreshold       int           // default: 5 failed attempts
	LockoutDuration        time.Duration // default: 15 minutes
	Logger                 logger.Logger // default: ConsoleJSONLogger
}

Config wires an Engine. Stores are injected directly, not constructed internally — the engine never hardcodes a storage backend. To run against Postgres, construct store/postgres.PostgresUserStore etc. and assign them here; for tests, use store/memory equivalents.

type Engine

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

Engine holds every wired-up dependency needed by the public facade functions (SignUp, Login, etc. in cryden.go). Consumers never construct this directly — always via New(cfg).

func New

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

New validates cfg, applies defaults for unset tuning knobs, and wires an Engine. Fails loudly (returns an error, never a silently insecure default) if JWTSecret or any required store is missing.

type Tokens

type Tokens = auth.Tokens

Tokens is the access/refresh token pair returned by Login and RefreshToken.

func Login

func Login(ctx context.Context, e *Engine, email, password, callerIP, userAgent string) (Tokens, error)

Login authenticates a user and issues a new session. callerIP and userAgent are required, caller-supplied.

func LoginWithOAuth added in v2.1.0

func LoginWithOAuth(ctx context.Context, e *Engine, provider, externalID, email, callerIP, userAgent string) (Tokens, error)

LoginWithOAuth is called after api has already completed the provider's redirect/callback flow and confirmed the person's identity — the engine itself never talks to Google/GitHub or performs an HTTP redirect. Returns *auth.ErrOAuthEmailConflict (retrievable via errors.As) if externalID's email matches an existing password-based account that isn't linked yet; the engine deliberately does not auto-link in that case.

func RefreshToken

func RefreshToken(ctx context.Context, e *Engine, rawRefreshToken string) (Tokens, error)

RefreshToken rotates a refresh token, issuing a new access/refresh pair. Returns auth.ErrTokenReused (wrapping token.ErrTokenReused) if reuse of an already-rotated token is detected — the entire session family has already been revoked by the time this returns.

Directories

Path Synopsis
Package ai holds the pure, reusable logic behind csax's AI-assisted admin features.
Package ai holds the pure, reusable logic behind csax's AI-assisted admin features.
cmd
smoketest command

Jump to

Keyboard shortcuts

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