clisignin

package
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Overview

Package clisignin signs the CLI in through the browser (R-406, design 06 §3.5).

The shape is OAuth 2.0's authorization code grant for a native app (RFC 8252) with PKCE (RFC 7636), with Pando as its own authorization server and the console as its consent page:

  1. `pando login` makes a verifier and sends only its SHA-256, the challenge, to the console in the address it opens.
  2. The person, signed in to the console however their account signs in — password and second factor, or an identity provider — approves it. Approve stores the challenge against their account and answers with a one-time code, which the browser carries to the CLI's loopback address, or which the person pastes into the CLI.
  3. The CLI trades the code and the verifier for a delegated token (Exchange).

The code alone is worth nothing: a code that reaches the wrong hands — a link sent to someone, a page that read the loopback redirect — cannot be traded without the verifier, which never left the process that started the sign-in. That is this flow's version of R-043's rule that a sign-in finishes only where it started.

Index

Constants

View Source
const CodeLifetime = 5 * time.Minute

CodeLifetime is how long an approval waits for the CLI [P]: long enough to copy a code from one window and paste it into a terminal, short enough that one left on a screen is soon worth nothing.

Variables

This section is empty.

Functions

func Challenge

func Challenge(verifier string) string

Challenge is the S256 code challenge for a verifier (RFC 7636 §4.2).

func Digest

func Digest(code string) string

Digest is what is stored of a code: its SHA-256, hex.

Types

type AccountReader

type AccountReader interface {
	ByID(ctx context.Context, userID string) (state.User, bool, error)
}

AccountReader reads accounts (state.Users).

type Approved

type Approved struct {
	Code secret.Value

	// RedirectURL is the CLI's loopback address with the code on it, or
	// empty when the person pastes the code instead.
	RedirectURL string

	ExpiresAt time.Time
}

Approved is an approval's one-time code and where the browser takes it.

type Exchanged

type Exchanged struct {
	Issued state.Issued

	// UserID and Username are the account the token acts as (R-058).
	UserID   string
	Username string

	// ApprovedBy is the session that approved it.
	ApprovedBy string
	Name       string
}

Exchanged is a token made for an approved sign-in.

type Request

type Request struct {
	// CodeChallenge is base64url(SHA-256(verifier)), unpadded.
	CodeChallenge       string `json:"code_challenge"`
	CodeChallengeMethod string `json:"code_challenge_method"`

	// Name is the token's name: the machine's, "laptop CLI".
	Name string `json:"name"`

	// Port is the loopback port the CLI is listening on, or 0 when it waits
	// for the code to be pasted.
	Port int `json:"port"`

	// State is echoed to the CLI's loopback address, so it can tell its own
	// sign-in's answer from any other request that reaches the port.
	State string `json:"state"`
}

Request is what the CLI put in the address it opened, as the console sends it on.

type Service

type Service struct {
	Store  Store
	Users  AccountReader
	Tokens TokenMinter
	Policy interface {
		Load(ctx context.Context) (policy.Document, error)
	}
	Clock clock.Clock
}

Service approves CLI sign-ins and trades them for tokens. It decides nothing about who is asking: the caller has established that a signed-in person, not a token, approved, and audits what happened.

func (*Service) Approve

func (s *Service) Approve(ctx context.Context, userID, approvedBy string, req Request) (Approved, error)

Approve records that userID, signed in as approvedBy, lets the CLI that made req sign in as them.

func (*Service) Exchange

func (s *Service) Exchange(ctx context.Context, code, verifier string) (Exchanged, error)

Exchange trades a code and the verifier behind its challenge for a delegated token. The approval is gone whatever the outcome. When a code that named an approval is refused, the Exchanged returned with the error says whose it was (UserID, ApprovedBy, Name); for any other refusal it is empty.

type Store

type Store interface {
	Create(ctx context.Context, s state.CLISignIn) error
	Take(ctx context.Context, codeHash string) (state.CLISignIn, bool, error)
}

Store keeps approvals (state.CLISignIns).

type TokenMinter

type TokenMinter interface {
	Create(ctx context.Context, kind, name, ownerUserID, createdBy string, expiresAt *time.Time) (state.Issued, error)
}

TokenMinter mints tokens (state.Tokens).

Jump to

Keyboard shortcuts

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