authkit

package module
v0.72.0 Latest Latest
Warning

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

Go to latest
Published: Jun 26, 2026 License: MIT Imports: 6 Imported by: 0

README

AuthKit

Embedded auth library for Go applications. (Standalone server coming later.)

Construction

(Basic embedded setup)

package main

import (
	"context"
	"net/http"
	"net/netip"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/open-rails/authkit"
	authkitgin "github.com/open-rails/authkit/adapters/gin"
	"github.com/open-rails/authkit/embedded"
	authhttp "github.com/open-rails/authkit/http"
	"github.com/open-rails/authkit/verify"
)

func setupAuth() (*gin.Engine, *authhttp.Server, authkit.Client, error) {
	ctx := context.Background()

	pg, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
	if err != nil {
		return nil, nil, nil, err
	}

	// Trust only infrastructure that overwrites/appends forwarded headers.
	trustedProxies := []netip.Prefix{
		netip.MustParsePrefix("10.0.0.0/8"),
		netip.MustParsePrefix("172.16.0.0/12"),
		netip.MustParsePrefix("192.168.0.0/16"),
	}

	cfg := embedded.Config{
		Token: embedded.TokenConfig{
			Issuer:               "https://app.example.com",
			IssuedAudiences:      []string{"myapp"},
			ExpectedAudiences:    []string{"myapp"},
			AccessTokenDuration:  15 * time.Minute,
			RefreshTokenDuration: 30 * 24 * time.Hour,
			SessionMaxPerUser:    3,
		},
		Frontend: embedded.FrontendConfig{
			BaseURL:           "https://app.example.com",
			OIDCReturnPath:    "/login/callback",
			VerifyPath:        "/verify",
			PasswordResetPath: "/reset",
			PasswordlessPath:  "/passwordless",
			InvitePath:        "/accept-invite",
		},
		Registration: embedded.RegistrationConfig{
			Verification:                 authkit.RegistrationVerificationRequired,
			NativeUserMode:               authkit.RegistrationModeOpen,
			PasswordlessLogin:            true,
			PasswordlessAutoRegistration: false,
		},
		Keys: embedded.KeysConfig{
			// Vault-mounted key directory. AuthKit reads the JWT signing keys from
			// <Path>/keys.json and the TOTP secret-encryption key (#148) from
			// <Path>/totp.key — a base64/hex-encoded 16/24/32-byte AES key, perms
			// 0600/0400. Hosts never load these secrets manually.
			Path: "/vault/auth",
		},
		Identity: embedded.IdentityConfig{},
		APIKeys: embedded.APIKeysConfig{
			Prefix: "myapp",
			MaxTTL: 90 * 24 * time.Hour,
		},
		TwoFactor: embedded.TwoFactorConfig{
			// Mode: Disabled | Optional | Required. Required gates the SESSION —
			// existing un-enrolled users are challenged on their next request.
			Mode:    authkit.TwoFactorOptional,
			Methods: []authkit.TwoFactorMethod{authkit.TwoFactorEmail, authkit.TwoFactorTOTP},
			// TOTPSecretKey is an override for tests; the normal path loads
			// <Keys.Path>/totp.key (see Keys above).
		},
		Passkeys: embedded.PasskeyConfig{
			RPID:             "app.example.com",
			RPDisplayName:    "My App",
			Origins:          []string{"https://app.example.com"},
			UserVerification: "preferred",
		},
		RBAC: []authkit.PersonaDef{
			{
				Name: authkit.RootPersona,
				Roles: []authkit.RoleDef{
					{
						Name: "support",
						Permissions: []string{
							"root:users:ban",
							"root:users:recover",
						},
					},
				},
				// Optional. Root capabilities are off unless the host enables them.
				Capabilities: authkit.PersonaCapabilities{CustomRoles: true},
				Catalog: []string{
					"root:users:ban",
					"root:users:recover",
				},
			},
			{
				Name:   "org",
				Parent: authkit.RootPersona,
				Roles: []authkit.RoleDef{
					{
						Name: "admin",
						Permissions: []string{
							"org:members:read",
							"org:members:invite",
						},
					},
				},
			},
			{
				Name:   "repo",
				Parent: "org",
				Capabilities: authkit.PersonaCapabilities{
					APIKeys:            true,
					RemoteApplications: true,
				},
				Roles: []authkit.RoleDef{
					{
						Name: "developer",
						Permissions: []string{
							"repo:models:read",
							"repo:models:deploy",
						},
					},
				},
			},
		},
		Environment:   "production",
		Schema:        "profiles",
		SolanaNetwork: "mainnet",
	}

	client, err := embedded.New(cfg, pg)
	if err != nil {
		return nil, nil, nil, err
	}

	srv := authhttp.NewServer(client,
		authhttp.WithTrustedProxies(trustedProxies),
		authhttp.WithLanguageConfig(authhttp.LanguageConfig{
			Supported: []string{"en", "es"},
			Default:   "en",
		}),
	)

	router := gin.New()
	v1 := router.Group("/api/v1")
	authkitgin.RegisterAPI(v1, srv,
		authkitgin.WithGroups(
			authhttp.RouteAuth,
			authhttp.RouteRegistration,
			authhttp.RouteAccount,
			authhttp.RouteAdmin,
			authhttp.RoutePermissionGroups,
		),
	)
	authkitgin.RegisterJWKS(router, srv)
	authkitgin.RegisterOIDC(router, srv, "/oidc")

	// Host route middleware definitions, in the same order as the examples below.
	optionalAuth := authkitgin.Use(verify.Optional(srv.Verifier()))
	requireAuth := authkitgin.Use(verify.Required(srv.Verifier()))
	optionalUser := authkitgin.Use(verify.OptionalUser(srv.Verifier()))
	requireUser := authkitgin.Use(verify.RequiredUser(srv.Verifier()))
	requirePremium := authkitgin.Use(verify.RequireEntitlement("premium"))
	requirePaidPlan := authkitgin.Use(verify.RequireAnyEntitlement("premium", "pro"))
	rootScope := func(*http.Request) verify.PermissionScope {
		return verify.PermissionScope{Persona: authkit.RootPersona}
	}
	requireBanUsersPermission := authkitgin.Use(verify.RequirePermission(client, "root:users:ban", rootScope))
	repoScope := func(c *gin.Context) verify.PermissionScope {
		return verify.PermissionScope{Persona: "repo", Instance: c.Param("repo")}
	}
	requireDeployPermission := authkitgin.RequirePermission(client, "repo:models:deploy", repoScope)
	sensitive := authkitgin.Use(verify.Sensitive())
	requireDeletePermission := authkitgin.RequirePermission(client, "repo:models:delete", repoScope)

	// ====== Public routes ======
	// Public host route: no AuthKit authentication required.
	router.GET("/api/v1/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, map[string]any{
			"ok":      true,
			"service": "doujins",
		})
	})

	// ====== Optional and required user routes ======
	// Optional-user host route: public when anonymous, enriched when a user token is present.
	router.GET("/api/v1/session/optional", optionalUser, func(c *gin.Context) {
		userClaims, ok := authkitgin.UserClaims(c)
		resp := map[string]any{"authenticated": ok}
		if ok {
			resp["user_id"] = userClaims.UserID
		}
		c.JSON(http.StatusOK, resp)
	})

	// Authenticated user host route: reads token claims and loads profile data only when needed.
	router.GET("/api/v1/account/debug", requireUser, func(c *gin.Context) {
		userClaims, _ := authkitgin.UserClaims(c)
		email, err := client.GetEmailByUserID(c.Request.Context(), userClaims.UserID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, map[string]any{"error": "user_lookup_failed"})
			return
		}

		c.JSON(http.StatusOK, map[string]any{
			"user_id":        userClaims.UserID,
			"email":          email,
			"token_email":    userClaims.Email,
			"email_verified": userClaims.EmailVerified,
			"session_id":     userClaims.SessionID,
		})
	})

	// ====== User account routes ======
	// Sensitive account route: requires recent step-up before changing email.
	router.POST("/api/v1/account/email", requireUser, sensitive, func(c *gin.Context) {
		userClaims, _ := authkitgin.UserClaims(c)
		c.JSON(http.StatusOK, map[string]any{
			"user_id":    userClaims.UserID,
			"session_id": userClaims.SessionID,
			"accepted":   true,
		})
	})

	// ====== Optional and required auth routes ======
	// Optional-auth host route: public when anonymous, enriched by any valid principal.
	router.GET("/api/v1/principal/optional", optionalAuth, func(c *gin.Context) {
		principal, ok := authkitgin.Principal(c)
		resp := map[string]any{"authenticated": ok}
		if ok {
			resp["principal_kind"] = principal.Kind
			resp["issuer"] = principal.Issuer
			resp["subject"] = principal.Subject
		}
		c.JSON(http.StatusOK, resp)
	})

	// Required-auth host route: accepts users, API keys, remote apps, or delegated tokens.
	router.GET("/api/v1/principal/current", requireAuth, func(c *gin.Context) {
		principal, _ := authkitgin.Principal(c)
		c.JSON(http.StatusOK, map[string]any{
			"principal_kind": principal.Kind,
			"issuer":         principal.Issuer,
			"subject":        principal.Subject,
		})
	})

	// Permission-gated host route: accepts any principal with repo:models:deploy.
	router.POST("/api/v1/repos/:repo/models/deploy", requireAuth, requireDeployPermission, func(c *gin.Context) {
		principal, _ := authkitgin.Principal(c)
		c.JSON(http.StatusOK, map[string]any{
			"principal_kind": principal.Kind,
			"issuer":         principal.Issuer,
			"subject":        principal.Subject,
			"repo":           c.Param("repo"),
			"permission":     "repo:models:deploy",
		})
	})

	// ====== Entitlement routes ======
	// Entitlement-gated host route: requires the premium entitlement on the user.
	router.GET("/api/v1/premium/download", requireUser, requirePremium, func(c *gin.Context) {
		userClaims, _ := authkitgin.UserClaims(c)
		c.JSON(http.StatusOK, map[string]any{
			"user_id":      userClaims.UserID,
			"entitlements": userClaims.Entitlements,
			"download_url": "/downloads/premium.zip",
		})
	})

	// Any-entitlement host route: requires at least one accepted entitlement.
	router.GET("/api/v1/account/export", requireUser, requirePaidPlan, func(c *gin.Context) {
		userClaims, _ := authkitgin.UserClaims(c)
		c.JSON(http.StatusOK, map[string]any{
			"user_id":      userClaims.UserID,
			"entitlements": userClaims.Entitlements,
			"export_id":    "exp_123",
		})
	})

	// ====== Permission routes ======
	// Root-admin host route: requires root:users:ban on the singleton root persona.
	router.POST("/api/v1/admin/users/:id/ban", requireUser, requireBanUsersPermission, func(c *gin.Context) {
		userClaims, _ := authkitgin.UserClaims(c)
		c.JSON(http.StatusOK, map[string]any{
			"admin_user_id":  userClaims.UserID,
			"banned_user_id": c.Param("id"),
		})
	})

	// Sensitive permission-gated host route: requires permission plus recent step-up.
	router.DELETE("/api/v1/repos/:repo/models/:id", requireUser, sensitive, requireDeletePermission, func(c *gin.Context) {
		userClaims, _ := authkitgin.UserClaims(c)
		c.JSON(http.StatusOK, map[string]any{
			"user_id":  userClaims.UserID,
			"repo":     c.Param("repo"),
			"model_id": c.Param("id"),
			"deleted":  true,
		})
	})

	return router, srv, client, nil
}

This exposes AuthKit routes such as /api/v1/token, /api/v1/me, and /.well-known/jwks.json.

Use embedded.New for in-process AuthKit operations and authhttp.NewServer for mounted HTTP routes. The future standalone server will use remote.New for the same authkit.Client contract.

RegisterAPI(v1, srv) registers every enabled JSON API route. Use WithGroups(...) only when the host wants to mount selected surfaces: auth, registration, account, admin, and permission_groups. Browser OIDC redirects are mounted separately with RegisterOIDC; JWKS is mounted with RegisterJWKS.

RBAC config and durability

Config.RBAC is a single []authkit.PersonaDef slice. Each persona is a permission namespace and declares roles with persona:resource:action grants. root is configured with the same shape as any other persona: Parent is empty, capabilities default off, and any host root entry is merged with AuthKit's intrinsic root owner and built-in root: permissions.

Role definitions and per-persona Catalog entries are in-memory config. Editing a role's grants changes what every holder of that role can do after the new schema is loaded. The containment shape and runtime rows are durable: group_persona_parents is reconciled from config, while group_user_roles, group_custom_roles, and api_keys keep name references to personas and roles.

Treat persona names and role names as durable identifiers. Do not rename in place; create a new name, migrate assignments, then retire the old one. Removing a role, catalog grant, or persona fails closed: unresolved names grant nothing, but AuthKit does not auto-delete those rows because a typo in config must not erase operator intent. Review and clean up drifted rows deliberately, and do not reuse a retired name for a different meaning until old assignments are cleared.


Advanced Host Flows

For session history, run migrations/clickhouse and pass a clickhouse.Conn to embedded.New with embedded.WithClickHouse(ch). authhttp.NewServer(client) uses that same client as the admin sign-in reader.

func mountAdvancedAuthExamples(
	router *gin.Engine,
	client authkit.Client,
	requireAuth gin.HandlerFunc,
	requireUser gin.HandlerFunc,
) {
	type Caller struct {
		Invoker string
		Payer   string
	}
	resolveCaller := func(_ context.Context, principal authkit.Principal) (Caller, error) {
		return Caller{
			Invoker: principal.Subject,
			Payer:   principal.Subject,
		}, nil
	}

	rootScope := func(*http.Request) verify.PermissionScope {
		return verify.PermissionScope{Persona: authkit.RootPersona}
	}
	requireRootRead := authkitgin.Use(verify.RequirePermission(client, "root:resources:read", rootScope))
	requireRootCredentialsManage := authkitgin.Use(verify.RequirePermission(client, "root:credentials:manage", rootScope))
	requireRootUsersInvite := authkitgin.Use(verify.RequirePermission(client, "root:users:invite", rootScope))

	// Operator route: list users for an admin screen.
	router.GET("/api/v1/operator/users", requireUser, requireRootRead, func(c *gin.Context) {
		users, err := client.AdminListUsers(c.Request.Context(), authkit.AdminUserListOptions{
			Page:     1,
			PageSize: 50,
			Status:   authkit.AdminUserStatusActive,
			Sort:     authkit.AdminUserSortCreatedAt,
			Desc:     true,
		})
		if err != nil {
			c.JSON(http.StatusInternalServerError, map[string]any{"error": "user_list_failed"})
			return
		}
		c.JSON(http.StatusOK, users)
	})

	// Operator route: create a user directly.
	router.POST("/api/v1/operator/users", requireUser, requireRootUsersInvite, func(c *gin.Context) {
		var req struct {
			Email    string `json:"email"`
			Username string `json:"username"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, map[string]any{"error": "invalid_request"})
			return
		}
		user, err := client.CreateUser(c.Request.Context(), req.Email, req.Username)
		if err != nil {
			c.JSON(http.StatusBadRequest, map[string]any{"error": "user_create_failed"})
			return
		}
		c.JSON(http.StatusOK, user)
	})

	// Operator route: register a trusted remote application issuer.
	router.POST("/api/v1/operator/remote-applications", requireUser, requireRootCredentialsManage, func(c *gin.Context) {
		var req struct {
			Slug              string `json:"slug"`
			PermissionGroupID string `json:"permission_group_id"`
			Issuer            string `json:"issuer"`
			JWKSURI           string `json:"jwks_uri"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, map[string]any{"error": "invalid_request"})
			return
		}
		app, err := client.UpsertRemoteApplication(c.Request.Context(), authkit.RemoteApplication{
			Slug:              req.Slug,
			PermissionGroupID: req.PermissionGroupID,
			Issuer:            req.Issuer,
			JWKSURI:           req.JWKSURI,
			Mode:              authkit.RemoteAppModeJWKS,
			Enabled:           true,
		})
		if err != nil {
			c.JSON(http.StatusBadRequest, map[string]any{"error": "remote_application_register_failed"})
			return
		}
		c.JSON(http.StatusOK, app)
	})

	// Platform route: mint a delegated token for another AuthKit-protected API.
	router.POST("/api/v1/platform/delegated-token", requireAuth, func(c *gin.Context) {
		var req struct {
			Subject string `json:"subject"`
			Tier    string `json:"tier"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, map[string]any{"error": "invalid_request"})
			return
		}
		token, err := client.MintDelegatedAccessToken(c.Request.Context(), authkit.DelegatedAccessParams{
			Audiences:        []string{"tensorhub"},
			DelegatedSubject: req.Subject,
			Permissions:      []string{"repo:models:deploy"},
			Attributes:       map[string]any{"tier": req.Tier},
			TTL:              15 * time.Minute,
		})
		if err != nil {
			c.JSON(http.StatusBadRequest, map[string]any{"error": "delegated_token_failed"})
			return
		}
		c.JSON(http.StatusOK, map[string]any{"access_token": token})
	})

	// Resource route: resolve AuthKit's raw principal into the app's caller model.
	router.POST("/api/v1/resources/invoke", requireAuth, func(c *gin.Context) {
		principal, _ := authkitgin.Principal(c)
		caller, err := resolveCaller(c.Request.Context(), principal)
		if err != nil {
			c.JSON(http.StatusUnauthorized, map[string]any{"error": "unauthorized"})
			return
		}
		c.JSON(http.StatusOK, map[string]any{
			"invoker": caller.Invoker,
			"payer":   caller.Payer,
		})
	})
}

Frontend code calls the AuthKit routes mounted by RegisterAPI:

POST /api/v1/password/login
POST /api/v1/token
GET  /api/v1/me
POST /api/v1/passwordless/start
POST /api/v1/passwordless/confirm
POST /api/v1/register
GET  /api/v1/auth/capabilities
POST /api/v1/oidc/{provider}/link/start

Documentation

Overview

Package authkit holds authentication primitives shared between authkit's issuing core and its verification layer: plain data types, opaque-credential parsing, and sentinel errors that carry NO dependency on Postgres or the rest of core (stdlib only). It exists so the verification path — and, later, a standalone verify module (agents #110/#107) — can depend on these without pulling in the storage layer. The core package re-exports every symbol here as an alias, so existing callers using core.X are unaffected.

Index

Constants

View Source
const (
	ImportStatusInserted   ImportUserStatus = "inserted"
	ImportStatusSkipped    ImportUserStatus = "skipped"
	ImportStatusRejected   ImportUserStatus = "rejected"
	AdminUserStatusActive  AdminUserStatus  = "active"     // not deleted, not banned
	AdminUserStatusBanned  AdminUserStatus  = "banned"     // not deleted, currently banned
	AdminUserStatusDeleted AdminUserStatus  = "deleted"    // soft-deleted
	AdminUserStatusAny     AdminUserStatus  = "any"        // no deleted/banned predicate
	AdminUserSortCreatedAt AdminUserSort    = "created_at" // default
	AdminUserSortLastLogin AdminUserSort    = "last_login"
	AdminUserSortUsername  AdminUserSort    = "username"
	AdminUserSortEmail     AdminUserSort    = "email"
)
View Source
const (
	ErrorTypeInvalidRequest = "invalid_request_error"
	ErrorTypeAuthentication = "authentication_error"
	ErrorTypeAuthorization  = "authorization_error"
	ErrorTypeRateLimit      = "rate_limit_error"
	ErrorTypeAPI            = "api_error"
)

Error type categories, aligned with openrails' / Stripe's taxonomy strings.

View Source
const (
	RemoteAppModeJWKS   = "jwks"
	RemoteAppModeStatic = "static"
)

Remote-application trust modes (#74). A remote_application is a federation PRINCIPAL whose credential is a key, with exactly one trust source:

jwks   — keys fetched + refreshed from JWKSURI; rotation is publishing a new
         kid at the same URL.
static — authorized_keys-style human-managed PEM list for principals without
         a JWKS endpoint; manual rotation by design.
View Source
const (
	// ServiceJWTTokenUse is the required `token_use` claim for service JWTs.
	ServiceJWTTokenUse = "service"
	// DefaultServiceJWTLifetime is the recommended lifetime for first-party
	// machine-to-machine service JWTs.
	DefaultServiceJWTLifetime = 15 * time.Minute
)
View Source
const PermWildcard = "*"

PermWildcard is the wildcard CHARACTER used inside namespace-anchored globs (`org:*`, `org:members:*`, `org:*:read`, `root:*`). A bare standalone `*` is NOT a valid grant — it is rejected everywhere.

Variables

View Source
var (
	// ErrInvalidAccessToken indicates an API key that does not exist, has a bad
	// secret, or whose owning permission group is gone. Deliberately indistinguishable from
	// a malformed token so callers learn nothing from the error.
	ErrInvalidAccessToken = errors.New("invalid_token")
	// ErrAccessTokenRevoked indicates the API key was explicitly revoked.
	ErrAccessTokenRevoked = errors.New("token_revoked")
	// ErrAccessTokenExpired indicates the API key is past its expires_at.
	ErrAccessTokenExpired = errors.New("token_expired")
)
View Source
var (
	ErrBootstrapDatabaseNotEmpty              = errors.New("bootstrap_database_not_empty")
	ErrCannotRemoveLastAdminRole              = errors.New("cannot_remove_last_admin_role")
	ErrAccountRegistrationInviteConsumed      = errors.New("account_registration_invite_consumed")
	ErrAccountRegistrationInviteEmailMismatch = errors.New("account_registration_invite_email_mismatch")
	ErrAccountRegistrationInviteExpired       = errors.New("account_registration_invite_expired")
	ErrAccountRegistrationInviteNotFound      = errors.New("account_registration_invite_not_found")
	ErrAccountRegistrationInviteRevoked       = errors.New("account_registration_invite_revoked")
	ErrCustomClaimsReserved                   = errors.New("custom_jwt_reserved_claim")
	ErrCustomJWTReservedType                  = errors.New("custom_jwt_reserved_type")
	ErrEmailAlreadyVerified                   = errors.New("email_already_verified")
	ErrEmailDeliveryFailed                    = errors.New("email_delivery_failed")
	ErrEmailInUse                             = errors.New("email_in_use")
	ErrEmailSenderUnavailable                 = errors.New("email_sender_unavailable")
	ErrEmptyCustomClaims                      = errors.New("custom_jwt_empty_claims")
	ErrEntitlementFilterUnavailable           = errors.New("authkit: entitlement filtering requires an EntitlementFilterProvider")
	ErrExternalInvitesDisabled                = errors.New("external_invites_disabled")
	ErrGroupNotFound                          = errors.New("permission_group_not_found")
	ErrInsufficientRoleAuthority              = errors.New("insufficient_role_authority")
	ErrInvalidAttributeDef                    = errors.New("invalid_attribute_def")
	ErrInvalidBootstrapManifest               = errors.New("invalid_bootstrap_manifest")
	ErrInvalidUntil                           = errors.New("invalid_until")
	ErrInviteEmailMismatch                    = errors.New("group_invite_email_mismatch")
	ErrInviteLinkExhausted                    = errors.New("group_invite_link_exhausted")
	ErrInviteLinkExpired                      = errors.New("group_invite_link_expired")
	ErrInviteLinkNotFound                     = errors.New("group_invite_link_not_found")
	ErrInviteLinkRevoked                      = errors.New("group_invite_link_revoked")
	ErrMissingSigner                          = errors.New("missing_signer")
	ErrNotGroupMember                         = errors.New("not_group_member")
	ErrOwnerSlugTaken                         = errors.New("owner_slug_taken")
	ErrPasskeyCloneDetected                   = errors.New("passkey_clone_detected")
	ErrPasskeyNotFound                        = errors.New("passkey_not_found")
	ErrPasskeyUserVerificationRequired        = errors.New("passkey_user_verification_required")
	ErrPasswordlessDisabled                   = errors.New("passwordless_disabled")
	ErrPasswordResetRequired                  = errors.New("password_reset_required")
	ErrPendingRegistrationNotFound            = errors.New("pending_registration_not_found")
	ErrPhoneAlreadyVerified                   = errors.New("phone_already_verified")
	ErrPhoneInUse                             = errors.New("phone_in_use")
	ErrRegistrationDisabled                   = errors.New("registration_disabled")
	ErrRemoteApplicationNotFound              = errors.New("remote_application_not_found")
	ErrRenameRateLimited                      = errors.New("rename_rate_limited")
	ErrReservedIssuer                         = errors.New("reserved_issuer")
	ErrResourceScopeDenied                    = errors.New("resource_scope_denied")
	ErrRoleAssignmentEscalation               = errors.New("role_assignment_escalation")
	ErrSMSDeliveryFailed                      = errors.New("sms_delivery_failed")
	ErrSMSSenderUnavailable                   = errors.New("sms_unavailable")
	ErrStepUpRequired                         = errors.New("step_up_required")
	ErrTooManyCustomClaims                    = errors.New("custom_jwt_too_many_claims")
	ErrTwoFAEnrollmentRequired                = errors.New("2fa_enrollment_required")
	ErrUserBanned                             = errors.New("user_banned")
	ErrUserNotFound                           = errors.New("user_not_found")
	ErrUserRoleNotFound                       = errors.New("user_role_not_found")
	ErrVerificationLinkExpired                = errors.New("verification_link_expired")
	ErrSIWSAddressMismatch                    = errors.New("siws_address_mismatch")
	ErrSIWSChallengeExpired                   = errors.New("siws_challenge_expired")
	ErrSIWSChallengeNotFound                  = errors.New("siws_challenge_not_found")
	ErrSIWSDomainInvalid                      = errors.New("siws_domain_invalid")
	ErrSIWSSignatureInvalid                   = errors.New("siws_signature_invalid")
	ErrSIWSTimestampInvalid                   = errors.New("siws_timestamp_invalid")
	ErrWalletAlreadyLinked                    = errors.New("wallet_already_linked")
	ErrProviderAlreadyLinked                  = errors.New("provider_already_linked")
)

Sentinel errors — the wire-contract error identities shared by the embedded engine and (Phase 2) the remote SDK so errors.Is works across transports (#138 contract inversion). internal/authcore aliases these.

View Source
var ErrAttributeDefNotFound = errors.New("attribute_def_not_found")

ErrAttributeDefNotFound indicates no registered remote-application attribute definition matched.

View Source
var ErrInvalidRemoteApplication = errors.New("invalid_remote_application")

ErrInvalidRemoteApplication indicates a malformed remote_application registration payload.

View Source
var ErrInvalidServiceJWT = errors.New("invalid_service_jwt")

ErrInvalidServiceJWT indicates a presented service JWT failed verification.

Functions

func APIKeyMarker

func APIKeyMarker(prefix string) string

APIKeyMarker returns the leading marker that identifies an API key for the given application prefix: "<prefix>_st_" when prefix is non-empty, else "st_".

func ErrorForCode added in v0.68.0

func ErrorForCode(code string) error

ErrorForCode maps a wire error code (a sentinel's Error() string) back to the sentinel, so a remote client re-derives errors.Is(err, authkit.ErrX) identity across the network. Unknown/empty codes return nil — the caller supplies its own fallback. The server emits err.Error() as the code; remote/ resolves it here, so the wire-error contract has ONE source of truth (#142).

func ErrorMessage

func ErrorMessage(code string) string

ErrorMessage returns a human-readable English message for a wire error code: a curated message for common codes, otherwise a humanized form of the code so the message is never empty. Localized catalogs are a future extension.

func ErrorTypeForStatus

func ErrorTypeForStatus(status int) string

ErrorTypeForStatus maps an HTTP status to its error-type category (the same inference openrails performs).

func FormatAPIKey

func FormatAPIKey(prefix, keyID, secret string) string

FormatAPIKey assembles the full presented token: <marker><key_id>_<secret>.

func HasAPIKeyPrefix

func HasAPIKeyPrefix(prefix, token string) bool

HasAPIKeyPrefix reports whether token carries the API-key marker for prefix. Used by middleware to route to the API-key path before attempting JWT verification.

func ParseAPIKey

func ParseAPIKey(prefix, token string) (keyID, secret string, ok bool)

ParseAPIKey splits a presented token into its key_id and secret. key_id and secret are base62 (no underscores), so the first "_" after the marker is the unambiguous delimiter. ok is false if the token lacks the marker or either part is empty.

func PermMatches

func PermMatches(grant, concrete string) bool

PermMatches reports whether a GRANT token authorizes a CONCRETE permission. The grant may be a literal (`org:members:read`) or a namespace-anchored glob where `*` wildcards a whole segment (`org:members:*`, `org:*:read`, `org:*`). The namespace (segment 0) must be a literal — a bare `*` (or a `*` namespace) never matches. A two-segment glob `ns:*` matches every concrete `ns:…` perm.

This is the shared, authz-critical matcher used by both core's RBAC checks and the verification layer's permission-coverage checks.

func PermissionTokenCovers

func PermissionTokenCovers(grant, requested string) bool

PermissionTokenCovers reports whether a stored grant token covers a requested permission token using AuthKit's namespace-anchored glob semantics.

Types

type APIKey

type APIKey struct {
	ID          string
	KeyID       string
	Name        string
	Role        string
	Permissions []string
	CreatedBy   string
	CreatedAt   time.Time
	LastUsedAt  *time.Time
	ExpiresAt   *time.Time
	RevokedAt   *time.Time
}

type APIKeyMintOptions

type APIKeyMintOptions struct {
	Name      string
	Role      string
	CreatedBy string
	ExpiresAt *time.Time
}

type APIKeys added in v0.72.0

type APIKeys interface {
	MintAPIKey(ctx context.Context, persona, instanceSlug, name, role, createdBy string, expiresAt *time.Time) (APIKey, string, error)
	MintAPIKeyWithOptions(ctx context.Context, persona, instanceSlug string, opts APIKeyMintOptions) (APIKey, string, error)
	ListAPIKeys(ctx context.Context, persona, instanceSlug string) ([]APIKey, error)
	RevokeAPIKey(ctx context.Context, persona, instanceSlug, tokenID string) (bool, error)
	ResolveAPIKey(ctx context.Context, keyID, secret string) (string, []string, error)
	ResolveAPIKeyDetailed(ctx context.Context, keyID, secret string) (ResolvedAPIKey, error)
}

APIKeys mints, lists, revokes, and resolves opaque API keys.

type AccountRegistrationInvite added in v0.72.0

type AccountRegistrationInvite struct {
	ID         string
	Email      string
	InvitedBy  string
	ExpiresAt  time.Time
	RevokedAt  *time.Time
	ConsumedAt *time.Time
	ConsumedBy *string
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

type AccountRegistrationInviteCreated added in v0.72.0

type AccountRegistrationInviteCreated struct {
	ID        string
	Code      string
	URL       string
	Email     string
	ExpiresAt time.Time
}

type Admin added in v0.72.0

type Admin interface {
	AdminCountUsers(ctx context.Context, opts AdminUserListOptions) (int64, error)
	AdminGetUser(ctx context.Context, id string) (*AdminUser, error)
	AdminListUserSessions(ctx context.Context, userID string) ([]Session, error)
	AdminListUsers(ctx context.Context, opts AdminUserListOptions) (*AdminListUsersResult, error)
	AdminRevokeUserSessions(ctx context.Context, userID string) error
	AdminSetPassword(ctx context.Context, userID, new string) error
	BanUser(ctx context.Context, userID string, reason *string, until *time.Time, bannedBy string) error
	UnbanUser(ctx context.Context, userID string) error
}

Admin is the intrinsic admin view of the user directory: list, inspect, ban, and admin-side session/password control.

type AdminListUsersResult

type AdminListUsersResult struct {
	Users  []AdminUser `json:"users"`
	Total  int64       `json:"total"`
	Limit  int         `json:"limit"`
	Offset int         `json:"offset"`
}

type AdminUser

type AdminUser struct {
	ID              string     `json:"id"`
	Email           *string    `json:"email"` // Nullable for phone-only users
	PhoneNumber     *string    `json:"phone_number"`
	Username        *string    `json:"username"`
	DiscordUsername *string    `json:"discord_username"`
	EmailVerified   bool       `json:"email_verified"`
	PhoneVerified   bool       `json:"phone_verified"`
	BannedAt        *time.Time `json:"banned_at,omitempty"`
	BannedUntil     *time.Time `json:"banned_until,omitempty"`
	BanReason       *string    `json:"ban_reason,omitempty"`
	BannedBy        *string    `json:"banned_by,omitempty"`
	DeletedAt       *time.Time `json:"deleted_at"`
	Biography       *string    `json:"biography"`
	CreatedAt       time.Time  `json:"created_at"`
	UpdatedAt       time.Time  `json:"updated_at"`
	LastLogin       *time.Time `json:"last_login"`
	Roles           []string   `json:"roles"`
	RemovedRoles    []string   `json:"removed_roles,omitempty"`
	Entitlements    []string   `json:"entitlements"`
}

type AdminUserListOptions

type AdminUserListOptions struct {
	Page        int
	PageSize    int
	Search      string          // ILIKE over username/email/phone_number
	Role        string          // root_role slug (e.g. "admin"); empty = no role filter
	Status      AdminUserStatus // empty = non-deleted (historical default)
	Sort        AdminUserSort   // empty = created_at
	Desc        bool            // true = descending
	Entitlement string          // empty = no entitlement filter; else provider-backed
}

type AdminUserSort

type AdminUserSort string

type AdminUserStatus

type AdminUserStatus string

type AuthCapabilities added in v0.72.0

type AuthCapabilities struct {
	Registration AuthRegistrationCapabilities `json:"registration"`
	Providers    []AuthProviderSummary        `json:"providers"`
	Password     AuthPasswordCapabilities     `json:"password"`
	Passwordless AuthPasswordlessCapabilities `json:"passwordless"`
	Passkeys     AuthPasskeyCapabilities      `json:"passkeys"`
	Solana       AuthSolanaCapabilities       `json:"solana"`
	Verification AuthVerificationCapabilities `json:"verification"`
	Languages    []string                     `json:"languages,omitempty"`
}

AuthCapabilities is the public, static auth feature-discovery response.

type AuthPasskeyCapabilities added in v0.72.0

type AuthPasskeyCapabilities struct {
	Login bool `json:"login"`
}

type AuthPasswordCapabilities added in v0.72.0

type AuthPasswordCapabilities struct {
	Login bool `json:"login"`
}

type AuthPasswordlessCapabilities added in v0.72.0

type AuthPasswordlessCapabilities struct {
	Enabled  bool     `json:"enabled"`
	Channels []string `json:"channels,omitempty"`
}

type AuthProviderSummary added in v0.72.0

type AuthProviderSummary struct {
	ID                   string `json:"id"`
	Name                 string `json:"name"`
	Kind                 string `json:"kind"`
	SupportsLogin        bool   `json:"supports_login"`
	SupportsRegistration bool   `json:"supports_registration"`
	SupportsLink         bool   `json:"supports_link"`
}

type AuthRegistrationCapabilities added in v0.72.0

type AuthRegistrationCapabilities struct {
	Mode                string `json:"mode"`
	InviteTokenRequired bool   `json:"invite_token_required"`
}

type AuthSolanaCapabilities added in v0.72.0

type AuthSolanaCapabilities struct {
	Login bool `json:"login"`
}

type AuthVerificationCapabilities added in v0.72.0

type AuthVerificationCapabilities struct {
	Registration string `json:"registration"`
}

type Authorizer added in v0.67.0

type Authorizer interface {
	Can(ctx context.Context, subjectID, subjectKind, persona, instanceSlug, perm string) (bool, error)
	ListEffectivePermissions(ctx context.Context, subjectID, subjectKind, persona, instanceSlug string) ([]string, error)
	IsUserAllowed(ctx context.Context, userID string) (bool, error)
	ListRoleSlugsByUserErr(ctx context.Context, userID string) ([]string, error)
}

Authorizer is a cross-cutting consumer slice (#143): the "can this subject do X here" methods. Unlike the per-topic interfaces in client.go, it spans three of them (Groups for permission checks, Users for the live-user/ban gate, Roles for role resolution), so it is defined here as its own narrow view rather than mapping to one. doujins's request gate depends on it.

Add a cross-cutting slice like this only when a real consumer signature needs one; the per-topic interfaces (Users, Tokens, Groups, ...) cover the rest.

type Bootstrap added in v0.72.0

type Bootstrap interface {
	// ApplyBootstrapManifest applies a parsed manifest. There is deliberately no
	// ApplyBootstrapManifestFile on the contract: a file path is the SERVER's
	// filesystem, meaningless over a remote transport (#142). Hosts with a file
	// load it themselves (e.g. embedded.LoadBootstrapManifestFile) then call this.
	ApplyBootstrapManifest(ctx context.Context, manifest BootstrapManifest, opts BootstrapReconcileOptions) (BootstrapManifestResult, error)
}

Bootstrap applies a parsed bootstrap manifest (operator/deploy seeding).

type BootstrapManifest

type BootstrapManifest struct {
	Users              []BootstrapManifestUser              `json:"users" yaml:"users"`
	RemoteApplications []BootstrapManifestRemoteApplication `json:"remote_applications" yaml:"remote_applications"`
	GroupRoles         []BootstrapManifestGroupRole         `json:"group_roles" yaml:"group_roles"`
}

type BootstrapManifestGroupRole

type BootstrapManifestGroupRole struct {
	Username              string `json:"username" yaml:"username"`
	RemoteApplicationSlug string `json:"remote_application_slug" yaml:"remote_application_slug"`
	Persona               string `json:"persona" yaml:"persona"`
	InstanceSlug          string `json:"instance_slug" yaml:"instance_slug"`
	Role                  string `json:"role" yaml:"role"`
}

type BootstrapManifestRemoteApplication

type BootstrapManifestRemoteApplication struct {
	Slug       string         `json:"slug" yaml:"slug"`
	Issuer     string         `json:"issuer" yaml:"issuer"`
	JWKSURI    string         `json:"jwks_uri" yaml:"jwks_uri"`
	PublicKeys []RemoteAppKey `json:"public_keys" yaml:"public_keys"`
	Enabled    *bool          `json:"enabled" yaml:"enabled"`
	RootRole   string         `json:"root_role" yaml:"root_role"`
}

type BootstrapManifestResult

type BootstrapManifestResult struct {
	DryRun               bool `json:"dry_run"`
	AlreadyApplied       bool `json:"already_applied"`
	UsersCreated         int  `json:"users_created"`
	UsersUpdated         int  `json:"users_updated"`
	PasswordsSet         int  `json:"passwords_set"`
	PasswordsKept        int  `json:"passwords_kept"`
	RootRoleAssignments  int  `json:"root_role_assignments"`
	GroupRoleAssignments int  `json:"group_role_assignments"`
	RemoteApplications   int  `json:"remote_applications"`
	RemoteAppRootRoles   int  `json:"remote_application_root_roles"`
}

type BootstrapManifestUser

type BootstrapManifestUser struct {
	Email         string                 `json:"email" yaml:"email"`
	PhoneNumber   string                 `json:"phone_number" yaml:"phone_number"`
	Username      string                 `json:"username" yaml:"username"`
	EmailVerified bool                   `json:"email_verified" yaml:"email_verified"`
	PhoneVerified bool                   `json:"phone_verified" yaml:"phone_verified"`
	Banned        bool                   `json:"banned" yaml:"banned"`
	BannedAt      *time.Time             `json:"banned_at" yaml:"banned_at"`
	BannedUntil   *time.Time             `json:"banned_until" yaml:"banned_until"`
	BanReason     *string                `json:"ban_reason" yaml:"ban_reason"`
	BannedBy      *string                `json:"banned_by" yaml:"banned_by"`
	Metadata      map[string]any         `json:"metadata" yaml:"metadata"`
	Password      *BootstrapUserPassword `json:"password" yaml:"password"`
	// RootRole assigns one root permission-group role to this user by name.
	// "owner" (the built-in apex, root:*) is seeded SEED-IF-ABSENT; any other
	// name is assigned as a same-named catalog role of the root persona.
	RootRole string `json:"root_role" yaml:"root_role"`
}

type BootstrapReconcileOptions

type BootstrapReconcileOptions struct {
	DryRun bool
	// StartupOnly applies the manifest at most once, using Name as the marker.
	// Leave false for ordinary operator/CLI applies.
	StartupOnly bool
	// Name scopes the startup apply-once marker. Empty means "default".
	Name string
}

type BootstrapUserPassword

type BootstrapUserPassword struct {
	Plaintext     string         `json:"plaintext" yaml:"plaintext"`
	Hash          string         `json:"hash" yaml:"hash"`
	HashAlgo      string         `json:"hash_algo" yaml:"hash_algo"`
	HashParams    map[string]any `json:"hash_params" yaml:"hash_params"`
	ResetRequired bool           `json:"reset_required" yaml:"reset_required"`
	// Enforce makes the password DESIRED-STATE (#89): re-asserted on every
	// reconcile. Default false = SEED-ONCE — the password is applied only when
	// the user is first created, so a password rotated out of band (via the
	// admin API) is never reverted to the manifest value on a later reconcile.
	// Must not be combined with ResetRequired (forcing a reset every run is
	// nonsensical).
	Enforce bool `json:"enforce" yaml:"enforce"`
}

type Client

Client is the portable AuthKit contract: the full set of operations meaningful across both the in-process (embedded) and the Phase-2 remote transports (issue #138), composed from the topic interfaces above. Infra accessors (Postgres, Keyfunc, JWKS, raw Options/Schema) are deliberately OFF this interface; they stay on the concrete *embedded.Client. Code against authkit.Client (or one of the topic interfaces) so swapping backends is construction-only:

var c authkit.Client = embedded.New(cfg, pg)     // today (in-process)
// var c authkit.Client = remote.New(url, creds) // Phase 2 (standalone)

type CreateAccountRegistrationInviteRequest added in v0.72.0

type CreateAccountRegistrationInviteRequest struct {
	Email     string
	InvitedBy string
	ExpiresIn time.Duration
}

type CreateGroupInviteLinkRequest

type CreateGroupInviteLinkRequest struct {
	Persona      string
	InstanceSlug string
	Role         string
	Email        string // empty => permission-group shareable link; set => permission-group email-bound invite
	MaxUses      *int
	ExpiresIn    time.Duration
	InvitedBy    string
}

type CreatePermissionGroupRequest

type CreatePermissionGroupRequest struct {
	Persona            string
	InstanceSlug       string
	ParentPersona      string
	ParentInstanceSlug string
	OwnerSubjectID     string
}

type CustomJWTMintOptions

type CustomJWTMintOptions struct {
	// Claims is the host's claim set, e.g. {"cap_kind": "...", "grants": [...],
	// "release_id": "..."}. Required and non-empty. It may carry `sub`/`aud`
	// (unless overridden by the Subject/Audiences options) but may NOT carry the
	// AuthKit-owned registered claims `iss`/`iat`/`exp`.
	Claims map[string]any
	// TTL is the token lifetime. Required (must be > 0); capped at
	// MaxCustomJWTLifetime.
	TTL time.Duration
	// Type is the JOSE `typ` header (e.g. "worker-capability+jwt"). When empty the
	// header is left unset — unlike the opinionated minters, MintCustomJWT does
	// not impose a default `typ`; the host owns the token shape. It may NOT be one
	// of AuthKit's own first-party classes (access / delegated-access /
	// remote-application-access / service `+jwt`) — doing so returns
	// ErrCustomJWTReservedType (AK2-AUTH-02).
	Type string
	// Subject, when set, becomes the `sub` claim and wins over any `sub` in Claims.
	Subject string
	// Audiences, when set, becomes the `aud` claim and wins over any `aud` in Claims.
	Audiences []string
	// Issuer, when set, becomes the `iss` claim; otherwise `iss` defaults to the
	// Service's configured Issuer. This is the ONLY way to override `iss`.
	Issuer string
}

type DelegatedAccessParams

type DelegatedAccessParams struct {
	// Issuer becomes the `iss` claim: the AuthKit issuer that signed the token.
	// Must match a remote_application registered with the validating resource server.
	// Required when minting via the free function; the *Service mint method
	// defaults it to the Service's configured Issuer when empty.
	Issuer string
	// Audiences becomes the `aud` claim: the target resource API(s), e.g.
	// "openrails", "tensorhub", or "gen-orchestrator".
	Audiences []string
	// DelegatedSubject becomes `delegated_sub`: the issuer-side subject id.
	// Required. No local account is implied in the receiving service.
	DelegatedSubject string
	// Permissions becomes the `permissions` claim: an array of resource-defined
	// permission strings (NOT OAuth's space-delimited `scope`). Receiving
	// services validate these against their own permission set.
	Permissions []string
	// Attributes becomes the `attributes` claim: the canonical app-specific
	// ESCAPE HATCH (#75). An object of issuer-asserted, NAMESPACED, OPAQUE
	// key/values that AuthKit transports + optionally shape-validates but NEVER
	// interprets — the semantics belong to the consuming app (tensorhub etc.).
	// Each value is set in ONE of two modes, per key:
	//   INLINE    — the value carries the full definition, e.g.
	//               {"tier":{"endpoints":[...],"caps":[...]}}. No lookup.
	//   REFERENCE — the value is a short string key, e.g. {"tier":"tier-1"},
	//               resolved by the consumer against a definition the
	//               remote_application registered ahead of time (see the
	//               attribute-def registry: Service.RegisterRemoteAppAttributeDef
	//               / ResolveRemoteAppAttributeDef). Keeps tokens small.
	// Reserved well-known keys: `tier` (opaque entitlement-tier string) and
	// `roles` (a uuid array; prefer the typed Roles field below). Everything
	// else is free-form per consuming app. Values are arbitrary JSON.
	Attributes map[string]any
	// Roles is a convenience for emitting the delegated subject's role UUIDs into
	// `attributes.roles` (a JSON array of UUID strings). Equivalent to setting
	// Attributes["roles"] yourself; when both are set this typed field wins.
	Roles []string
	// TTL is the token lifetime. Defaults to 15m when zero.
	TTL time.Duration
	// JTI, when set, becomes the `jti` claim (token identifier). Optional.
	JTI string
	// NotBefore, when set, becomes the `nbf` claim. Optional.
	NotBefore time.Time
}

type Entitlements added in v0.72.0

type Entitlements interface {
	ListEntitlements(ctx context.Context, userID string) []string
}

Entitlements reads a user's active entitlement names from the host-provided EntitlementsProvider.

type ErrorEnvelope

type ErrorEnvelope struct {
	Error ErrorObject `json:"error"`
}

ErrorEnvelope is the top-level error response: {"error": {...}}.

func NewErrorEnvelope

func NewErrorEnvelope(status int, code string, param *string, metadata map[string]any) ErrorEnvelope

NewErrorEnvelope builds the canonical nested error envelope for an HTTP status + machine code: the type is derived from the status and the message from the code catalog. param and metadata are optional (omitted when nil/empty).

type ErrorObject

type ErrorObject struct {
	Type     string         `json:"type"`
	Code     string         `json:"code"`
	Message  string         `json:"message"`
	Param    *string        `json:"param,omitempty"`
	Metadata map[string]any `json:"metadata,omitempty"`
}

ErrorObject is the nested error detail carried under the top-level "error" key.

type GroupInviteLink struct {
	ID                string
	PermissionGroupID string
	Role              string
	InvitedBy         string
	Email             string // "" = permission-group shareable link; set = permission-group email-bound invite
	MaxUses           *int   // nil = unlimited
	Uses              int
	ExpiresAt         *time.Time
	RevokedAt         *time.Time
	CreatedAt         time.Time
	UpdatedAt         time.Time
}

type GroupInviteLinkCreated

type GroupInviteLinkCreated struct {
	ID   string
	Code string
	URL  string
}

type GroupMember

type GroupMember struct {
	SubjectID   string
	SubjectKind string
	Role        string
}

type Groups added in v0.72.0

type Groups interface {
	CreatePermissionGroup(ctx context.Context, req CreatePermissionGroupRequest) (string, error)
	EnsureRootGroup(ctx context.Context) (string, error)
	SeedPermissionGroupContainment(ctx context.Context) error
	ResolveGroupIDForSlug(ctx context.Context, persona, instanceSlug string) (string, error)
	CreateAccountRegistrationInvite(ctx context.Context, req CreateAccountRegistrationInviteRequest) (AccountRegistrationInviteCreated, error)
	RevokeAccountRegistrationInvite(ctx context.Context, inviteID, actorUserID string) error
	AssignGroupRole(ctx context.Context, persona, instanceSlug, subjectID, subjectKind, role string) error
	AssignGroupRoleAs(ctx context.Context, actorUserID, persona, instanceSlug, subjectID, subjectKind, role string) error
	UnassignGroupRoleAs(ctx context.Context, actorUserID, persona, instanceSlug, subjectID, subjectKind, role string) error
	RemoveGroupSubjectAs(ctx context.Context, actorUserID, persona, instanceSlug, subjectID, subjectKind string) error
	ListGroupMembers(ctx context.Context, persona, instanceSlug string) ([]GroupMember, error)
	ListSubjectGroups(ctx context.Context, subjectID, subjectKind string) ([]SubjectGroupMembership, error)
	Can(ctx context.Context, subjectID, subjectKind, persona, instanceSlug, perm string) (bool, error)
	ListEffectivePermissions(ctx context.Context, subjectID, subjectKind, persona, instanceSlug string) ([]string, error)
	CreateGroupInviteLink(ctx context.Context, req CreateGroupInviteLinkRequest) (GroupInviteLinkCreated, error)
	ListGroupInviteLinks(ctx context.Context, persona, instanceSlug string) ([]GroupInviteLink, error)
	RevokeGroupInviteLink(ctx context.Context, persona, instanceSlug, linkID string) error
	RedeemGroupInviteLink(ctx context.Context, code, redeemerUserID string) (RedeemGroupInviteLinkResult, error)
	ExternalInvitesEnabled() bool
}

Groups is the permission-group surface: lifecycle, membership, role assignment, authorization checks, and invite links.

type ImportUserInput

type ImportUserInput struct {
	Email         string
	PhoneNumber   string
	Username      string
	EmailVerified bool
	PhoneVerified bool
	BannedAt      *time.Time
	BannedUntil   *time.Time
	BanReason     *string
	BannedBy      *string
	Metadata      map[string]any
	CreatedAt     *time.Time
	UpdatedAt     *time.Time

	// Optional pre-hashed credential to import alongside the user (bulk legacy
	// migration). When PasswordHash is non-empty and the user row is inserted,
	// ImportUsers stores it verbatim. The verify-time whitelist (argon2id/bcrypt,
	// else legacy-reset-required) still governs login; bulk import does not
	// re-validate the hash, matching single-row UpsertPasswordHash.
	PasswordHash string
	HashAlgo     string
	HashParams   []byte
}

type ImportUserResult

type ImportUserResult struct {
	Index  int
	UserID string // set when Status == inserted
	Status ImportUserStatus
	Reason string // set for skipped/rejected (machine-ish: "duplicate_in_batch", "already_exists", or a validation code)
}

type ImportUserStatus

type ImportUserStatus string

type ImportUsersResult

type ImportUsersResult struct {
	Results  []ImportUserResult
	Inserted int
	Skipped  int
	Rejected int
}

type MFAStatus

type MFAStatus struct {
	Enabled        bool
	Satisfied      bool
	AllowedMethods []string
}

type Maintenance added in v0.72.0

type Maintenance interface {
	CleanupExpiredAuthState(ctx context.Context) error
	ValidateVerificationConfiguration() error
}

Maintenance is operational upkeep run outside a request: expire stale auth state, validate the verification configuration.

type Passwordless added in v0.72.0

type Passwordless interface {
	StartPasswordless(ctx context.Context, req PasswordlessStartRequest) (PasswordlessStartResult, error)
	ConfirmPasswordlessCode(ctx context.Context, identifier, code string) (PasswordlessConfirmResult, error)
	ConfirmPasswordlessToken(ctx context.Context, token string) (PasswordlessConfirmResult, error)
	RecordFailedPasswordlessCode(ctx context.Context, identifier string)
	ClearPasswordlessCodeAttempts(ctx context.Context, identifier string)
}

Passwordless drives the email/SMS code/link login flow.

type PasswordlessConfirmResult

type PasswordlessConfirmResult struct {
	UserID   string
	Method   string
	ReturnTo string
}

type PasswordlessStartRequest

type PasswordlessStartRequest struct {
	Identifier         string
	Mode               string
	ReturnTo           string
	PreferredLanguage  string
	AccountInviteToken string
}

type PasswordlessStartResult

type PasswordlessStartResult struct {
	Sent    bool
	Channel string
	Code    string
	LinkURL string
}

type Passwords added in v0.72.0

type Passwords interface {
	ChangePassword(ctx context.Context, userID, current, new string, keepSessionID *string) error
	UpsertPasswordHash(ctx context.Context, userID, hash, algo string, params []byte) error
	VerifyUserPassword(ctx context.Context, userID, pass string) bool
}

Passwords is the password credential surface: change, import, verify.

type PersonaCapabilities added in v0.72.0

type PersonaCapabilities struct {
	APIKeys            bool
	RemoteApplications bool
	CustomRoles        bool
}

PersonaCapabilities are opt-in generated management capabilities for a persona.

type PreferredLanguage

type PreferredLanguage struct {
	Language string
}

type Principal added in v0.72.0

type Principal struct {
	Kind    PrincipalKind `json:"kind"`
	Issuer  string        `json:"issuer,omitempty"`
	Subject string        `json:"subject,omitempty"`
}

Principal is the small generic-auth shape host adapters expose.

type PrincipalKind added in v0.72.0

type PrincipalKind string

PrincipalKind is the broad AuthKit credential class for a verified request.

const (
	PrincipalKindUser              PrincipalKind = "user"
	PrincipalKindAPIKey            PrincipalKind = "api_key"
	PrincipalKindRemoteApplication PrincipalKind = "remote_application"
	PrincipalKindDelegated         PrincipalKind = "delegated"
	PrincipalKindService           PrincipalKind = "service"
)

type Providers added in v0.72.0

type Providers interface {
	LinkProvider(ctx context.Context, userID, provider, subject string, email *string) error
	LinkProviderByIssuer(ctx context.Context, userID, issuer, providerSlug, subject string, email *string) error
	UnlinkProvider(ctx context.Context, userID, provider string) error
	GetProviderUsername(ctx context.Context, userID, provider string) (string, error)
}

Providers links and unlinks external identity providers on an account.

type RBACDriftReport added in v0.72.0

type RBACDriftReport struct {
	GroupUserRoles int `json:"group_user_roles"`
	CustomRoles    int `json:"group_custom_roles"`
	APIKeys        int `json:"api_keys"`
}

func (RBACDriftReport) Total added in v0.72.0

func (r RBACDriftReport) Total() int

type RedeemGroupInviteLinkResult

type RedeemGroupInviteLinkResult struct {
	Persona      string
	InstanceSlug string
	Role         string
}

type RegistrationMode added in v0.72.0

type RegistrationMode string

RegistrationMode is the public native-user self-registration policy (#147). It governs ONLY public self-registration; operators can always create users through privileged APIs, bootstrap, or manual DB operations regardless of mode.

Open       — anyone may self-register.
InviteOnly — self-registration requires a valid account-registration invite
             bound to the registrant's email (see account-registration invites).
Closed      — no public self-registration at all.

The former AdminOnly / AdminBootstrapOnly / ManifestOnly modes were removed (#147): they described operator-side creation, not a public self-registration policy, and are subsumed by "use the privileged APIs" under any mode.

const (
	RegistrationModeOpen       RegistrationMode = "open"
	RegistrationModeInviteOnly RegistrationMode = "invite_only"
	RegistrationModeClosed     RegistrationMode = "closed"
)

type RegistrationVerificationPolicy added in v0.72.0

type RegistrationVerificationPolicy string

RegistrationVerificationPolicy controls whether a newly-registered contact must be verified.

const (
	RegistrationVerificationNone     RegistrationVerificationPolicy = "none"
	RegistrationVerificationOptional RegistrationVerificationPolicy = "optional"
	RegistrationVerificationRequired RegistrationVerificationPolicy = "required"
)

type RemoteAppAttributeDef

type RemoteAppAttributeDef struct {
	RemoteApplicationID string
	Key                 string
	Version             int32
	Definition          json.RawMessage
}

RemoteAppAttributeDef is a remote_application's registered attribute definition: the full inline value a REFERENCE-mode delegated-token attribute resolves to (#75). Definition is opaque JSON the consuming app interprets.

type RemoteAppKey

type RemoteAppKey struct {
	KID          string `json:"kid,omitempty" yaml:"kid,omitempty"`
	PublicKeyPEM string `json:"public_key_pem" yaml:"public_key_pem"`
}

RemoteAppKey is one entry of a static-mode principal's human-managed key list (stored as jsonb; edited like an authorized_keys file).

type RemoteApplication

type RemoteApplication struct {
	ID                string
	Slug              string
	PermissionGroupID string // controlling permission-group id
	Issuer            string // OIDC iss
	JWKSURI           string // OIDC jwks_uri (jwks mode only)
	// Mode is the trust source: RemoteAppModeJWKS (fetch from JWKSURI) XOR
	// RemoteAppModeStatic (human-managed PublicKeys list). Never both.
	Mode string
	// PublicKeys is the static-mode key list (empty in jwks mode).
	PublicKeys []RemoteAppKey
	Enabled    bool
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

RemoteApplication is a registered federation principal: an external issuer authkit trusts to mint delegated/remote-application tokens. It is a plain data view; persistence and lifecycle live in core.

type RemoteApplicationAccessParams

type RemoteApplicationAccessParams struct {
	// Issuer becomes the `iss` claim: the remote_application's OIDC issuer,
	// registered with the validating resource server. Required when minting via
	// the free function; the *Service mint method defaults it to the Service's
	// configured Issuer when empty.
	Issuer string
	// Audiences becomes the `aud` claim: the target resource API(s).
	Audiences []string
	// TTL is the token lifetime. Defaults to 15m when zero.
	TTL time.Duration
	// JTI, when set, becomes the `jti` claim. Optional.
	JTI string
	// NotBefore, when set, becomes the `nbf` claim. Optional.
	NotBefore time.Time
	// Permissions, when non-nil, becomes the `permissions` claim: a DOWN-SCOPING
	// request for least-privilege (#76 amendment). The stored grant is the
	// ceiling; effective = this claim, but EVERY claimed perm must be within the
	// stored grant — an out-of-grant claimed perm REJECTS the token at verify (a
	// remote application access token can never widen). nil/absent => no claim
	// => full stored ceiling (backward-compatible with v0.28.0 tokens).
	Permissions []string
}

type RemoteApps added in v0.72.0

type RemoteApps interface {
	UpsertRemoteApplication(ctx context.Context, in RemoteApplication) (*RemoteApplication, error)
	GetRemoteApplication(ctx context.Context, issuer string) (*RemoteApplication, error)
	DeleteRemoteApplication(ctx context.Context, issuer string) error
	ListRemoteApplications(ctx context.Context, activeOnly bool) ([]RemoteApplication, error)
	ResolveRemoteApplicationAuthority(ctx context.Context, appID string) ([]string, error)
	ResolveRemoteAppAttributeDef(ctx context.Context, appID, key string, version int32) (*RemoteAppAttributeDef, error)
}

RemoteApps manages trusted remote applications (federation issuers) and resolves their stored authority.

type ResolvedAPIKey

type ResolvedAPIKey struct {
	APIKeyID string
	KeyID    string
	// PermissionGroupID is the controlling permission-group id.
	PermissionGroupID string
	Role              string
	Permissions       []string
}

ResolvedAPIKey is the API-key resolution result. Permissions is the key's role resolved to its effective permission set AT VERIFY TIME (so a role edit is reflected immediately — perms are never frozen into the key).

type Roles added in v0.72.0

type Roles interface {
	AssignRoleBySlug(ctx context.Context, userID, slug string) error
	AssignRoleBySlugAs(ctx context.Context, actorUserID, userID, slug string) error
	RemoveRoleBySlug(ctx context.Context, userID, slug string) error
	RemoveRoleBySlugAs(ctx context.Context, actorUserID, userID, slug string) error
	UpsertRoleBySlug(ctx context.Context, name, slug string, description *string) error
	ListRoleSlugsByUser(ctx context.Context, userID string) []string
	ListRoleSlugsByUserErr(ctx context.Context, userID string) ([]string, error)
}

Roles is global root-role assignment. The *As variants are the actor-checked (no-escalation) path; the plain ones are the bootstrap path.

type Senders added in v0.72.0

type Senders interface {
	HasEmailSender() bool
	HasSMSSender() bool
	SMSAvailable() bool
	CheckSMSHealth(ctx context.Context) error
}

Senders reports whether the configured message senders are available and healthy.

type ServiceJWTClaims

type ServiceJWTClaims struct {
	Issuer      string
	Subject     string
	Audiences   []string
	IssuedAt    time.Time
	NotBefore   time.Time
	ExpiresAt   time.Time
	JTI         string
	TokenUse    string
	Permissions []string
	Scope       []string
}

ServiceJWTClaims is the canonical AuthKit claim shape for caller-minted machine-to-machine JWTs. Permissions are requested capabilities; receiving services must still intersect them with server-side grants.

type ServiceJWTMintOptions

type ServiceJWTMintOptions struct {
	Subject     string
	Audiences   []string
	Permissions []string
	Lifetime    time.Duration
	NotBefore   time.Time
	IssuedAt    time.Time
	JTI         string
}

type Session

type Session struct {
	ID                  string
	FamilyID            string
	CreatedAt           time.Time
	LastAuthenticatedAt *time.Time
	LastUsedAt          time.Time
	ExpiresAt           *time.Time
	RevokedAt           *time.Time
	UserAgent           *string
	IPAddr              *string
}

Session is a sanitized session view (no tokens). Part of the wire contract.

type Sessions added in v0.72.0

type Sessions interface {
	ExchangeRefreshToken(ctx context.Context, refreshToken string, ua string, ip net.IP) (string, time.Time, string, error)
	ListUserSessions(ctx context.Context, userID string) ([]Session, error)
	RevokeAllSessions(ctx context.Context, userID string, keepSessionID *string) error
}

Sessions is the refresh-session surface: refresh-token exchange, list, and revoke-all.

type SubjectGroupMembership

type SubjectGroupMembership struct {
	Persona      string
	InstanceSlug string
	Role         string
}

type Tokens added in v0.72.0

type Tokens interface {
	IssueAccessToken(ctx context.Context, userID, email string, extra map[string]any) (string, time.Time, error)
	MintCustomJWT(ctx context.Context, opts CustomJWTMintOptions) (string, error)
	MintDelegatedAccessToken(ctx context.Context, p DelegatedAccessParams) (string, error)
	MintRemoteApplicationAccessToken(ctx context.Context, p RemoteApplicationAccessParams) (string, error)
	MintServiceJWT(ctx context.Context, opts ServiceJWTMintOptions) (string, ServiceJWTClaims, error)
}

Tokens issues the app's JWTs: access, service, delegated, remote-application, and custom.

type TwoFactorMethod added in v0.72.0

type TwoFactorMethod string

TwoFactorMethod is one second-factor channel a host enables.

const (
	TwoFactorEmail TwoFactorMethod = "email"
	TwoFactorSMS   TwoFactorMethod = "sms"
	TwoFactorTOTP  TwoFactorMethod = "totp"
)

type TwoFactorMode added in v0.72.0

type TwoFactorMode string

TwoFactorMode is the host's account-wide 2FA enrollment policy.

const (
	// TwoFactorDisabled turns 2FA off entirely: no user enrollment/challenge/
	// verify routes are usable.
	TwoFactorDisabled TwoFactorMode = "disabled"
	// TwoFactorOptional lets users enroll a second factor if they choose; an
	// un-enrolled user is not blocked from normal session use.
	TwoFactorOptional TwoFactorMode = "optional"
	// TwoFactorRequired forces every user to enroll a second factor before normal
	// session use. Existing un-enrolled users are challenged on their next
	// authenticated request (the session, not just signup, is gated).
	TwoFactorRequired TwoFactorMode = "required"
)

type User

type User struct {
	ID              string
	Email           *string // Nullable - phone-only users have NULL email
	PhoneNumber     *string
	Username        *string
	DiscordUsername *string
	EmailVerified   bool
	PhoneVerified   bool
	BannedAt        *time.Time
	BannedUntil     *time.Time
	BanReason       *string
	BannedBy        *string
	DeletedAt       *time.Time
	Biography       *string
	CreatedAt       time.Time
	UpdatedAt       time.Time
	LastLogin       *time.Time
}

User is the public user view returned by AuthKit lookups. Plain data — part of the wire contract shared by the embedded engine and (Phase 2) the remote SDK. See #138 (contract inversion): definitions live here in the lean, pgx-free contract package; internal/authcore aliases back to these.

type UserRef added in v0.66.0

type UserRef struct {
	ID       string
	Username string // "" if unset
	Email    string // "" if unset
}

UserRef is a slim user projection (id + display fields) returned by batch lookups like Client.UsersByIDs — resolving many user IDs to display data in one query, without N+1 single fetches. Part of the wire contract.

type Users added in v0.72.0

type Users interface {
	CreateUser(ctx context.Context, email, username string) (*User, error)
	GetEmailByUserID(ctx context.Context, id string) (string, error)
	GetUserByEmail(ctx context.Context, email string) (*User, error)
	GetUserByPhone(ctx context.Context, phone string) (*User, error)
	GetUserBySolanaAddress(ctx context.Context, address string) (*User, error)
	GetUserByUsername(ctx context.Context, username string) (*User, error)
	GetUserMetadata(ctx context.Context, userID string) (map[string]any, error)
	PatchUserMetadata(ctx context.Context, userID string, patch map[string]any) error
	HardDeleteUser(ctx context.Context, userID string) error
	SoftDeleteUser(ctx context.Context, id string) error
	RestoreUser(ctx context.Context, id string) error
	SetEmailVerified(ctx context.Context, id string, v bool) error
	UpdateBiography(ctx context.Context, id string, bio *string) error
	UpdateEmail(ctx context.Context, id, email string) error
	UpdateUsername(ctx context.Context, id, username string) error
	UpdateImportedUser(ctx context.Context, userID string, input ImportUserInput) (*User, error)
	ImportUsers(ctx context.Context, inputs []ImportUserInput) (ImportUsersResult, error)
	ListUsersDeletedBefore(ctx context.Context, cutoff time.Time, limit int) ([]string, error)
	TimeUntilUsernameRenameAvailable(ctx context.Context, userID string, now time.Time) (int64, error)
	IsUserAllowed(ctx context.Context, userID string) (bool, error)
	// UsersByIDs resolves many user IDs to slim display projections (id +
	// username/email) in ONE query: the batch read for "render N authors"
	// without N+1. Missing IDs are simply absent from the result. (Replaces the
	// removed authkit/identity store; writes go through UpdateUsername/UpdateEmail,
	// which enforce the rename cooldown + validation raw table writes skip.)
	UsersByIDs(ctx context.Context, ids []string) ([]UserRef, error)
}

Users is account create/read/update/delete, identity lookups, metadata, and bulk import/read.

Directories

Path Synopsis
adapters
chi
gin
Package testing provides utilities for testing applications that use authkit.
Package testing provides utilities for testing applications that use authkit.
cmd
authkit-server command
Command authkit-server is the standalone, self-hostable AuthKit server (#142).
Command authkit-server is the standalone, self-hostable AuthKit server (#142).
Re-exports of the public types, constants, sentinel errors, and helper functions implemented in internal/authcore.
Re-exports of the public types, constants, sentinel errors, and helper functions implemented in internal/authcore.
internal
db
Schema indirection (authkit issue 69).
Schema indirection (authkit issue 69).
genremote command
Command genremote generates the AuthKit remote SDK (authkit/remote) and the management-API method registry (authkit/server) from the authkit.Client interface in client.go — ONE source of truth for both transports (#142).
Command genremote generates the AuthKit remote SDK (authkit/remote) and the management-API method registry (authkit/server) from the authkit.Client interface in client.go — ONE source of truth for both transports (#142).
migrations
postgres
Package migrations embeds AuthKit's Postgres schema migrations.
Package migrations embeds AuthKit's Postgres schema migrations.
Package remote is the AuthKit remote SDK: a Go client that talks to a standalone AuthKit server's management API over HTTP and satisfies the SAME authkit.Client contract an in-process embedded.Client does (#142), so a host swaps embedded↔remote with one construction line:
Package remote is the AuthKit remote SDK: a Go client that talks to a standalone AuthKit server's management API over HTTP and satisfies the SAME authkit.Client contract an in-process embedded.Client does (#142), so a host swaps embedded↔remote with one construction line:
Package server hosts the AuthKit management HTTP API — the wire contract a standalone AuthKit server exposes and the authkit/remote SDK consumes (#142).
Package server hosts the AuthKit management HTTP API — the wire contract a standalone AuthKit server exposes and the authkit/remote SDK consumes (#142).
Package siws implements Sign In With Solana (SIWS) authentication.
Package siws implements Sign In With Solana (SIWS) authentication.
storage

Jump to

Keyboard shortcuts

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