auth

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package auth contains the SafeDep CLI's authentication flows. It owns the OAuth2 device-code login, the static API-key login, and the helpers that read and write credentials via dry/cloud's keychain.

Commands under internal/cmd/auth invoke these flows. Nothing else in the CLI talks to the keychain directly.

Index

Constants

View Source
const (

	// DefaultAPIKeyExpiryDays is the lifetime of API keys created by the
	// device-code login flow. Override with --api-key-expiry-days.
	DefaultAPIKeyExpiryDays = 90

	// gRPCAppName identifies our connections to SafeDep Cloud in logs.
	GRPCAppName = "safedep-cli"
)

Variables

View Source
var CLIScopes = []string{"offline_access", "openid", "profile", "email"}

CLIScopes are the OAuth scopes we request. offline_access is required to receive a refresh token.

View Source
var ErrEmailNotVerified = errors.New("auth: email not verified")

ErrEmailNotVerified is returned by RunDeviceFlow when Auth0 rejects the device authorisation because the user's email address is unverified. Callers can detect it with errors.Is to offer a retry path.

View Source
var ErrRefreshFailed = errors.New("auth: refresh token invalid or expired: run `safedep auth login` to re-authenticate")

ErrRefreshFailed indicates the refresh token is expired, revoked, or otherwise invalid. Callers should prompt the user to re-authenticate.

Functions

func APIKeyName

func APIKeyName(hostname string, now time.Time) string

APIKeyName returns the human-readable name used when creating API keys from the device login flow. Stable enough for users to identify keys in the cloud UI. Unique enough to avoid collisions on repeated logins.

func AccessTokenExpiry

func AccessTokenExpiry(token string) (time.Time, error)

AccessTokenExpiry decodes the unverified `exp` claim of a JWT and returns it as a UTC time. Verification is the identity provider's job. We only need the expiry to drive UI hints and the "session expired" error path.

func Audience

func Audience() string

Audience returns the OAuth audience, honouring the env override.

func ClientID

func ClientID() string

ClientID returns the OAuth client ID, honouring the env override.

func DeviceCodeURL

func DeviceCodeURL() string

DeviceCodeURL returns the device-code endpoint, honouring the env override.

func GenerateTenantDomain

func GenerateTenantDomain(orgName string) string

GenerateTenantDomain generates a unique domain slug for an org name by combining a slugified version of the org name with a random adjective-noun suffix and 3 random alphanumeric characters.

func Hostname

func Hostname() string

Hostname returns the machine hostname, falling back to "unknown" on error.

func IsExpired

func IsExpired(token string, now time.Time) bool

IsExpired reports whether the token's exp claim is in the past. Tokens without a parseable exp claim are treated as expired so callers fall through to a re-login path.

func NewRegistrationPrompter

func NewRegistrationPrompter(accessToken string) func() (*RegistrationInput, error)

NewRegistrationPrompter returns a BootstrapInput-compatible RegistrationPrompter closure that calls PromptRegistration with the given access token.

func NormalizeTenantDomain

func NormalizeTenantDomain(s string) string

NormalizeTenantDomain converts an arbitrary string into a valid tenant domain slug. It applies NFKD normalization, strips combining marks, converts to lowercase, replaces non-alphanumeric chars with hyphens, collapses runs, and trims leading/trailing hyphens.

func PrintVerification

func PrintVerification(verificationURL, userCode string)

PrintVerification prints device-flow verification details and, in rich mode, attempts to open the browser automatically.

func PromptTenantPicker

func PromptTenantPicker(tenants []string) (string, error)

PromptTenantPicker presents a selection form when the user has access to multiple tenants and must choose one for the active profile.

func RefreshAndPersistIfExpired

func RefreshAndPersistIfExpired(ctx context.Context, store cloud.CredentialStore, creds *cloud.Credentials, keychainOpts []cloud.KeychainOption) (*cloud.Credentials, error)

RefreshAndPersistIfExpired checks whether creds contain an expired token and, if so, silently refreshes it using the stored refresh token, persists the new tokens, and returns fresh credentials. Returns creds unchanged when not expired.

func RegisterTenant

func RegisterTenant(ctx context.Context, in RegisterTenantInput) (string, error)

RegisterTenant creates a new tenant for a first-time user by calling OnboardingService.OnboardUser. It retries up to 3 total attempts on domain uniqueness conflicts, regenerating the domain suffix each attempt. On exhausted retries it returns a user-facing error message.

func SaveAPIKey

func SaveAPIKey(_ context.Context, store cloud.CredentialStore, in APIKeyInput) error

SaveAPIKey persists the API key + tenant to the provided keychain store. The store is expected to be already scoped to the active profile by the caller.

func SaveBootstrapResult

func SaveBootstrapResult(store cloud.CredentialStore, accessToken, refreshToken string, b *BootstrapResult) error

SaveBootstrapResult persists the access token, refresh token, and (if present) API key from a completed bootstrap to the credential store.

func TokenURL

func TokenURL() string

TokenURL returns the token endpoint, honouring the env override.

func VerifyAPIKey

func VerifyAPIKey(ctx context.Context, in APIKeyInput) error

VerifyAPIKey checks that the supplied API key + tenant authenticate against the SafeDep data plane. We connect and issue a low-cost RPC. A successful round trip means the key is valid for that tenant.

Types

type APIKeyInput

type APIKeyInput struct {
	APIKey string
	Tenant string
}

APIKeyInput is the data needed to persist an API-key credential.

type BootstrapInput

type BootstrapInput struct {
	AccessToken     string
	PreferredTenant string

	CreateAPIKey     bool
	APIKeyExpiryDays int
	APIKeyName       string
	Picker           TenantPicker

	// RegistrationPrompter is called when the user has no accessible tenant.
	// It should collect registration data interactively and return it. When
	// nil, the zero-tenant case returns the existing "no accessible tenant"
	// error (preserving behaviour for callers that do not support registration).
	RegistrationPrompter func() (*RegistrationInput, error)

	// ConnFor is the control-plane connection builder. Optional. When
	// nil, the package-local default is used. Tests inject a fake.
	ConnFor ControlPlaneConnFunc
}

BootstrapInput captures everything PostOAuthBootstrap needs to provision a tenant and (optionally) an API key on top of a fresh access token.

type BootstrapResult

type BootstrapResult struct {
	Tenant          string
	APIKey          string
	APIKeyExpiresAt time.Time
}

BootstrapResult reports what the bootstrap step accomplished.

func PostOAuthBootstrap

func PostOAuthBootstrap(ctx context.Context, in BootstrapInput) (*BootstrapResult, error)

PostOAuthBootstrap completes the work that follows a successful device flow: discover accessible tenants, pick one, optionally create an API key. It does not write to the keychain. The caller does that, since the keychain store is owned by App.

type ControlPlaneConnFunc

type ControlPlaneConnFunc func(token, tenant string) (*grpc.ClientConn, error)

ControlPlaneConnFunc opens a control-plane gRPC connection for the supplied (token, tenant). tenant may be empty for the bootstrap call to GetUserInfo. Tests inject a fake here. Production callers leave it nil and the package-local default is used.

type DeviceFlowResult

type DeviceFlowResult struct {
	AccessToken  string
	RefreshToken string
}

DeviceFlowResult is the outcome of a successful OAuth2 device-code authorisation: the access + refresh token pair returned by the IdP.

func RefreshTokens

func RefreshTokens(ctx context.Context, accessToken, refreshToken string) (*DeviceFlowResult, error)

RefreshTokens exchanges a refresh token for a fresh access + refresh token pair using golang.org/x/oauth2. Returns ErrRefreshFailed when the server rejects the token; callers should direct the user to re-login.

func RunDeviceFlow

func RunDeviceFlow(ctx context.Context, sink DeviceFlowSink, retries ...DeviceFlowRetry) (*DeviceFlowResult, error)

RunDeviceFlow performs a complete OAuth2 device-code authorisation against the configured SafeDep identity provider. Optional retry policies are applied in order: the first matching policy fires its OnRetry side-effect and retries the flow once. Existing callers that pass no retries are unaffected.

type DeviceFlowRetry

type DeviceFlowRetry struct {
	ShouldRetry func(err error) bool
	OnRetry     func(err error)
	OnExhausted func() error
}

DeviceFlowRetry defines when and how to retry a failed device flow attempt. Each policy is applied at most once: if ShouldRetry matches, OnRetry fires, and the flow is attempted again. If the retry also fails with a matching error, OnExhausted returns the terminal error shown to the user.

func EmailVerificationRetry

func EmailVerificationRetry(stdin io.Reader) DeviceFlowRetry

EmailVerificationRetry returns a DeviceFlowRetry that handles the email-not-verified case: warns the user, waits for them to press Enter after verifying, then allows one retry. stdin is typically os.Stdin; callers may inject a reader for testing.

type DeviceFlowSink

type DeviceFlowSink func(verificationURL, userCode string)

DeviceFlowSink reports the verification URL and user code to the user. Implementations decide how to present them (TUI banner, opening a browser, etc.). The sink runs once before polling begins.

type RegisterTenantInput

type RegisterTenantInput struct {
	AccessToken        string
	Email              string
	Name               string
	OrganizationName   string
	OrganizationDomain string
	// ConnFor is the control-plane connection builder. Optional. When nil,
	// the package-local default is used. Tests inject a fake.
	ConnFor ControlPlaneConnFunc
}

RegisterTenantInput holds the parameters for RegisterTenant.

type RegistrationInput

type RegistrationInput struct {
	Email              string
	Name               string
	OrganizationName   string
	OrganizationDomain string
}

RegistrationInput holds user-supplied data for first-time tenant registration. It is passed via BootstrapInput.RegistrationPrompter when the user has no accessible tenant yet.

func PromptRegistration

func PromptRegistration(accessToken string) (RegistrationInput, error)

PromptRegistration collects first-time registration data via interactive huh forms. The email from the access token is shown and editable; the final value is included in the returned RegistrationInput.

type Status

type Status struct {
	Profile        string
	Tenant         string
	APIKey         bool
	OAuth          bool
	OAuthExpiresAt time.Time
}

Status describes what credentials a profile currently holds.

func BuildStatus

func BuildStatus(_ context.Context, profile string, opts []cloud.KeychainOption) (Status, error)

BuildStatus inspects the keychain via two resolvers (one per credential type) and returns what the active profile holds. Missing-credentials errors are treated as "not configured" rather than failures.

type TenantPicker

type TenantPicker func(tenants []string) (string, error)

TenantPicker resolves the tenant when the user has access to multiple. Invoked only when len(tenants) > 1 and no preferred tenant matches.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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