package idp import ( "bytes" "context" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/json" "errors" "fmt" "io" "time" dexstorage "github.com/dexidp/dex/storage" "tug/internal/transport/web" ) func record(data []byte) (pending, error) { var p pending if len(data) == 0 { return p, nil } data = bytes.TrimSpace(data) if len(data) == 0 || data[0] != '{' { return p, errDeadRequest } d := json.NewDecoder(bytes.NewReader(data)) d.DisallowUnknownFields() if err := d.Decode(&p); err != nil { return p, errDeadRequest } if err := d.Decode(new(any)); err != io.EOF { return p, errDeadRequest } return p, nil } func bindingDigest(proof string) string { sum := sha256.Sum256([]byte(proof)) return base64.RawURLEncoding.EncodeToString(sum[:]) } func (v *Verifier) bound(ctx context.Context, state, proof string) (dexstorage.AuthRequest, pending, error) { a, p, err := v.waiting(ctx, state) if err != nil { return a, pending{}, err } return a, p, checkBinding(p, proof, false) } func (v *Verifier) BoundClient(ctx context.Context, state, proof string) (string, error) { a, _, err := v.bound(ctx, state, proof) return a.ClientID, err } func checkBinding(p pending, proof string, bare bool) error { if bare && p.Binding == "" && proof == "" { return nil } if proof == "" || p.Binding == "" || subtle.ConstantTimeCompare([]byte(p.Binding), []byte(bindingDigest(proof))) != 1 { return web.ErrLoginElsewhere } return nil } func requestFailure(err error) error { switch { case errors.Is(err, errDeadRequest), errors.Is(err, dexstorage.ErrNotFound): return fmt.Errorf("%w: %w", web.ErrLoginExpired, err) case errors.Is(err, web.ErrLoginElsewhere): return err case err != nil: return fmt.Errorf("%w: auth request: %w", web.ErrLoginBusy, err) } return nil } type LoginClaim struct { Proof string Expiry time.Time Silent bool request dexstorage.AuthRequest record pending } func loginClaim(a dexstorage.AuthRequest, p pending, proof string) LoginClaim { return LoginClaim{Proof: proof, Expiry: a.Expiry, Silent: a.Prompt == promptNone, request: a, record: p} } type LoginRequest struct { Silent bool Expiry time.Time } func (v *Verifier) Unclaimed(ctx context.Context, state string) (LoginRequest, error) { a, p, err := v.waiting(ctx, state) if err != nil { return LoginRequest{}, err } if p.Binding != "" { return LoginRequest{}, web.ErrLoginElsewhere } return LoginRequest{Silent: a.Prompt == promptNone, Expiry: a.Expiry}, nil } func (v *Verifier) Claim(ctx context.Context, state, proof string) (LoginClaim, error) { if proof == "" { return LoginClaim{}, web.ErrLoginElsewhere } a, p, err := v.waiting(ctx, state) if err != nil { return LoginClaim{}, err } if p.Binding != "" { if err := checkBinding(p, proof, false); err != nil { return LoginClaim{}, err } return loginClaim(a, p, proof), nil } var claim LoginClaim err = v.deps.dex.UpdateAuthRequest(ctx, state, func(a dexstorage.AuthRequest) (dexstorage.AuthRequest, error) { if !v.live(a) { return a, errDeadRequest } p, err := record(a.ConnectorData) if err != nil { return a, err } if p.Binding == "" { p.Binding = bindingDigest(proof) a.ConnectorData = encodePending(p) } if err := checkBinding(p, proof, false); err != nil { return a, err } claim = loginClaim(a, p, proof) return a, nil }) if err != nil { return LoginClaim{}, requestFailure(err) } return claim, nil } func (v *Verifier) updateBound(ctx context.Context, state, proof string, bare bool, update func(dexstorage.AuthRequest, pending) (pending, error)) error { err := v.deps.dex.UpdateAuthRequest(ctx, state, func(a dexstorage.AuthRequest) (dexstorage.AuthRequest, error) { if !v.live(a) { return a, errDeadRequest } p, err := record(a.ConnectorData) if err != nil { return a, err } if err := checkBinding(p, proof, bare); err != nil { return a, err } p, err = update(a, p) if err != nil { return a, err } a.ConnectorData = encodePending(p) return a, nil }) return requestFailure(err) }