Documentation
¶
Overview ¶
Package kal @notice Authentication and authorization for gqlgen applications on Postgres, as an embedded library rather than a service.
@dev Three positions, each argued against how the rest of the ecosystem does it:
**Sessions in your database.** Opaque server-side sessions, so revoking one is an UPDATE and "log out everywhere" is one statement. Kratos, SuperTokens and Zitadel take your users table into a separate service, which costs you the JOIN, the shared transaction, and a second backup and migration story. Because the session cookie is the long-lived credential, kal ships no refresh token at all — the rotating-family subsystem every JWT-first library must build does not exist here.
**Identity in the request context.** The middleware is net/http, mounted inside luima's adaptor, so a resolver reads a typed Principal from its own ctx. Anonymous is not an error: one endpoint serves public and private fields in the same document, and the graph decides.
**Authorization in the WHERE clause.** Scope composes the caller's ownership predicate into the statement. A policy engine answers "may Alice read document 7"; it does not stop a list query returning everything, and it cannot apply to a DELETE without a read-then-check round trip that has a TOCTOU window.
The packages ¶
This package re-exports the four below, so the common case needs one import:
[github.com/ulas96/kal/authn] passwords, registration, login, recovery [github.com/ulas96/kal/authz] Principal, @auth, Scope, coverage, RLS [github.com/ulas96/kal/session] sessions, the cookie, the middleware, the JWT leg [github.com/ulas96/kal/kalerr] the error contract [github.com/ulas96/kal/migrations] the schema, as .sql behind an embed.FS
The types below are aliases, not copies, so the two spellings are interchangeable. The cost, stated plainly: a genuinely new sub-package export is invisible from here until it is added by hand, and tests/ asserts the identity of the ones that exist.
Wiring ¶
Requires luima ≥ 0.2.0 for the HTTPMiddleware and Configure seams:
auth, err := kal.New(kal.Config{
DB: db,
BaseURL: "https://app.example.com",
Mailer: myMailer,
})
app := luima.New(luima.Config{
Schema: generated.NewExecutableSchema(c),
HTTPMiddleware: []func(http.Handler) http.Handler{auth.Middleware()},
Configure: auth.Configure(),
ErrorPresenter: kal.PresentError,
})
with `c.Directives.Auth = auth.Directive()` and authz.DirectiveSDL pasted into the schema.
Index ¶
- Constants
- func AssertAuthCoverage(schema graphql.ExecutableSchema, exempt ...string) error
- func AssertDirectivesWired(directiveRoot any) error
- func HasRole(ctx context.Context, role string) bool
- func PresentError(ctx context.Context, err error) *gqlerror.Error
- func Scope(ctx context.Context, column string) func(*orm.Query) *orm.Query
- func SetupZK(kind ZKCircuit, pkw, vkw io.Writer) error
- func ValidatePassword(password string) error
- func ZKCircuitInfo(kind ZKCircuit) (int, [32]byte, error)
- func ZKKnowledgeValid(w ZKKnowledgeWitness) bool
- func ZKMembershipValid(w ZKMembershipWitness) bool
- func ZKProofSize() int
- type Auth
- type AuthLevel
- type Config
- type Error
- type LogMailer
- type Mailer
- type Message
- type Params
- type Principal
- type SessionInfo
- type ZKCircuit
- type ZKClaim
- type ZKClaimKind
- type ZKClaims
- type ZKConfig
- type ZKCredential
- type ZKField
- func NewZKAudience(deployment, policy, epoch string) ZKField
- func ZKChallengeField(token string) (ZKField, error)
- func ZKKnowledgeCommitment(secret ZKSecret) (ZKField, error)
- func ZKMembershipCommitment(secret ZKSecret, attribute uint64) (ZKField, error)
- func ZKNullifier(secret ZKSecret, audience ZKField) (ZKField, error)
- type ZKKnowledgeCircuit
- type ZKKnowledgeRequest
- type ZKKnowledgeWitness
- type ZKMembershipCircuit
- type ZKMembershipPublic
- type ZKMembershipRequest
- type ZKMembershipWitness
- type ZKMerklePath
- type ZKOptions
- type ZKProvingKey
- type ZKSecret
- type ZKService
- type ZKVerifiedClaim
- type ZKVerifyingKey
Constants ¶
const ( // DefaultMaxAliases @notice Selections allowed in one document. DefaultMaxAliases = 100 // DefaultMaxDepth @notice Nesting allowed in one document. // // @dev luima's ComplexityLimit does not cover this: gqlgen's complexity is per selected // field, so 400 levels of nesting through a cyclic schema costs about 400 and sails past a // 1000 limit. DefaultMaxDepth = 15 )
Guard defaults. Every one of them is a limit on the document, not on the request, because GraphQL executes many operations per request and an HTTP-request rate limiter counts none of them.
const ( LevelAnonymous = authz.LevelAnonymous LevelAuthenticated = authz.LevelAuthenticated ZKCircuitKnowledge = zkauthn.CircuitKnowledge ZKCircuitMembership = zkauthn.CircuitMembership ZKClaimRecurring = zkauthn.ClaimRecurring ZKClaimOneShot = zkauthn.ClaimOneShot ZKMerkleDepth = zkauthn.MerkleDepth ZKSecretSize = zkauthn.SecretSize ZKKnowledgeConstraints = zkauthn.KnowledgeConstraints ZKMembershipConstraints = zkauthn.MembershipConstraints ZKKnowledgeCircuitID = zkauthn.KnowledgeCircuitID ZKMembershipCircuitID = zkauthn.MembershipCircuitID )
The AuthLevel values, re-exported so a consumer need not import authz for a switch.
Variables ¶
This section is empty.
Functions ¶
func AssertAuthCoverage ¶
func AssertAuthCoverage(schema graphql.ExecutableSchema, exempt ...string) error
AssertAuthCoverage @notice Fails if any field is reachable without an @auth annotation. Call it from a test. See authz.AssertAuthCoverage.
func AssertDirectivesWired ¶
AssertDirectivesWired @notice Fails if any directive implementation is nil. See authz.AssertDirectivesWired.
func HasRole ¶
HasRole @notice Whether the caller holds the named role. See authz.HasRole.
func PresentError ¶
PresentError @notice luima's presenter plus an extensions.code for kal's errors. Set it as luima's Config.ErrorPresenter. See kalerr.PresentError.
func Scope ¶
Scope @notice The caller's ownership predicate, for luima's crud options. See authz.Scope.
func SetupZK ¶ added in v0.2.0
SetupZK @notice Runs setup for one kal ZK circuit. See zkauthn.Setup.
func ValidatePassword ¶
ValidatePassword @notice Applies the password policy. See authn.ValidatePassword.
func ZKKnowledgeValid ¶ added in v0.2.0
func ZKKnowledgeValid(w ZKKnowledgeWitness) bool
func ZKMembershipValid ¶ added in v0.2.0
func ZKMembershipValid(w ZKMembershipWitness) bool
func ZKProofSize ¶ added in v0.2.0
func ZKProofSize() int
Types ¶
type Auth ¶
type Auth struct {
// Sessions @notice Issue, look up, rotate, revoke and list sessions.
Sessions *session.Sessions
// Accounts @notice Register, log in, recover, change a password.
Accounts *authn.Accounts
// Roles @notice Grant and revoke role membership.
Roles *authz.Roles
// Hasher @notice Password hashing, for importing an existing user base.
Hasher *authn.Hasher
// JWT @notice The optional bearer-token leg. Nil unless JWTIssuer was set.
JWT *session.JWT
// ZK @notice The optional proof and credential service. Nil unless Config.ZK was set.
ZK *zkauthn.ZK
// contains filtered or unexported fields
}
Auth @notice Everything kal exposes to an application, wired and validated.
func New ¶
New @notice Validates the configuration and wires everything up.
@dev Fails loudly on anything that cannot have a safe default. Every other field has one, and every default is the production posture.
@param cfg the configuration; DB, BaseURL and Mailer are required @return *Auth the wired library @return error a description of what is missing or contradictory
func (*Auth) Configure ¶
Configure @notice Applies kal's gqlgen extensions: the anti-batching guard, conditional introspection, and suggestion suppression.
Pass it to luima's Config.Configure (luima ≥ 0.2.0).
@dev SetDisableSuggestion is here rather than optional because gqlparser's "Did you mean …?" text passes straight through luima's presenter by design, so a caller guessing a field name still learns the real one with introspection off.
@return func(*handler.Server) applied immediately before the handler is mounted
func (*Auth) Directive ¶
func (a *Auth) Directive() func(context.Context, any, graphql.Resolver, AuthLevel, []string, *bool, []string) (any, error)
Directive @notice The @auth implementation for your generated DirectiveRoot.
c.Directives.Auth = auth.Directive()
Paste authz.DirectiveSDL into your schema for the matching declaration.
func (*Auth) Middleware ¶
Middleware @notice The net/http middleware that resolves the session cookie into a Principal, carries the cookie jar, and enforces the cross-site transport guard.
Pass it to luima's Config.HTTPMiddleware (luima ≥ 0.2.0), or mount it in any net/http stack.
@return func(http.Handler) http.Handler outermost-first middleware
func (*Auth) Migrate ¶
Migrate @notice Applies every embedded migration in order.
@dev A convenience, not a migration framework: it runs the .sql files and nothing else — no version table, no down migrations, no locking. Applications with their own tooling should feed it migrations.FS instead. Each file is idempotent only in the sense that a second run fails loudly on the existing tables rather than corrupting them.
@param ctx the context for the statements @return error the first failure, naming the file
type AuthLevel ¶
AuthLevel @notice The @auth directive's requires argument. See authz.AuthLevel.
type Config ¶
type Config struct {
// DB @notice The pool everything runs on. Required.
DB *pg.DB
// BaseURL @notice The origin every emailed link is built under. Required.
//
// @dev Cannot be derived from the request: a link origin taken from the Host header is
// Host-header injection, and a password-reset email is the last place to accept
// attacker-controlled input. Must be https outside loopback.
BaseURL string
// Mailer @notice Delivers verification, reset and invite messages. Required.
//
// @dev No default, and deliberately no silent no-op: "I forgot to configure email" must
// fail at construction, not at 3am when nobody can reset a password. [LogMailer] is the
// development answer, and its name says not to ship it.
Mailer Mailer
// TableSchema @notice Postgres schema holding the auth_* tables. Empty means search_path.
TableSchema string
// IdleTimeout @notice Session inactivity timeout. Default 12h.
IdleTimeout time.Duration
// AbsoluteTimeout @notice Hard session lifetime, never extended. Default 14d.
AbsoluteTimeout time.Duration
// CookieName @notice The session cookie. Default "__Host-kal_session".
//
// @dev Change it only to run two kal instances on one origin. Keep the __Host- prefix: it
// is what stops a sibling subdomain overwriting the cookie.
CookieName string
// Argon2 @notice Password hashing parameters. Zero fields take the OWASP defaults.
Argon2 authn.Params
// MaxConcurrentHashes @notice In-flight Argon2 ceiling. Zero means GOMAXPROCS.
//
// @dev Each hash holds ~19 MiB, so this is what stops concurrent logins from becoming a
// remote OOM. Per replica: behind N replicas the real ceiling is N times this.
MaxConcurrentHashes int64
// BypassRole @notice A role for which [Scope] is a no-op and any @auth roles requirement
// is satisfied. Empty means none — there is no implicit "admin".
BypassRole string
// MFAWindow @notice How recently MFA must have been satisfied for @auth(mfa: true).
// Default 15m.
MFAWindow time.Duration
// AllowUnverifiedLogin @notice Lets accounts log in before verifying their email. Off by
// default.
AllowUnverifiedLogin bool
// ClientIP @notice How to attribute a request to a client address. Default: the host part
// of RemoteAddr.
//
// @dev Not X-Forwarded-For by default — that header is client-supplied unless a trusted
// proxy overwrites it, and a spoofable address turns per-IP rate limiting into a bypass.
ClientIP func(*http.Request) string
// AllowIntrospection @notice Decides per request whether introspection is answered. Nil
// means never.
//
// @dev luima turns introspection on and, since 0.2.0, offers Config.DisableIntrospection to
// turn it off again — an all-or-nothing deploy-time switch. This is the per-request form, so
// it can be role-gated: func(ctx) bool { return authz.HasRole(ctx, "admin") }. Off by
// default, because the zero Config is the production posture.
AllowIntrospection func(context.Context) bool
// SensitiveFields @notice Fields that may be selected at most once per document. Nil takes
// kal's defaults (login, register, the recovery mutations).
//
// @dev Set this if your login mutation has another name, or the aliasing guard protects
// nothing.
SensitiveFields []string
// MaxAliases @notice Selections allowed per document. Zero means 100. Negative disables.
MaxAliases int
// MaxDepth @notice Nesting allowed per document. Zero means 15. Negative disables.
MaxDepth int
// JWTIssuer @notice The iss claim for the optional JWT leg. Empty disables it.
JWTIssuer string
// JWTKeys @notice Ed25519 signing keys, newest first. Required when JWTIssuer is set.
//
// @dev Two keys make rotation a deploy rather than an outage: the first signs, all verify.
JWTKeys []ed25519.PrivateKey
// ZK @notice Optional zero-knowledge MFA, membership login and proven claims. Nil keeps
// today's authentication and dependency behavior at runtime.
ZK *ZKConfig
}
Config @notice Assembles kal.
@dev The zero value is the good *production* configuration, and there is no development mode that weakens a security property. This is a deliberate inversion of luima's invariant, where a zero Config is the good development configuration with the playground and introspection on. For an auth library that polarity is wrong: a Dev bool that relaxes a cookie attribute or skips a check is a vulnerability shipped as a convenience, and it reaches production, because that is what environment flags do. Anything a developer needs is an ordinary field with an obvious name.
type Error ¶
Error @notice A client-visible auth error with a stable code. See kalerr.Error.
type LogMailer ¶
LogMailer @notice A development Mailer that logs messages. See authn.LogMailer.
type Mailer ¶
Mailer @notice Delivers kal's transactional messages. See authn.Mailer.
type Principal ¶
Principal @notice The authenticated caller. See authz.Principal.
func From ¶
From @notice Returns the caller, and whether there is one. See authz.From.
type SessionInfo ¶
SessionInfo @notice One live session, as shown to its owner. See session.Info.
type ZKClaimKind ¶ added in v0.2.0
type ZKClaims ¶ added in v0.2.0
func NewZKClaims ¶ added in v0.2.0
type ZKConfig ¶ added in v0.2.0
type ZKConfig struct {
KnowledgeVerifyingKey io.Reader
KnowledgeVerifyingKeySHA256 []byte
MembershipVerifyingKey io.Reader
MembershipVerifyingKeySHA256 []byte
RootGrace time.Duration
MaxConcurrentVerifications int64
}
ZKConfig @notice Enables Groth16 knowledge and anonymous-membership authentication.
@dev The operator supplies both verifying keys from read-only storage and pins their hashes in application source. Proving keys are never loaded by the server or committed with kal.
type ZKCredential ¶ added in v0.2.0
type ZKCredential = zkauthn.Credential
type ZKField ¶ added in v0.2.0
ZKField @notice A canonical BN254 scalar encoding. See zkauthn.Field.
func NewZKAudience ¶ added in v0.2.0
func ZKChallengeField ¶ added in v0.2.0
func ZKKnowledgeCommitment ¶ added in v0.2.0
func ZKMembershipCommitment ¶ added in v0.2.0
type ZKKnowledgeCircuit ¶ added in v0.2.0
type ZKKnowledgeCircuit = zkauthn.KnowledgeCircuit
type ZKKnowledgeRequest ¶ added in v0.2.0
type ZKKnowledgeRequest = zkauthn.KnowledgeRequest
type ZKKnowledgeWitness ¶ added in v0.2.0
type ZKKnowledgeWitness = zkauthn.KnowledgeWitness
type ZKMembershipCircuit ¶ added in v0.2.0
type ZKMembershipCircuit = zkauthn.MembershipCircuit
type ZKMembershipPublic ¶ added in v0.2.0
type ZKMembershipPublic = zkauthn.MembershipPublic
type ZKMembershipRequest ¶ added in v0.2.0
type ZKMembershipRequest = zkauthn.MembershipRequest
type ZKMembershipWitness ¶ added in v0.2.0
type ZKMembershipWitness = zkauthn.MembershipWitness
func ZKMembershipWitnessFor ¶ added in v0.2.0
func ZKMembershipWitnessFor(credential ZKCredential, path ZKMerklePath, claim ZKClaim, nullifier, challenge ZKField) ZKMembershipWitness
type ZKMerklePath ¶ added in v0.2.0
type ZKMerklePath = zkauthn.MerklePath
func ZKSingleLeafPath ¶ added in v0.2.0
func ZKSingleLeafPath(commitment ZKField) (ZKMerklePath, error)
type ZKProvingKey ¶ added in v0.2.0
type ZKProvingKey = zkauthn.ProvingKey
func LoadZKProvingKey ¶ added in v0.2.0
func LoadZKProvingKey(kind ZKCircuit, r io.Reader) (*ZKProvingKey, error)
type ZKSecret ¶ added in v0.2.0
ZKSecret @notice A generated 31-byte proof secret. See zkauthn.Secret.
type ZKVerifiedClaim ¶ added in v0.2.0
type ZKVerifiedClaim = zkauthn.VerifiedClaim
type ZKVerifyingKey ¶ added in v0.2.0
type ZKVerifyingKey = zkauthn.VerifyingKey
func LoadZKVerifyingKey ¶ added in v0.2.0
Directories
¶
| Path | Synopsis |
|---|---|
|
Package authn @notice Proving who a caller is.
|
Package authn @notice Proving who a caller is. |
|
Package authz @notice Who the caller is: the Principal, carried in the request context.
|
Package authz @notice Who the caller is: the Principal, carried in the request context. |
|
Package kalerr @notice kal's error contract: a client-visible auth error with a stable machine-readable code, and the presenter that puts it on the wire.
|
Package kalerr @notice kal's error contract: a client-visible auth error with a stable machine-readable code, and the presenter that puts it on the wire. |
|
Package migrations @notice The auth schema, as plain SQL behind an embed.FS.
|
Package migrations @notice The auth schema, as plain SQL behind an embed.FS. |
|
Package session @notice Opaque server-side sessions in Postgres — kal's primary credential.
|
Package session @notice Opaque server-side sessions in Postgres — kal's primary credential. |
|
Package zkauthn @notice Groth16 authentication and anonymous authorization over BN254.
|
Package zkauthn @notice Groth16 authentication and anonymous authorization over BN254. |
|
Package zkauthz @notice Request-local authorization for claims verified by zkauthn.
|
Package zkauthz @notice Request-local authorization for claims verified by zkauthn. |