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}