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
- Variables
- func APIKeyName(hostname string, now time.Time) string
- func AccessTokenExpiry(token string) (time.Time, error)
- func Audience() string
- func ClientID() string
- func DeviceCodeURL() string
- func GenerateTenantDomain(orgName string) string
- func Hostname() string
- func IsExpired(token string, now time.Time) bool
- func NewRegistrationPrompter(accessToken string) func() (*RegistrationInput, error)
- func NormalizeTenantDomain(s string) string
- func PrintVerification(verificationURL, userCode string)
- func PromptTenantPicker(tenants []string) (string, error)
- func RefreshAndPersistIfExpired(ctx context.Context, store cloud.CredentialStore, creds *cloud.Credentials, ...) (*cloud.Credentials, error)
- func RegisterTenant(ctx context.Context, in RegisterTenantInput) (string, error)
- func SaveAPIKey(_ context.Context, store cloud.CredentialStore, in APIKeyInput) error
- func SaveBootstrapResult(store cloud.CredentialStore, accessToken, refreshToken string, ...) error
- func TokenURL() string
- func VerifyAPIKey(ctx context.Context, in APIKeyInput) error
- type APIKeyInput
- type BootstrapInput
- type BootstrapResult
- type ControlPlaneConnFunc
- type DeviceFlowResult
- type DeviceFlowRetry
- type DeviceFlowSink
- type RegisterTenantInput
- type RegistrationInput
- type Status
- type TenantPicker
Constants ¶
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 ¶
var CLIScopes = []string{"offline_access", "openid", "profile", "email"}
CLIScopes are the OAuth scopes we request. offline_access is required to receive a refresh token.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Status describes what credentials a profile currently holds.
func BuildStatus ¶
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 ¶
TenantPicker resolves the tenant when the user has access to multiple. Invoked only when len(tenants) > 1 and no preferred tenant matches.