gae

package module
v0.1.35 Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Index

Constants

View Source
const (
	KindUser         = "User"
	KindIdentity     = "Identity"
	KindChannel      = "Channel"
	KindAuthToken    = "AuthToken"
	KindRefreshToken = "RefreshToken"
	KindAPIKey       = "APIKey"
	KindUsername     = "Username"
)

Kind constants for Datastore entities

View Source
const KindAppRegistration = "AppRegistration"
View Source
const KindDeviceAuthorization = "DeviceAuthorization"

KindDeviceAuthorization is the Datastore Kind for RFC 8628 device authorization records. Exported so tests can drive bulk cleanup without poking the entity name string from inside the test setup.

View Source
const KindKidKey = "KidKey"
View Source
const KindSigningKey = "SigningKey"

Variables

This section is empty.

Functions

This section is empty.

Types

type APIKeyEntity

type APIKeyEntity struct {
	Key        *datastore.Key `datastore:"__key__"` // Key is the KeyID
	KeyHash    string         `datastore:"key_hash,noindex"`
	Subject    string         `datastore:"subject"`
	Name       string         `datastore:"name"`
	Scopes     []byte         `datastore:"scopes,noindex"` // JSON encoded
	CreatedAt  time.Time      `datastore:"created_at"`
	ExpiresAt  time.Time      `datastore:"expires_at,omitempty"`
	HasExpiry  bool           `datastore:"has_expiry"`
	LastUsedAt time.Time      `datastore:"last_used_at"`
	RevokedAt  time.Time      `datastore:"revoked_at,omitempty"`
	Revoked    bool           `datastore:"revoked"`
}

APIKeyEntity is the Datastore entity for API keys

type APIKeyStore

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

APIKeyStore implements core.APIKeyStore using Google Cloud Datastore

func NewAPIKeyStore

func NewAPIKeyStore(client *datastore.Client, namespace string) *APIKeyStore

NewAPIKeyStore creates a new Datastore-backed APIKeyStore

func (*APIKeyStore) CreateAPIKey

CreateAPIKey mints a new key. The KeyID (the entity key) is returned in FullKey concatenated with the secret as "<keyID>_<secret>" — only the bcrypt hash of the secret is persisted, so the raw secret can never be retrieved again. Callers must surface FullKey to the end-user immediately; losing it means rotating. HasExpiry tracks whether the caller supplied an ExpiresAt — needed because Datastore stores a zero time.Time identically to a not-set value, but the contract distinguishes "never expires" from "set to 1970-01-01."

func (*APIKeyStore) GetAPIKeyByID

GetAPIKeyByID returns the metadata row for req.KeyID, or core.ErrAPIKeyNotFound when absent. Does NOT validate secret, revocation, or expiry — use ValidateAPIKey for the full check.

func (*APIKeyStore) ListSubjectAPIKeys added in v0.1.3

ListSubjectAPIKeys returns every API key (revoked or active) for req.Subject. The KeyHash field is cleared from results — listing the hash to an admin UI would leak bcrypt-protected material that's only meant to live inside validation. Order is unspecified.

func (*APIKeyStore) RevokeAPIKey

RevokeAPIKey flips Revoked=true under transaction. Returns core.ErrAPIKeyNotFound when the key doesn't exist; already-revoked is a no-op success (admin "revoke twice" calls must remain safe).

func (*APIKeyStore) UpdateAPIKeyLastUsed

UpdateAPIKeyLastUsed bumps LastUsedAt to time.Now() under transaction. Returns an unwrapped datastore.ErrNoSuchEntity (not core.ErrAPIKeyNotFound) when the key is missing — this is on the validation hot path and a missing key already implies an upstream lookup bug rather than user-facing flow. Callers may swallow this error since the bump is advisory.

func (*APIKeyStore) ValidateAPIKey

ValidateAPIKey performs the full check that GetAPIKeyByID skips. Three error sentinels: core.ErrAPIKeyNotFound for "no such key, malformed input, or wrong secret" (collapsed deliberately so a probe can't distinguish); core.ErrTokenRevoked for revoked keys; core.ErrTokenExpired for expired keys. The FullKey format is "oa_<idTail>_<secret>" — first segment fixed, second the id tail, third the secret. Any deviation maps to ErrAPIKeyNotFound.

type AppRegistrationEntity added in v0.1.12

type AppRegistrationEntity struct {
	Key                       *datastore.Key `datastore:"__key__"`
	ClientID                  string         `datastore:"client_id,noindex"`
	ClientDomain              string         `datastore:"client_domain,noindex"`
	SigningAlg                string         `datastore:"signing_alg,noindex"`
	AuthorizationDetailsTypes []string       `datastore:"authorization_details_types,noindex"`
	CreatedAt                 int64          `datastore:"created_at,noindex"` // unix nanos — keeps Datastore microsecond truncation out of the equality check
	Revoked                   bool           `datastore:"revoked,noindex"`

	ClientName              string   `datastore:"client_name,noindex"`
	ClientURI               string   `datastore:"client_uri,noindex"`
	RedirectURIs            []string `datastore:"redirect_uris,noindex"`
	GrantTypes              []string `datastore:"grant_types,noindex"`
	Scope                   string   `datastore:"scope,noindex"`
	TokenEndpointAuthMethod string   `datastore:"token_endpoint_auth_method,noindex"`

	RegistrationAccessToken string `datastore:"registration_access_token,noindex"`
	RegistrationClientURI   string `datastore:"registration_client_uri,noindex"`
}

AppRegistrationEntity is the Datastore entity for app registrations. All non-key fields are noindex — we only ever look up by the entity key (client_id), and Datastore's 1500-byte per-property index limit would reject long values like a registration_client_uri or many redirect_uris.

type ChannelEntity

type ChannelEntity struct {
	Key         *datastore.Key `datastore:"__key__"`
	Provider    string         `datastore:"provider"`
	IdentityKey string         `datastore:"identity_key"`
	Credentials []byte         `datastore:"credentials,noindex"` // JSON encoded
	Profile     []byte         `datastore:"profile,noindex"`     // JSON encoded
	CreatedAt   time.Time      `datastore:"created_at"`
	UpdatedAt   time.Time      `datastore:"updated_at"`
	ExpiresAt   time.Time      `datastore:"expires_at"` // when channel auth expires
	Version     int            `datastore:"version"`
}

ChannelEntity is the Datastore entity for authentication channels Key format: Provider + ":" + IdentityKey

type ChannelStore

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

ChannelStore implements accounts.ChannelStore using Google Cloud Datastore

func NewChannelStore

func NewChannelStore(client *datastore.Client, namespace string) *ChannelStore

NewChannelStore creates a new Datastore-backed ChannelStore

func (*ChannelStore) GetChannel

GetChannel returns the (provider, identity) channel, or fmt.Errorf("channel not found") when absent. With req.CreateIfMissing set, an empty channel is created in the same call and NewCreated is set — pairs with provider-bind flows ("first time logging in with Google") that want a "fetch or provision" step without a separate write. The entity key derives from "<provider>:<identityKey>" so the same identity bound to two providers (e.g., Google and GitHub) is two distinct rows.

func (*ChannelStore) GetChannelsByIdentity

GetChannelsByIdentity returns every channel for a given identity_key across all providers — backed by an indexed query on the identity_key property. Returns an empty slice (not an error) when nothing matches. Order is unspecified.

func (*ChannelStore) SaveChannel

SaveChannel upserts req.Channel and increments Version each call. CreatedAt is preserved across overwrites; on first write it's stamped from time.Now() and Version starts at 1. Credentials and Profile maps are JSON-encoded into noindex byte properties — Datastore's 1500-byte index limit makes indexing them impractical, and nothing queries by their contents anyway.

type DeviceAuthorizationEntity added in v0.1.29

type DeviceAuthorizationEntity struct {
	Key               *datastore.Key `datastore:"__key__"`
	DeviceCode        string         `datastore:"device_code,noindex"`
	UserCode          string         `datastore:"user_code,noindex"`
	UserCodeUpper     string         `datastore:"user_code_upper"`
	ClientID          string         `datastore:"client_id,noindex"`
	Scopes            []string       `datastore:"scopes,noindex"`
	RequestedAudience string         `datastore:"requested_audience,noindex"`
	Status            string         `datastore:"status,noindex"`
	ApprovedSubject   string         `datastore:"approved_subject,noindex"`
	CreatedAt         int64          `datastore:"created_at,noindex"`
	ExpiresAt         int64          `datastore:"expires_at"`
	LastPolledAt      int64          `datastore:"last_polled_at,noindex"`
	IntervalSeconds   int            `datastore:"interval_seconds,noindex"`
}

DeviceAuthorizationEntity is the Datastore entity for a single device authorization. Two properties carry indexes:

  • UserCodeUpper — the case/dash-normalized form used by GetByUserCode. Indexed because it's the only secondary lookup path. The original UserCode column preserves the display form (XXXX-XXXX) the device showed the user.
  • ExpiresAt — indexed so CleanupExpired's range query stays selective as the table grows.

Every other property is noindex. Datastore's 1500-byte per-property index limit is irrelevant for the unindexed string fields, and disabling indexing saves write cost.

Times are stored as unix nanos (int64) — Datastore truncates time.Time to microsecond precision, and the unix-nanos round-trip matches what GAEAppStore already does for the same reason.

type GAEAppStore added in v0.1.12

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

GAEAppStore implements core.AppRegistrationStore using Google Cloud Datastore. Multi-node compatible — Datastore is the shared source of truth — and slots into stores/gae/ alongside GAEKeyStore and GAEKidStore.

func NewAppStore added in v0.1.12

func NewAppStore(client *datastore.Client, namespace string) *GAEAppStore

NewAppStore creates a new Datastore-backed AppRegistrationStore.

func (*GAEAppStore) DeleteApp added in v0.1.12

DeleteApp removes the registration for req.ClientID. Returns core.ErrAppNotFound if no such registration exists, matching the other AppRegistrationStore backends — Datastore's bare Delete is idempotent and silently succeeds on missing keys, so we Get-then-Delete to honor the contract (the conformance suite's TestDeleteNonexistent asserts on ErrAppNotFound).

func (*GAEAppStore) GetApp added in v0.1.12

GetApp returns the registration for req.ClientID, or core.ErrAppNotFound when the entity is absent. Other Datastore errors (network, auth, quota) surface unwrapped so callers can distinguish "not registered" from "infrastructure problem."

func (*GAEAppStore) ListApps added in v0.1.12

ListApps returns every registration in this namespace. Order is unspecified (Datastore queries without an explicit Order return results in key order, which is implementation-defined). Returns an empty slice (not an error) for an empty namespace, matching the other backends' "fresh store has no apps" shape.

func (*GAEAppStore) SaveApp added in v0.1.12

SaveApp inserts or replaces the registration for req.App.ClientID. Empty client_id is rejected with the same error message as InMemoryAppStore / FSAppStore / GORMAppStore so the shared appstoretest contract passes uniformly across backends.

type GAEDeviceAuthStore added in v0.1.29

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

GAEDeviceAuthStore implements core.DeviceAuthorizationStore on Google Cloud Datastore. Multi-node compatible (Datastore is the shared source of truth) and slots into stores/gae/ alongside GAEAppStore, GAEKeyStore, and GAEKidStore (issue 270).

func NewDeviceAuthStore added in v0.1.29

func NewDeviceAuthStore(client *datastore.Client, namespace string) *GAEDeviceAuthStore

NewDeviceAuthStore creates a new Datastore-backed DeviceAuthorizationStore. The namespace lets multiple oneauth deployments share a single Datastore project without collisions.

func (*GAEDeviceAuthStore) ApproveDeviceAuthorization added in v0.1.29

ApproveDeviceAuthorization transitions a pending authorization to approved and binds the subject + scopes. Wrapped in a Datastore transaction so two concurrent approvers don't both win — the second transaction commits against stale state and returns ErrDeviceAuthorizationNotFound (matching InMemoryDeviceAuthorizationStore semantics: the second writer sees the no-pending-record state).

func (*GAEDeviceAuthStore) CleanupExpired added in v0.1.29

CleanupExpired enumerates the store and removes every record whose ExpiresAt is at or before the current wall-clock time. Uses the indexed expires_at property + DeleteMulti so the call cost scales with the number of expired records, not the table size.

func (*GAEDeviceAuthStore) CreateDeviceAuthorization added in v0.1.29

CreateDeviceAuthorization inserts a new authorization. Empty device_code or user_code is rejected; collisions on either return a generic error matching the in-memory store's wording so a downstream shared contract suite stays uniform.

The collision check is a two-step probe (Get by device_code key, then query by user_code_upper). Without a transaction this is racy under concurrent writes — the entity-key Put at the end provides serializing last-write-wins semantics, but two simultaneous create-with-same-code requests could both pass the pre-check and both write. That race is acceptable for the device-code path (256 bits of entropy makes collision a programmer error, not a real failure mode) and for the user_code path (the caller draws from the same CSPRNG).

func (*GAEDeviceAuthStore) DeleteDeviceAuthorization added in v0.1.29

DeleteDeviceAuthorization removes the authorization. Returns ErrDeviceAuthorizationNotFound when no record matches. Datastore's bare Delete is idempotent (silent success on missing keys), so we Get-then-Delete to honor the contract — same pattern GAEAppStore uses for ErrAppNotFound parity.

func (*GAEDeviceAuthStore) DenyDeviceAuthorization added in v0.1.29

DenyDeviceAuthorization transitions a pending authorization to denied. Wrapped in a transaction for the same reason as ApproveDeviceAuthorization.

func (*GAEDeviceAuthStore) GetByDeviceCode added in v0.1.29

GetByDeviceCode returns the authorization for the given device_code, or ErrDeviceAuthorizationNotFound. Does NOT filter by status or expiry — the caller (the token endpoint handler) checks both.

func (*GAEDeviceAuthStore) GetByUserCode added in v0.1.29

GetByUserCode returns the authorization for the given user_code (case- and dash-insensitive comparison), or ErrDeviceAuthorizationNotFound. Query by the indexed user_code_upper property — O(log n) at the Datastore tier.

func (*GAEDeviceAuthStore) UpdatePollingState added in v0.1.29

UpdatePollingState records a poll attempt — always updates LastPolledAt; raises IntervalSeconds by 5 when SlowDown is true (RFC 8628 §3.5). Read-modify-write inside a transaction so concurrent polls don't lose interval bumps.

type GAEKeyStore

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

GAEKeyStore implements keys.KeyStorage using Google Cloud Datastore.

func NewKeyStore

func NewKeyStore(client *datastore.Client, namespace string) *GAEKeyStore

NewKeyStore creates a new Datastore-backed KeyStore.

func (*GAEKeyStore) DeleteKey

DeleteKey removes the signing-key entry for req.ClientID. Returns keys.ErrKeyNotFound when the entry is absent — Datastore's bare Delete is idempotent and silently succeeds on missing keys, so the implementation does a Get-then-Delete to honor the KeyStorage contract (the conformance suite asserts on this sentinel). Other Datastore errors (network, auth, quota) surface unwrapped so callers can distinguish "no such key" from "infrastructure problem."

func (*GAEKeyStore) GetKey

GetKey returns the signing-key record for req.ClientID, or keys.ErrKeyNotFound when the entry is absent. KeyBytes are returned as []byte (the same shape PutKey accepted) — callers are responsible for parsing back to the algorithm-specific key type.

func (*GAEKeyStore) GetKeyByKid

GetKeyByKid resolves a key by its kid (the JWT header identifier), used by JWKS-driven validators that only know the kid, not the client_id. Returns keys.ErrKidNotFound when no entry carries the requested kid. The query runs against the "kid" property — the only indexed non-key field on SigningKeyEntity — and reads at most one entity (Limit(1)) since kids are unique per JWKS surface.

func (*GAEKeyStore) ListKeyIDs

ListKeyIDs enumerates every client_id with a registered signing key in this namespace. Returns an empty slice (not an error) for an empty namespace, matching the other KeyStorage backends' "fresh store has no keys" shape. Order is unspecified — Datastore key-only queries return results in implementation-defined order; callers that need deterministic order must sort.

func (*GAEKeyStore) PutKey

PutKey upserts a per-client signing key. The record's Key field must be []byte (HMAC secret or marshaled PEM); any other concrete type is rejected with keys.ErrAlgorithmMismatch — KeyStorage callers serialize before reaching the backend, and a wrong type here means the caller skipped that step. Kid is filled in from utils.ComputeKid(KeyBytes, Algorithm) when the caller leaves it empty so JWKS lookups stay deterministic across restarts. Existing entries with the same ClientID are overwritten — KeyStorage has no separate "create vs update" verb.

type GAEKidStore added in v0.0.83

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

GAEKidStore implements keys.KidStorage using Google Cloud Datastore.

func NewKidStore added in v0.0.83

func NewKidStore(client *datastore.Client, namespace string) *GAEKidStore

NewKidStore creates a new Datastore-backed KidStorage.

func (*GAEKidStore) Add added in v0.0.83

Add registers a kid→key entry in the verification-only grace cache used during key rotation. The Key field must be []byte (HMAC secret or marshaled PEM); any other concrete type is rejected with keys.ErrAlgorithmMismatch. ExpiresAt of the zero time means "never expires"; otherwise GetKeyByKid treats now > ExpiresAt as not-found. Existing entries with the same kid are overwritten — KidStorage has no separate "create vs update" verb.

func (*GAEKidStore) CleanExpired added in v0.0.83

CleanExpired sweeps the grace cache for entries whose ExpiresAt has passed and returns the number removed. Implementation reads every entity in the namespace and filters in process — Datastore doesn't expose a "delete where" verb, and the grace cache is small (bounded by rotation cardinality × grace period). Entries with zero ExpiresAt are immortal and skipped. Safe to call concurrently with GetKeyByKid; reads filter on their own independently.

func (*GAEKidStore) GetKey added in v0.0.83

GetKey always returns keys.ErrKeyNotFound. KidStorage is a verification-only grace cache keyed by kid, not by client_id — there is no way to ask "give me the rotation key for this client" because rotation entries are scoped to a kid, not a subject. Callers should reach for GetKeyByKid instead. The method exists only because keys.KidStorage embeds keys.KeyLookup (which carries this verb); the not-found sentinel is the documented behavior across all KidStorage backends, not a GAE-specific gap.

func (*GAEKidStore) GetKeyByKid added in v0.0.83

GetKeyByKid resolves a kid to its grace-period key during rotation. Returns keys.ErrKidNotFound when no entry exists or when ExpiresAt is non-zero and already in the past. The expiry check runs on read so a cleanup job that hasn't yet swept stale entries doesn't return them as valid — CleanExpired's role is reclaiming storage, not gating reads.

func (*GAEKidStore) Remove added in v0.0.83

Remove is idempotent — Datastore's Delete silently succeeds on missing keys, and KidStorage's contract permits that ("removing a kid that was never added is not an error"). The verification-only nature of the grace cache means a stale Remove during rotation is harmless.

type GAEUser

type GAEUser struct {
	UserID      string         `json:"user_id"`
	Active      bool           `json:"is_active"`
	UserProfile map[string]any `json:"profile"`
	CreatedAt   time.Time      `json:"created_at"`
	UpdatedAt   time.Time      `json:"updated_at"`
}

GAEUser implements the accounts.User interface

func (*GAEUser) Id

func (u *GAEUser) Id() string

func (*GAEUser) Profile

func (u *GAEUser) Profile() map[string]any

type IdentityEntity

type IdentityEntity struct {
	Key       *datastore.Key `datastore:"__key__"`
	Type      string         `datastore:"type"`
	Value     string         `datastore:"value"`
	UserID    string         `datastore:"user_id"`
	Verified  bool           `datastore:"verified"`
	CreatedAt time.Time      `datastore:"created_at"`
	UpdatedAt time.Time      `datastore:"updated_at"`
	Version   int            `datastore:"version"`
}

IdentityEntity is the Datastore entity for identities Key format: Type + ":" + Value

func IdentityToEntity

func IdentityToEntity(i *accounts.Identity, key *datastore.Key) *IdentityEntity

IdentityToEntity is the write-path counterpart of ToIdentity. The caller supplies the namespaced datastore key — this helper does not derive it because the store struct (IdentityStore) owns the namespace, not this package-level function. The entity name follows the "<type>:<value>" format documented on IdentityEntity.

func (*IdentityEntity) ToIdentity

func (e *IdentityEntity) ToIdentity() *accounts.Identity

ToIdentity copies every IdentityEntity field into a new accounts.Identity. The Datastore Key is NOT carried over — the returned domain object has no pointer back to its storage row, so callers that still need the key must hold the entity. Used by IdentityStore methods immediately after tx.Get to hand domain objects back to the accounts layer.

type IdentityStore

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

IdentityStore implements accounts.IdentityStore using Google Cloud Datastore

func NewIdentityStore

func NewIdentityStore(client *datastore.Client, namespace string) *IdentityStore

NewIdentityStore creates a new Datastore-backed IdentityStore

func (*IdentityStore) GetIdentity

GetIdentity returns the identity for (req.IdentityType, req.IdentityValue), or fmt.Errorf("identity not found") when absent. With req.CreateIfMissing set, an unbound identity (UserID="") is created in the same call and NewCreated is set in the response — pairs with the signup flow's "ensure-or-create" need without forcing two round trips. The entity key derives from "<type>:<value>" so identities of different types with the same value (e.g., email:foo@bar vs phone:foo@bar) are distinct rows.

func (*IdentityStore) GetUserIdentities

GetUserIdentities returns every identity bound to req.UserID. Backed by an indexed query on the user_id property — unlike most properties in this package, user_id is intentionally indexed because this lookup is on the hot path of "show me all the ways this user can log in." Returns an empty slice (not an error) for a user with no identities yet.

func (*IdentityStore) MarkIdentityVerified

MarkIdentityVerified flips Verified=true and bumps Version under a transaction. Idempotent at the result level — re-marking an already-verified identity still succeeds and still bumps Version (useful for audit trails that want to count verification attempts).

func (*IdentityStore) SaveIdentity

SaveIdentity writes req.Identity unconditionally — used after callers have mutated fields not covered by the dedicated SetUserForIdentity / MarkIdentityVerified helpers. Does NOT bump Version; the dedicated helpers do that under transaction. Callers that need version increments should reach for the appropriate helper instead.

func (*IdentityStore) SetUserForIdentity

SetUserForIdentity binds an identity to a user. Runs under a Datastore transaction so two concurrent calls can't observe a stale UserID and race each other; Version is incremented on every write for optimistic-lock tooling downstream. Identity must already exist — there is no CreateIfMissing here; pair with GetIdentity to provision first.

type KidKeyEntity added in v0.0.83

type KidKeyEntity struct {
	Key       *datastore.Key `datastore:"__key__"`
	KeyBytes  []byte         `datastore:"key_bytes,noindex"`
	Algorithm string         `datastore:"algorithm"`
	ClientID  string         `datastore:"client_id"`
	ExpiresAt time.Time      `datastore:"expires_at,noindex"`
}

KidKeyEntity is the Datastore entity for kid→key grace entries.

type RefreshTokenEntity

type RefreshTokenEntity struct {
	Key                  *datastore.Key `datastore:"__key__"` // Key is the token hash
	Subject              string         `datastore:"subject"`
	ClientID             string         `datastore:"client_id,omitempty"`
	DeviceInfo           []byte         `datastore:"device_info,noindex"` // JSON encoded
	Family               string         `datastore:"family"`
	Generation           int            `datastore:"generation"`
	Scopes               []byte         `datastore:"scopes,noindex"`                // JSON encoded
	AuthorizationDetails []byte         `datastore:"authorization_details,noindex"` // JSON encoded, RFC 9396
	CreatedAt            time.Time      `datastore:"created_at"`
	ExpiresAt            time.Time      `datastore:"expires_at"`
	LastUsedAt           time.Time      `datastore:"last_used_at"`
	RevokedAt            time.Time      `datastore:"revoked_at,omitempty"`
	Revoked              bool           `datastore:"revoked"`
}

RefreshTokenEntity is the Datastore entity for refresh tokens

type RefreshTokenStore

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

RefreshTokenStore implements core.RefreshTokenStore using Google Cloud Datastore

func NewRefreshTokenStore

func NewRefreshTokenStore(client *datastore.Client, namespace string) *RefreshTokenStore

NewRefreshTokenStore creates a new Datastore-backed RefreshTokenStore

func (*RefreshTokenStore) CleanupExpiredTokens

CleanupExpiredTokens removes (a) every token whose ExpiresAt has passed and (b) every revoked token whose RevokedAt is older than 24 hours. The 24-hour grace on revoked rows lets ongoing rotation paths still detect reuse against recently-revoked tokens before they vanish entirely. Intended for a periodic background sweep, not per-request invocation.

func (*RefreshTokenStore) CreateRefreshToken

CreateRefreshToken mints a new refresh-token row with a fresh family id (the first generation in its family). The token string itself is returned to the caller; only its SHA-256 hash is persisted as the entity key — the raw token must never round-trip through the database. Family is a 16-character prefix of a secure random; rotation chains keep this Family constant so RevokeTokenFamily can hit the whole lineage at once on reuse detection.

func (*RefreshTokenStore) GetRefreshToken

GetRefreshToken returns the token row for req.Token (looked up by SHA-256 hash). Returns core.ErrTokenNotFound when absent. Does NOT validate Revoked / ExpiresAt — callers that need that should use RotateRefreshToken (which enforces both under transaction). This method exists for read-only inspection paths (admin tooling, audit).

func (*RefreshTokenStore) GetSubjectTokens added in v0.1.3

GetSubjectTokens returns every active (non-revoked) refresh token for req.Subject — backs the "list active sessions" admin / self-service flow. The plaintext Token field is intentionally cleared from each result; only the hash and metadata travel back to callers, since the store never holds the raw token to begin with.

func (*RefreshTokenStore) RevokeRefreshToken

RevokeRefreshToken flips Revoked=true under transaction. Idempotent: a missing or already-revoked token returns success, not an error — explicit logout / RFC 7009 revocation must be safe to call multiple times.

func (*RefreshTokenStore) RevokeSubjectTokens added in v0.1.3

RevokeSubjectTokens revokes every active (non-revoked) refresh token for req.Subject — used by "log out everywhere" and admin force-revoke paths. Iterates the indexed subject query and stamps Revoked/RevokedAt outside a transaction (the working set can be larger than Datastore's per-tx limit); callers tolerate a brief window where some tokens are revoked and others aren't, since the worst case is a not-yet-revoked token getting one more rotation that then fails reuse detection on the next try.

func (*RefreshTokenStore) RevokeTokenFamily

RevokeTokenFamily revokes every active token in a rotation lineage. Called by RotateRefreshToken's reuse-attack path (also addressable from admin tooling). Like RevokeSubjectTokens, runs outside a transaction — the family can have more entities than a single transaction permits.

func (*RefreshTokenStore) RotateRefreshToken

RotateRefreshToken implements OAuth refresh-token rotation with reuse detection. Under transaction: revoke the old token, mint a new one in the same Family (Generation+1), preserve Subject/ClientID/Scopes/DeviceInfo/ AuthorizationDetails. Three error sentinels:

  • core.ErrTokenNotFound — old token doesn't exist
  • core.ErrTokenExpired — old token's ExpiresAt has passed
  • core.ErrTokenReused — old token was already revoked (reuse attack)

On reuse, the entire family is revoked OUTSIDE the transaction (Datastore transactions can touch at most 25 entity groups; a family can outgrow that). The transaction itself is best-effort: if the family revoke fails, the reuse is still reported to the caller, and a subsequent admin sweep or RevokeSubjectTokens call can finish the cleanup.

type SigningKeyEntity

type SigningKeyEntity struct {
	Key       *datastore.Key `datastore:"__key__"`
	KeyBytes  []byte         `datastore:"key_bytes,noindex"`
	Algorithm string         `datastore:"algorithm"`
	Kid       string         `datastore:"kid"`
}

SigningKeyEntity is the Datastore entity for per-client signing keys.

type TokenStore

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

TokenStore implements core.TokenStore using Google Cloud Datastore

func NewTokenStore

func NewTokenStore(client *datastore.Client, namespace string) *TokenStore

NewTokenStore creates a new Datastore-backed TokenStore

func (*TokenStore) CreateToken

CreateToken mints a fresh verification token via core.GenerateSecureToken (the token is the entity key, so collisions would manifest as overwrites; the secure-random width makes collision negligible). ExpiresAt is computed from req.ExpiryDuration at insert time — the persisted absolute time, not the relative duration, drives GetToken's expiry check.

func (*TokenStore) DeleteSubjectTokens added in v0.1.3

DeleteSubjectTokens removes every verification token of req.Type for req.Subject. Used during password-reset / email-change cleanup to invalidate every outstanding token a subject holds for the same action. Returns success (not an error) when the subject has no matching tokens — the no-op case is normal.

func (*TokenStore) DeleteToken

DeleteToken is idempotent — Datastore's Delete silently succeeds on missing keys, and verification tokens are single-use by design (delete after the action they unlock fires). No "not found" error.

func (*TokenStore) GetToken

GetToken returns the token or one of two error strings: "token not found" when absent, "token expired" when the entity exists but its ExpiresAt has passed. Expired entities are deleted on read — a single-shot self-clean so stale rows don't pile up between explicit DeleteSubjectTokens sweeps. Callers that need to distinguish the two failures match the error string (a sentinel introduction is tracked separately).

type UserEntity

type UserEntity struct {
	Key       *datastore.Key `datastore:"__key__"`
	IsActive  bool           `datastore:"is_active"`
	Profile   []byte         `datastore:"profile,noindex"` // JSON encoded
	CreatedAt time.Time      `datastore:"created_at"`
	UpdatedAt time.Time      `datastore:"updated_at"`
	Version   int            `datastore:"version"`
}

UserEntity is the Datastore entity for users

type UserStore

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

UserStore implements accounts.UserStore using Google Cloud Datastore

func NewUserStore

func NewUserStore(client *datastore.Client, namespace string) *UserStore

NewUserStore creates a new Datastore-backed UserStore

func (*UserStore) CreateUser

CreateUser inserts a new user keyed by req.UserID. The caller picks the ID — this backend doesn't generate one — and re-creating the same ID silently overwrites (Datastore's Put is upsert-shaped, not insert-only). CreatedAt and UpdatedAt are stamped from time.Now() on the way in; the returned GAEUser mirrors the persisted entity so callers don't have to re-read.

func (*UserStore) GetUserById

GetUserById returns the user for req.UserID. Absent users surface as fmt.Errorf("user not found: %s", req.UserID) — not a sentinel — diverging from the keys.ErrKeyNotFound / core.ErrAppNotFound pattern used elsewhere in this package. Callers that need to distinguish "missing" from other failures currently match on the string prefix; introducing a sentinel is tracked separately (it's an interface change, not a doc fix).

func (*UserStore) SaveUser

SaveUser writes req.User, preserving CreatedAt across overwrites (re-saving an existing user keeps the original create time and only bumps UpdatedAt). IsActive defaults to true unless the concrete *GAEUser carries an explicit false — the accounts.User interface has no Active() getter, so non-GAEUser implementations always end up active. Callers that need a different default should pass a *GAEUser.

type UsernameEntity

type UsernameEntity struct {
	Key       *datastore.Key `datastore:"__key__"`
	Username  string         `datastore:"username"` // Original case-preserved username
	UserID    string         `datastore:"user_id"`
	CreatedAt time.Time      `datastore:"created_at"`
}

UsernameEntity is the Datastore entity for username -> userID mapping Key is the username (lowercased for case-insensitive lookup)

type UsernameStore

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

UsernameStore implements accounts.UsernameStore using Google Cloud Datastore

func NewUsernameStore

func NewUsernameStore(client *datastore.Client, namespace string) *UsernameStore

NewUsernameStore creates a new Datastore-backed UsernameStore

func (*UsernameStore) ChangeUsername

ChangeUsername atomically moves a reservation from OldUsername to NewUsername. Two paths:

  • Case-only change (lowercased forms equal) — single-entity transaction that just updates the display Username; ownership is verified before write ("username not owned by user").
  • True rename — multi-entity transaction that asserts the new lowercased form is free, deletes the old entity, and inserts the new one in the same transaction so concurrent reservers can't win the race in the gap.

Error strings: "old username not found", "old username not owned by user", "new username already taken". CreatedAt resets to now on the new row — the reservation is conceptually fresh for the new username.

func (*UsernameStore) GetUserByUsername

GetUserByUsername resolves a username (case-insensitively) to its UserID. Returns fmt.Errorf("username not found") when absent — same string-error pattern as the other accounts-package lookups; sentinel introduction is tracked separately.

func (*UsernameStore) ReleaseUsername

ReleaseUsername drops the reservation. Idempotent — Datastore's Delete silently succeeds on missing keys, and account-cleanup paths must remain safe to retry. Does NOT verify ownership; callers gating release on "the right user is asking" must check upstream (account deletion, admin release).

func (*UsernameStore) ReserveUsername

ReserveUsername binds req.Username to req.UserID. The entity key is the lowercased form (case-insensitive uniqueness); the Username field preserves the original casing for display. Idempotent for the same (username, user) pair — re-reserving by the owner refreshes the display casing but doesn't error. A reservation owned by a different user returns fmt.Errorf("username already taken"). Runs under transaction to close the check-then-write race between two concurrent reservations.

type VerificationTokenEntity added in v0.1.0

type VerificationTokenEntity struct {
	Key       *datastore.Key             `datastore:"__key__"`
	Type      localauth.VerificationType `datastore:"type"`
	Subject   string                     `datastore:"subject"`
	Email     string                     `datastore:"email"`
	CreatedAt time.Time                  `datastore:"created_at"`
	ExpiresAt time.Time                  `datastore:"expires_at"`
}

VerificationTokenEntity is the Datastore entity for localauth verification tokens.

func VerificationTokenToEntity added in v0.1.0

func VerificationTokenToEntity(t *localauth.VerificationToken, key *datastore.Key) *VerificationTokenEntity

VerificationTokenToEntity is the write-path counterpart of ToVerificationToken. Caller supplies the namespaced datastore key whose name carries the token string; the t.Token field is NOT persisted as a separate property — it lives only in the key, matching the ToVerificationToken contract.

func (*VerificationTokenEntity) ToVerificationToken added in v0.1.0

func (e *VerificationTokenEntity) ToVerificationToken() *localauth.VerificationToken

ToVerificationToken reconstructs the plaintext Token from e.Key.Name. Non-obvious invariant: the token string is the entity key name, not a stored property — consumers must hold the entity (not just its struct-tagged property bag) to recover the token. This is also why the helper is a method on *VerificationTokenEntity rather than a free function: it depends on the key, which lives on the entity, not on the property struct.

Jump to

Keyboard shortcuts

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