Raw
  1package idp
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7	"net/url"
  8	"slices"
  9	"strings"
 10	"time"
 11
 12	dexstorage "github.com/dexidp/dex/storage"
 13
 14	"tug/internal/service/identity"
 15	"tug/internal/storage"
 16	"tug/internal/transport/oidcrp"
 17	"tug/internal/transport/web"
 18)
 19
 20var errDeadRequest = errors.New("idp: the auth request is not waiting for a login")
 21
 22type Verifier struct {
 23	deps *deps
 24}
 25
 26var _ web.PasswordLogin = (*Verifier)(nil)
 27
 28func (v *Verifier) live(a dexstorage.AuthRequest) bool {
 29	return a.ConnectorID == ConnectorID && !a.LoggedIn && v.deps.now().Before(a.Expiry)
 30}
 31
 32func (v *Verifier) waiting(ctx context.Context, state string) (dexstorage.AuthRequest, pending, error) {
 33	a, err := v.deps.dex.GetAuthRequest(ctx, state)
 34	if err != nil {
 35		return a, pending{}, requestFailure(err)
 36	}
 37	if !v.live(a) {
 38		return a, pending{}, requestFailure(errDeadRequest)
 39	}
 40	p, err := record(a.ConnectorData)
 41	return a, p, requestFailure(err)
 42}
 43
 44func (v *Verifier) Login(ctx context.Context, username, pw, state string) (string, error) {
 45	_, target, err := v.LoginUser(ctx, username, pw, state)
 46	return target, err
 47}
 48
 49func (v *Verifier) LoginUser(ctx context.Context, username, pw, state string) (string, string, error) {
 50	result, err := v.loginUserEpoch(ctx, username, pw, state, "", "", true)
 51	return result.UserID, result.Target, err
 52}
 53
 54type LoginResult struct {
 55	UserID   string
 56	Target   string
 57	ClientID string
 58	Epoch    int64
 59}
 60
 61func (v *Verifier) LoginUserEpoch(ctx context.Context, username, pw, state, browser, proof string) (LoginResult, error) {
 62	return v.loginUserEpoch(ctx, username, pw, state, browser, proof, false)
 63}
 64
 65func (v *Verifier) loginUserEpoch(ctx context.Context, username, pw, state, browser, proof string, bare bool) (LoginResult, error) {
 66	a, p, err := v.waiting(ctx, state)
 67	if err != nil {
 68		return LoginResult{}, err
 69	}
 70	if err := checkBinding(p, proof, bare); err != nil {
 71		return LoginResult{}, err
 72	}
 73	user, err := v.deps.identity.Authenticate(ctx, username, pw)
 74	if errors.Is(err, identity.ErrUnavailable) {
 75		return LoginResult{}, fmt.Errorf("%w: %w", web.ErrLoginBusy, err)
 76	}
 77	if err != nil {
 78		v.loginRefused(ctx, a.ClientID, err)
 79		return LoginResult{}, err
 80	}
 81	authenticatedHook(ctx, user.ID)
 82	target, err := v.admit(ctx, user.ID, user.CredentialsEpoch, state, browser, proof, bare)
 83	if err != nil {
 84		return LoginResult{}, err
 85	}
 86	return LoginResult{UserID: user.ID, Target: target, ClientID: a.ClientID, Epoch: user.CredentialsEpoch}, nil
 87}
 88
 89func (v *Verifier) ResumeSession(ctx context.Context, sessionID string, since time.Time, claim LoginClaim) (string, error) {
 90	a, p := claim.request, claim.record
 91	if !v.live(a) {
 92		return "", requestFailure(errDeadRequest)
 93	}
 94	if err := checkBinding(p, claim.Proof, false); err != nil {
 95		return "", err
 96	}
 97	if err := v.resumable(a, since); err != nil {
 98		return "", err
 99	}
100	row, err := v.deps.store.ForwardSessionByID(ctx, sessionID)
101	if err != nil {
102		return "", sessionEpochFailure(err)
103	}
104	if p.Hint != "" && oidcrp.CheckSubject(p.Hint, row.UserID, ConnectorID) != nil {
105		return "", web.ErrLoginRequired
106	}
107	resumeHook(ctx, row.UserID)
108	epoch, err := v.deps.store.SessionEpoch(ctx, sessionID, v.deps.accountDomain, v.deps.now())
109	if err != nil {
110		return "", sessionEpochFailure(err)
111	}
112	return v.Admit(ctx, row.UserID, epoch, a.ID, row.Browser, claim.Proof)
113}
114
115func (v *Verifier) resumable(a dexstorage.AuthRequest, since time.Time) error {
116	if slices.Contains(strings.Fields(a.Prompt), "login") {
117		return fmt.Errorf("%w: prompt=login", web.ErrLoginRequired)
118	}
119	if a.MaxAge >= 0 && v.deps.now().Sub(since) > time.Duration(a.MaxAge)*time.Second {
120		return fmt.Errorf("%w: the session is older than max_age", web.ErrLoginRequired)
121	}
122	return nil
123}
124
125func sessionEpochFailure(err error) error {
126	if errors.Is(err, storage.ErrNotFound) {
127		return fmt.Errorf("%w: %w", web.ErrNoSession, err)
128	}
129	return fmt.Errorf("%w: read the session's credentials epoch: %w", web.ErrLoginBusy, err)
130}
131
132func (v *Verifier) Admit(ctx context.Context, userID string, epoch int64, state, browser, proof string) (string, error) {
133	return v.admit(ctx, userID, epoch, state, browser, proof, false)
134}
135
136func (v *Verifier) admit(ctx context.Context, userID string, epoch int64, state, browser, proof string, bare bool) (string, error) {
137	if browser != "" && !storage.ValidBrowser(browser) {
138		return "", fmt.Errorf("%w: the browser marker is malformed", web.ErrLoginBusy)
139	}
140	var value string
141	err := v.updateBound(ctx, state, proof, bare, func(a dexstorage.AuthRequest, p pending) (pending, error) {
142		p = pending{Binding: p.Binding, Hint: p.Hint, UserID: userID, Epoch: &epoch, Browser: browser}
143		value, p.Digest, p.Expiry = v.newValue(a.Expiry)
144		return p, nil
145	})
146	if err != nil {
147		return "", err
148	}
149	q := url.Values{}
150	q.Set("state", state)
151	q.Set(ValueParam, value)
152	return v.deps.issuer + "/callback?" + q.Encode(), nil
153}