authowl

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 18 Imported by: 0

README

AuthOwl Go SDK

Complete Go guide · JWT issuer setup

Server-side SDK for AuthOwl, the multi-tenant auth SaaS.

Zero dependencies. Every primitive it needs — P-256 ECDSA, HMAC-SHA256, JSON, HTTP — is in the standard library. Backend auth is where a surprise transitive dependency is least welcome.

go get github.com/authowl/authowl-sdk/sdks/go

This package is the relying-party side of AuthOwl. It never signs anyone in: your frontend authenticates against the AuthOwl server directly, and this SDK validates what arrives at your backend.

Verify a token

projectID := "2f1c9a84-..."
issuer := "https://api.authowl.dev/api/projects/" + projectID + "/auth"

verifier := &authowl.Verifier{
    Issuer:   issuer,
    Audience: projectID,
    Keys:     authowl.NewRemoteKeySource(issuer + "/jwks"),
}

verified, err := verifier.Verify(ctx, token)
if err != nil {
    log.Printf("denied: %s", authowl.CodeOf(err))
    return
}
fmt.Println(verified.Subject, verified.Membership)

RemoteKeySource caches the JWKS for five minutes and survives key rotation by forcing a single rate-limited refetch when it meets an unknown kid.

Authorize a request

Has is the real authorization primitive. It fails closed: an invalid, tampered, expired, or wrong-audience token returns false, not an error.

ok, err := verifier.Has(ctx, token, authowl.Query{
    Permission: authowl.Match("org:billing:read"),
})
if err != nil {
    // Only a CONFIGURATION mistake reaches here - a backend that silently denies
    // every request because of a missing env var is far worse to debug.
    http.Error(w, "auth misconfigured", http.StatusInternalServerError)
    return
}
if !ok {
    http.Error(w, "forbidden", http.StatusForbidden)
    return
}

Every present field in a Query must hold (AND). Use Match so an explicit empty value remains a criterion and fails closed instead of looking omitted:

authowl.Query{
    Role:       authowl.Match("admin"),
    Permission: authowl.Match("org:billing:read"),
    TeamID:     authowl.Match("team_alpha"),
}

TeamID checks group membership, not authority — teams grant nothing on their own, and a token minted before teams shipped can never satisfy a TeamID query.

// `secure` must reflect the SERVER's cookie mode - derive it from the API URL
// scheme (https => true), not from the incoming request.
name := authowl.SessionCookieName(projectID, true)
cookie, err := r.Cookie(name)

Verify a webhook

body, _ := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))

ok, err := authowl.VerifyWebhook(authowl.WebhookInput{
    // The EXACT request bytes: re-serializing the JSON reorders keys and breaks the HMAC.
    RawBody:         body,
    Timestamp:       r.Header.Get("authowl-timestamp"),
    SignatureHeader: r.Header.Get("authowl-signature"),
    Secrets:         []string{current, previous}, // both, during rotation overlap
    Now:             time.Now().Unix(),
})

err is non-nil only for invalid local configuration; anything wrong with the untrusted request returns (false, nil).

Error codes

CodeOf(err) returns a code shared verbatim with every other AuthOwl SDK, so a Go log line means the same thing as a TypeScript one. Match on the code, never the message.

Conformance

go test ./...

Runs the shared 125-vector corpus from conformance/vectors. If a case fails, this implementation has diverged from the contract — the fix belongs in the code, not the vector. See conformance/README.md.

License

MIT

Documentation

Index

Constants

View Source
const DefaultClockTolerance = 60 * time.Second

DefaultClockTolerance is the skew allowed on exp/nbf when none is configured.

View Source
const MaxClockTolerance = 300 * time.Second

MaxClockTolerance caps the configurable skew. A tolerance larger than this keeps revoked tokens alive too long to be called authorization.

Variables

View Source
var ErrPublishableKeyRequired = errors.New("authowl: publishableKey is required")

ErrPublishableKeyRequired is returned for an empty key.

View Source
var ErrSecretKeySupplied = errors.New(
	"authowl: a secret key was passed where a publishable key was expected; " +
		"never embed secret keys in client code",
)

ErrSecretKeySupplied is returned when a secret key reaches a function that wants a publishable one. This check runs BEFORE any shape validation and is a hard rule across every AuthOwl SDK: a leaked `sk_` key is a full compromise of the project, so it must never be quietly accepted as a publishable key.

View Source
var ErrWebhookConfig = errors.New("authowl: invalid webhook verification config")

ErrWebhookConfig reports invalid LOCAL configuration - bad secrets or an out-of-range tolerance. It is deliberately an error rather than a false return: untrusted input should be rejected quietly, but a misconfigured endpoint silently dropping every delivery is a bug that must be loud.

Functions

func ClockSkew

func ClockSkew(d time.Duration) *time.Duration

ClockSkew returns a pointer for Verifier.ClockTolerance.

func Match

func Match(value string) *string

Match marks a Query criterion as present. An empty value remains present and therefore fails closed instead of being treated as omitted.

func SessionCookieName

func SessionCookieName(projectID string, secure bool) string

SessionCookieName returns the exact session-cookie name the AuthOwl server sets for a project.

dev  (http):  p_<idNoDashes>.session_token
prod (https): __Secure-p_<idNoDashes>.session_token

Note the DOT joining the prefix and the name, and the `__Secure-` (not `__Host-`) prefix - both are easy to get wrong by hand, and getting them wrong means reading a cookie the server never set. `secure` must reflect the SERVER's cookie mode: derive it from the API URL's scheme (https => true).

func Tolerance

func Tolerance(seconds int) *int

Tolerance returns a pointer for WebhookInput.ToleranceSeconds.

func VerifyWebhook

func VerifyWebhook(input WebhookInput) (bool, error)

VerifyWebhook reports whether a delivery carries a valid signature.

Returns (false, nil) for anything wrong with the untrusted request, and a non-nil error only for invalid local configuration.

Types

type Environment

type Environment string

Environment is the deployment a publishable key belongs to.

const (
	EnvironmentLive Environment = "live"
	EnvironmentTest Environment = "test"
)

type ErrorCode

type ErrorCode string

ErrorCode identifies why a token was refused. The codes are shared verbatim with every other AuthOwl SDK (see conformance/vectors/jwt-verify.json), so a log line from the Go SDK means the same thing as one from the TypeScript SDK.

const (
	ErrTokenVerificationFailed   ErrorCode = "TOKEN_VERIFICATION_FAILED"
	ErrTokenConfigInvalid        ErrorCode = "TOKEN_CONFIG_INVALID"
	ErrTokenMalformed            ErrorCode = "TOKEN_MALFORMED"
	ErrTokenAlgorithmUnsupported ErrorCode = "TOKEN_ALGORITHM_UNSUPPORTED"
	ErrTokenSignatureInvalid     ErrorCode = "TOKEN_SIGNATURE_INVALID"
	ErrTokenClaimInvalid         ErrorCode = "TOKEN_CLAIM_INVALID"
	ErrJWKSFetchFailed           ErrorCode = "JWKS_FETCH_FAILED"
	ErrJWKSFetchTimeout          ErrorCode = "JWKS_FETCH_TIMEOUT"
	ErrJWKSHTTPError             ErrorCode = "JWKS_HTTP_ERROR"
	ErrJWKSResponseTooLarge      ErrorCode = "JWKS_RESPONSE_TOO_LARGE"
	ErrJWKSDocumentInvalid       ErrorCode = "JWKS_DOCUMENT_INVALID"
	ErrJWKSTooManyKeys           ErrorCode = "JWKS_TOO_MANY_KEYS"
	ErrJWKSKeyInvalid            ErrorCode = "JWKS_KEY_INVALID"
	ErrJWKSDuplicateKID          ErrorCode = "JWKS_DUPLICATE_KID"
	ErrJWKSKeyNotFound           ErrorCode = "JWKS_KEY_NOT_FOUND"
)

func CodeOf

func CodeOf(err error) ErrorCode

CodeOf reports the AuthOwl error code carried by err, or "" if err did not come from this package. Handy for structured logging and metrics.

type JWK

type JWK struct {
	Alg string
	Crv string
	Kid string
	Kty string
	Use string
	X   string
	Y   string
	// contains filtered or unexported fields
}

JWK is a published ES256 verification key.

func ParseJWKS

func ParseJWKS(document []byte) ([]*JWK, error)

ParseJWKS validates a JWKS document and returns its verification keys.

The document must be an object whose ONLY member is a `keys` array - extra top-level members are refused, not ignored, so an issuer cannot slip verifier-affecting metadata past this parser.

func (*JWK) PublicKey

func (k *JWK) PublicKey() *ecdsa.PublicKey

PublicKey returns the parsed P-256 public key.

type KeySource

type KeySource interface {
	// ResolveKey returns the key for kid, or the first published key when kid is
	// empty. It must return a *VerificationError with ErrJWKSKeyNotFound when no
	// key matches.
	ResolveKey(ctx context.Context, kid string) (*JWK, error)
}

KeySource resolves the verification key named by a token's `kid`.

type Membership

type Membership struct {
	// Role is the member's canonical role key (owner/admin/member or a project role).
	Role string `json:"role"`
	// Permissions holds the effective permission ids: `org:sys_*` system claims
	// plus the operator's custom `org:<feature>:<action>` ids.
	Permissions []string `json:"permissions"`
	// Teams are the team ids held inside the ACTIVE organization.
	//
	// Nil (not empty) when the token carries no `teams` claim at all, which is
	// what a token minted before teams shipped looks like. HasTeam then reports
	// false rather than guessing: an absent claim is never read as "any team".
	Teams []string `json:"teams,omitempty"`
}

Membership is the active-organization membership carried by a verified token.

func (*Membership) Has

func (m *Membership) Has(query Query) bool

Has reports whether the membership satisfies EVERY criterion in the query. Returns false when there is no membership, and false for an empty query - asking nothing never grants anything.

func (*Membership) HasPermission

func (m *Membership) HasPermission(permission string) bool

HasPermission reports whether the membership's permission claim includes permission. Exact match only - no prefix or substring matching.

func (*Membership) HasTeam

func (m *Membership) HasTeam(teamID string) bool

HasTeam reports whether the membership's team claim includes teamID.

Teams are pure grouping: belonging to one grants nothing on its own, so this is for the application's own gating, never an authority check.

type PublishableKey

type PublishableKey struct {
	Prefix    string
	Env       Environment
	ProjectID string
}

PublishableKey is the decoded form of a pk_live_… / pk_test_… key.

func DecodePublishableKey

func DecodePublishableKey(key string) (PublishableKey, error)

DecodePublishableKey validates a publishable key and extracts its project id.

type Query

type Query struct {
	Role       *string `json:"role,omitempty"`
	Permission *string `json:"permission,omitempty"`
	TeamID     *string `json:"teamId,omitempty"`
}

Query is a Clerk-style has() query. Every present field must hold (AND). Pointers preserve the difference between an omitted criterion and an explicit empty string, which must deny rather than silently disappearing.

type RemoteKeySource

type RemoteKeySource struct {
	URI    string
	Client *http.Client
	// Now is injectable for deterministic tests. Nil means time.Now.
	Now func() time.Time
	// contains filtered or unexported fields
}

RemoteKeySource fetches and caches a project's published JWKS.

An unknown `kid` may be a freshly rotated key, so it forces ONE cache-bypassing refetch to try to pick it up. That forced refetch is rate-limited: a flood of bogus-kid tokens must not turn into a flood of outbound JWKS requests, which would be a cheap amplification lever against the issuer. Legitimate rotation is unaffected - the server keeps signing with the old kid long enough for the normal TTL refresh to carry the new one.

func NewRemoteKeySource

func NewRemoteKeySource(uri string) *RemoteKeySource

NewRemoteKeySource returns a key source that reads the project's JWKS URL.

func (*RemoteKeySource) ResolveKey

func (r *RemoteKeySource) ResolveKey(ctx context.Context, kid string) (*JWK, error)

ResolveKey implements KeySource.

type StaticKeySource

type StaticKeySource struct{ Keys []*JWK }

StaticKeySource serves a fixed key set. Use it in tests, or when keys are provisioned out of band rather than fetched.

func NewStaticKeySource

func NewStaticKeySource(document []byte) (*StaticKeySource, error)

NewStaticKeySource parses a JWKS document once and serves it forever.

func (*StaticKeySource) ResolveKey

func (s *StaticKeySource) ResolveKey(_ context.Context, kid string) (*JWK, error)

ResolveKey implements KeySource.

type VerificationError

type VerificationError struct {
	Code    ErrorCode
	Message string
}

VerificationError is returned by Verify and the JWKS parser. Match on Code, never on Message: the codes are contractual, the messages are not.

func (*VerificationError) Error

func (e *VerificationError) Error() string

type VerifiedToken

type VerifiedToken struct {
	// Subject is the signed-in user id, or "" when the token carries no `sub`.
	Subject string
	// Membership is the active-org membership, or nil when the token carries none.
	Membership *Membership
	// Claims is the full verified claim set, for callers reading extra claims.
	Claims map[string]any
}

VerifiedToken is the result of a successful verification.

type Verifier

type Verifier struct {
	// Issuer is the expected `iss`: the project's AuthOwl auth base URL.
	Issuer string
	// Audience is the expected `aud`: the project id.
	Audience string
	// Keys resolves verification keys. Use NewRemoteKeySource in production.
	Keys KeySource
	// ClockTolerance bounds exp/nbf skew, from 0 through MaxClockTolerance.
	//
	// A POINTER so "unset" and "explicitly zero" stay distinct: nil means
	// DefaultClockTolerance, while ClockSkew(0) demands an exact match. A plain
	// duration would let Go's zero value turn the strictest setting into the
	// default one.
	ClockTolerance *time.Duration
	// Now is injectable for deterministic tests. Nil means time.Now.
	Now func() time.Time
}

Verifier performs stateless verification of AuthOwl project JWTs.

This is the REAL server-side authorization primitive. It verifies the ES256 signature against the project's published JWKS and checks issuer, audience, and expiry BEFORE reading any claim, so no permission is ever granted off an unverified claim.

func (*Verifier) Has

func (v *Verifier) Has(ctx context.Context, token string, query Query) (bool, error)

Has verifies the token and evaluates the query against its membership.

Fails CLOSED: an invalid, tampered, expired, or wrong-audience token returns false rather than an error, so a caller that ignores errors still denies. Configuration mistakes are the exception - those return an error, because a misconfigured backend silently denying every request is far worse to debug.

func (*Verifier) HasPermission

func (v *Verifier) HasPermission(ctx context.Context, token, permission string) (bool, error)

HasPermission verifies the token and reports whether it grants permission. Fails closed exactly like Has.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, token string) (*VerifiedToken, error)

Verify checks a project JWT and returns its subject, membership, and claims.

Checks run in a deliberate order - structure, algorithm, key, signature, then claims - so a token with a bad signature always reports as a signature failure even when its claims are also invalid.

type WebhookInput

type WebhookInput struct {
	// RawBody must be the EXACT request bytes. Do not parse and re-serialize the
	// JSON before verifying - re-serialization reorders keys and breaks the HMAC.
	RawBody []byte
	// Timestamp is the delivery's timestamp header, as a string.
	Timestamp string
	// SignatureHeader is the delivery's signature header (`v1=<hex>`, comma separated).
	SignatureHeader string
	// Secrets holds the current and, during rotation overlap, the previous secret.
	Secrets []string
	// Now is the current time in Unix seconds.
	Now int64
	// ToleranceSeconds bounds accepted clock skew, from 0 through 3600.
	//
	// A POINTER so that "unset" and "explicitly zero" stay distinct: nil means
	// the 300-second default, while Tolerance(0) demands an exact-second match.
	// With a plain int, Go's zero value would silently turn the strictest
	// setting into the loosest one.
	ToleranceSeconds *int
}

WebhookInput is one delivery to verify.

Jump to

Keyboard shortcuts

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