appauth

package
v0.40.14 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 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