package idp import ( "context" "errors" "fmt" "net/url" "slices" "strings" "time" dexstorage "github.com/dexidp/dex/storage" "tug/internal/service/identity" "tug/internal/storage" "tug/internal/transport/oidcrp" "tug/internal/transport/web" ) var errDeadRequest = errors.New("idp: the auth request is not waiting for a login") type Verifier struct { deps *deps } var _ web.PasswordLogin = (*Verifier)(nil) func (v *Verifier) live(a dexstorage.AuthRequest) bool { return a.ConnectorID == ConnectorID && !a.LoggedIn && v.deps.now().Before(a.Expiry) } func (v *Verifier) waiting(ctx context.Context, state string) (dexstorage.AuthRequest, pending, error) { a, err := v.deps.dex.GetAuthRequest(ctx, state) if err != nil { return a, pending{}, requestFailure(err) } if !v.live(a) { return a, pending{}, requestFailure(errDeadRequest) } p, err := record(a.ConnectorData) return a, p, requestFailure(err) } func (v *Verifier) Login(ctx context.Context, username, pw, state string) (string, error) { _, target, err := v.LoginUser(ctx, username, pw, state) return target, err } func (v *Verifier) LoginUser(ctx context.Context, username, pw, state string) (string, string, error) { result, err := v.loginUserEpoch(ctx, username, pw, state, "", "", true) return result.UserID, result.Target, err } type LoginResult struct { UserID string Target string ClientID string Epoch int64 } func (v *Verifier) LoginUserEpoch(ctx context.Context, username, pw, state, browser, proof string) (LoginResult, error) { return v.loginUserEpoch(ctx, username, pw, state, browser, proof, false) } func (v *Verifier) loginUserEpoch(ctx context.Context, username, pw, state, browser, proof string, bare bool) (LoginResult, error) { a, p, err := v.waiting(ctx, state) if err != nil { return LoginResult{}, err } if err := checkBinding(p, proof, bare); err != nil { return LoginResult{}, err } user, err := v.deps.identity.Authenticate(ctx, username, pw) if errors.Is(err, identity.ErrUnavailable) { return LoginResult{}, fmt.Errorf("%w: %w", web.ErrLoginBusy, err) } if err != nil { v.loginRefused(ctx, a.ClientID, err) return LoginResult{}, err } authenticatedHook(ctx, user.ID) target, err := v.admit(ctx, user.ID, user.CredentialsEpoch, state, browser, proof, bare) if err != nil { return LoginResult{}, err } return LoginResult{UserID: user.ID, Target: target, ClientID: a.ClientID, Epoch: user.CredentialsEpoch}, nil } func (v *Verifier) ResumeSession(ctx context.Context, sessionID string, since time.Time, claim LoginClaim) (string, error) { a, p := claim.request, claim.record if !v.live(a) { return "", requestFailure(errDeadRequest) } if err := checkBinding(p, claim.Proof, false); err != nil { return "", err } if err := v.resumable(a, since); err != nil { return "", err } row, err := v.deps.store.ForwardSessionByID(ctx, sessionID) if err != nil { return "", sessionEpochFailure(err) } if p.Hint != "" && oidcrp.CheckSubject(p.Hint, row.UserID, ConnectorID) != nil { return "", web.ErrLoginRequired } resumeHook(ctx, row.UserID) epoch, err := v.deps.store.SessionEpoch(ctx, sessionID, v.deps.accountDomain, v.deps.now()) if err != nil { return "", sessionEpochFailure(err) } return v.Admit(ctx, row.UserID, epoch, a.ID, row.Browser, claim.Proof) } func (v *Verifier) resumable(a dexstorage.AuthRequest, since time.Time) error { if slices.Contains(strings.Fields(a.Prompt), "login") { return fmt.Errorf("%w: prompt=login", web.ErrLoginRequired) } if a.MaxAge >= 0 && v.deps.now().Sub(since) > time.Duration(a.MaxAge)*time.Second { return fmt.Errorf("%w: the session is older than max_age", web.ErrLoginRequired) } return nil } func sessionEpochFailure(err error) error { if errors.Is(err, storage.ErrNotFound) { return fmt.Errorf("%w: %w", web.ErrNoSession, err) } return fmt.Errorf("%w: read the session's credentials epoch: %w", web.ErrLoginBusy, err) } func (v *Verifier) Admit(ctx context.Context, userID string, epoch int64, state, browser, proof string) (string, error) { return v.admit(ctx, userID, epoch, state, browser, proof, false) } func (v *Verifier) admit(ctx context.Context, userID string, epoch int64, state, browser, proof string, bare bool) (string, error) { if browser != "" && !storage.ValidBrowser(browser) { return "", fmt.Errorf("%w: the browser marker is malformed", web.ErrLoginBusy) } var value string err := v.updateBound(ctx, state, proof, bare, func(a dexstorage.AuthRequest, p pending) (pending, error) { p = pending{Binding: p.Binding, Hint: p.Hint, UserID: userID, Epoch: &epoch, Browser: browser} value, p.Digest, p.Expiry = v.newValue(a.Expiry) return p, nil }) if err != nil { return "", err } q := url.Values{} q.Set("state", state) q.Set(ValueParam, value) return v.deps.issuer + "/callback?" + q.Encode(), nil }