Help us shape the future of Go! Take the 2026 Go Developer Survey

appauth

package
v0.40.4 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package appauth is the server side of the native app's sign-in (iOS design §2): the app never handles a password or an IdP token. It opens the server's own web sign-in in a system browser sheet and receives a one-time code, bound to its PKCE challenge, which it exchanges for a session token.

app:     verifier, challenge = S256(verifier)
browser: GET /api/auth/app/start?code_challenge=…   → Begin (state cookie)
         … the usual OIDC sign-in …
         GET /api/auth/app/finish (signed in, state cookie)
                                     → consent page ("Sign in the app as …?")
         POST /api/auth/app/finish (state cookie + consent token)
                                                     → Finish → 302 callback?code=…
app:     POST /api/auth/app/token {code, code_verifier}
                                                     → Redeem → user id

What makes it safe (DA's conditions on the design):

  • the callback is fixed by the server, never taken from the request;
  • the state cookie binds finish to the browser that started, and finish mints a code only after the signed-in user confirms on a same-origin page (a POST with a token derived from that cookie): a start link sent to someone else, opened in their already-signed-in browser, can't hand the sender a code without them pressing Continue;
  • the callback is fixed per server, and rivolt.dev uses its https Universal Link, never the claimable rivolt:// scheme;
  • the code lives 60 s, works once, and is only good with the verifier whose S256 is the challenge (a wrong verifier burns it);
  • only HMACs of the state value and the code are stored.

Index

Constants

View Source
const (
	// StartTTL is how long a started sign-in may take to come back to
	// finish (the user signs in with the IdP in between).
	StartTTL = 10 * time.Minute
	// CodeTTL is how long the one-time code is good for: the app redeems it
	// immediately after the browser sheet closes.
	CodeTTL = 60 * time.Second
)
View Source
const MaxOutstanding = 1000

MaxOutstanding caps the sign-ins started and not yet finished or expired, across all users: start is unauthenticated, so this bounds what a flood of starts can put in the table (DA on #769).

Variables

View Source
var ErrBusy = errors.New("appauth: too many sign-ins in progress")

ErrBusy is Begin refusing because MaxOutstanding sign-ins are in flight.

View Source
var ErrInvalid = errors.New("appauth: invalid or expired")

ErrInvalid covers every way a start value, code or verifier can be wrong: unknown, expired, already used, or not matching. Callers can't tell which, so neither can an attacker.

Functions

func Challenge

func Challenge(verifier string) string

Challenge is S256: base64url(SHA-256(verifier)), no padding (RFC 7636).

func ValidChallenge

func ValidChallenge(c string) bool

ValidChallenge reports whether c looks like an S256 PKCE challenge.

Types

type Store

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

Store keeps sign-in attempts in app_auth_requests.

func New

func New(db *sql.DB, pepper []byte) (*Store, error)

New returns a Store. pepper is the session-token HMAC key (the cookie secret), so state values and codes are stored like session tokens.

func (*Store) Begin

func (s *Store) Begin(ctx context.Context, challenge string) (string, error)

Begin records a sign-in attempt for challenge and returns the raw state value the caller puts in the browser's state cookie.

func (*Store) CheckConsent

func (s *Store) CheckConsent(state, token string) bool

CheckConsent reports whether token is ConsentToken(state).

func (*Store) ConsentToken

func (s *Store) ConsentToken(state string) string

ConsentToken is the CSRF token for the consent form shown at finish: an HMAC of the browser's state value, so only a page that saw this browser's state cookie can produce it. The code is minted only on a POST carrying it (DA on #769: finish never redirects with a code on a GET).

func (*Store) Finish

func (s *Store) Finish(ctx context.Context, state string, uid uuid.UUID) (string, error)

Finish binds the signed-in user to the attempt the browser's state value names and returns the one-time code for the app. Each attempt yields one code: a second Finish (a replayed finish URL) fails.

func (*Store) Purge

func (s *Store) Purge(ctx context.Context) (int64, error)

Purge deletes sign-in attempts that expired more than a day ago (the retention job; Begin also trims as it goes). It returns the rows deleted.

func (*Store) Redeem

func (s *Store) Redeem(ctx context.Context, code, verifier string) (uuid.UUID, error)

Redeem exchanges a code and its PKCE verifier for the user it was issued to. The row is deleted before the verifier is checked, so a code works once and a wrong verifier burns it.

Jump to

Keyboard shortcuts

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