Files
headscale/hscontrol/types/credential.go
T
Kristoffer Dalby 393dd3e2d9 db: store all credentials in one SHA-256-hashed table
API keys, pre-auth keys and OAuth clients/tokens share one table and verify
path. Secrets carry 256 bits of crypto/rand entropy, so a SHA-256 digest
needs no stretching; bcrypt/argon2id rows rehash on use until 0.32.
2026-09-26 00:33:12 +02:00

91 lines
3.0 KiB
Go

package types
import (
"time"
)
// CredentialKind discriminates rows of the credentials table.
type CredentialKind string
const (
CredentialAPIKey CredentialKind = "api"
CredentialPreAuthKey CredentialKind = "authkey"
CredentialOAuthClient CredentialKind = "oauth_client" //nolint:gosec // discriminator value, not a credential
CredentialOAuthToken CredentialKind = "oauth_token" //nolint:gosec // discriminator value, not a credential
)
// Credential is the unified storage model for every authenticatable secret:
// API keys, pre-auth keys, OAuth clients, and OAuth access tokens. Each row is
// discriminated by [Credential.Kind]. Only a hash of the secret is stored:
// SHA-256, or a legacy bcrypt/Argon2id hash until the next authentication.
// Identifier is the public, indexed lookup value — the 12-char prefix for API
// keys, pre-auth keys and access tokens, and the client id for OAuth clients —
// and is unique within a kind.
//
// Per-kind fields are sparse by design: Reusable/Ephemeral/Used apply to
// pre-auth keys, LastSeen to API keys, Scopes to OAuth credentials, ClientID
// links an OAuth token to its issuing client, and Tags to pre-auth keys and
// OAuth credentials.
type Credential struct {
ID uint64 `gorm:"primary_key"`
Kind CredentialKind `gorm:"not null;index:idx_credentials_identifier,unique,priority:1"`
Identifier string `gorm:"index:idx_credentials_identifier,unique,priority:2"`
Hash []byte
// UserID records the owning (user-owned pre-auth key, API key) or creating
// (tagged pre-auth key, OAuth client) user. Deleting the user nulls it.
UserID *uint
User *User `gorm:"constraint:OnDelete:SET NULL;"`
Description string
Scopes []string `gorm:"serializer:json"`
Tags []string `gorm:"serializer:json"`
Reusable bool
Ephemeral bool `gorm:"default:false"`
Used bool `gorm:"default:false"`
LastSeen *time.Time
// ClientID links an OAuth access token (Kind == CredentialOAuthToken) back to
// the Identifier of its issuing OAuth client.
ClientID string
CreatedAt *time.Time
Expiration *time.Time
Revoked *time.Time
}
// IsTagged reports whether this credential carries tags. For a pre-auth key
// credential, a node registered with it becomes a tagged node.
func (c *Credential) IsTagged() bool {
return len(c.Tags) > 0
}
// AsCredential projects a pre-auth key back onto a [Credential] (kind
// authkey), used to set a node's AuthKey association during registration from a
// pre-auth key projection.
func (pak *PreAuthKey) AsCredential() *Credential {
if pak == nil {
return nil
}
return &Credential{
ID: pak.ID,
Kind: CredentialPreAuthKey,
Identifier: pak.Prefix,
Hash: pak.Hash,
UserID: pak.UserID,
User: pak.User,
Description: pak.Description,
Reusable: pak.Reusable,
Ephemeral: pak.Ephemeral,
Used: pak.Used,
Tags: pak.Tags,
CreatedAt: pak.CreatedAt,
Expiration: pak.Expiration,
Revoked: pak.Revoked,
}
}