• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vocdoni / saas-backend / 17262379833

27 Aug 2025 09:07AM UTC coverage: 58.431% (+0.4%) from 57.998%
17262379833

Pull #206

github

web-flow
f/csp refactor fixes (#217)

* fix AuthOnly flow
* adapt to changes introduced by new type Phone
* fix test race condition
* drop unused methods
* lint
Pull Request #206: csp: Refactor csp to allow login with arbitrary authFields and twoFaFields

132 of 180 new or added lines in 6 files covered. (73.33%)

21 existing lines in 4 files now uncovered.

5482 of 9382 relevant lines covered (58.43%)

28.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.11
/csp/auth.go
1
// Package csp implements the Census Service Provider functionality
2
package csp
3

4
import (
5
        "crypto/sha256"
6
        "encoding/base32"
7
        "time"
8

9
        "github.com/google/uuid"
10
        "github.com/vocdoni/saas-backend/csp/notifications"
11
        "github.com/vocdoni/saas-backend/db"
12
        "github.com/vocdoni/saas-backend/internal"
13
        "github.com/xlzd/gotp"
14
        "go.vocdoni.io/dvote/log"
15
)
16

17
// BundleAuthToken method generates a new authentication token for a user in
18
// a process of a bundle. It generates a new token, secret and code from the
19
// attempt number. It composes the notification challenge and pushes it to
20
// the queue to be sent. It returns the token as HexBytes.
21
func (c *CSP) BundleAuthToken(bID, uID internal.HexBytes, to string,
22
        ctype notifications.ChallengeType,
23
) (internal.HexBytes, error) {
13✔
24
        // check the input parameters
13✔
25
        if len(bID) == 0 {
14✔
26
                return nil, ErrNoBundleID
1✔
27
        }
1✔
28
        if len(uID) == 0 {
13✔
29
                return nil, ErrNoUserID
1✔
30
        }
1✔
31

32
        // For auth-only cases (no challenge type and no destination), create a pre-verified token
33
        if to == "" && ctype == "" {
12✔
34
                return c.createAuthOnlyToken(bID, uID)
1✔
35
        }
1✔
36

37
        // get last token for the user and bundle
38
        lastToken, err := c.Storage.LastCSPAuth(uID, bID)
10✔
39
        if err != nil && err != db.ErrTokenNotFound {
10✔
40
                log.Warnw("error getting last token",
×
41
                        "userID", uID,
×
42
                        "bundleID", bID,
×
43
                        "error", err)
×
44
                return nil, ErrStorageFailure
×
45
        }
×
46
        // check if the last token was created less than the cooldown time
47
        if lastToken != nil && time.Since(lastToken.CreatedAt) < c.notificationCoolDownTime {
11✔
48
                log.Warnw("cooldown time not reached",
1✔
49
                        "userID", uID,
1✔
50
                        "bundleID", bID)
1✔
51
                return nil, ErrAttemptCoolDownTime
1✔
52
        }
1✔
53
        // generate a new token, secret and code from the attempt number
54
        token, code, err := c.generateToken(uID, bID)
9✔
55
        if err != nil {
9✔
56
                return nil, err
×
57
        }
×
58
        // create the new token
59
        if err := c.Storage.SetCSPAuth(token, uID, bID); err != nil {
9✔
60
                log.Warnw("error setting new token",
×
61
                        "userID", uID,
×
62
                        "bundleID", bID,
×
63
                        "error", err)
×
64
                return nil, ErrStorageFailure
×
65
        }
×
66
        log.Debugw("new auth token stored",
9✔
67
                "userID", uID,
9✔
68
                "bundleID", bID,
9✔
69
                "token", token)
9✔
70
        // compose the notification challenge
9✔
71
        ch, err := notifications.NewNotificationChallenge(ctype, uID, bID, to, code)
9✔
72
        if err != nil {
10✔
73
                log.Warnw("error composing notification challenge",
1✔
74
                        "userID", uID,
1✔
75
                        "bundleID", bID,
1✔
76
                        "error", err)
1✔
77
                return nil, ErrNotificationFailure
1✔
78
        }
1✔
79
        // push the challenge to the queue to be sent
80
        if err := c.notifyQueue.Push(ch); err != nil {
8✔
81
                log.Warnw("error pushing notification challenge",
×
82
                        "userID", uID,
×
83
                        "bundleID", bID,
×
84
                        "error", err)
×
85
                return nil, ErrNotificationFailure
×
86
        }
×
87
        return token, nil
8✔
88
}
89

90
// VerifyBundleAuthToken method verifies the authentication token for a user
91
// in a process of a bundle. It gets the user data from the token and checks
92
// if the process is already consumed. It checks if the process is related to
93
// the user and if the token matches. It verifies the solution and updates the
94
// user data in the storage. It returns an error if the process is already
95
// consumed, if the process is not related to the user, if the token does not
96
// match, if the solution is not correct or if there is an error updating the
97
// user data.
98
func (c *CSP) VerifyBundleAuthToken(token internal.HexBytes, solution string) error {
11✔
99
        if len(token) == 0 {
12✔
100
                return ErrInvalidAuthToken
1✔
101
        }
1✔
102
        if len(solution) == 0 {
11✔
103
                return ErrInvalidSolution
1✔
104
        }
1✔
105
        // get the user data from the token
106
        authTokenData, err := c.Storage.CSPAuth(token)
9✔
107
        if err != nil {
10✔
108
                log.Warnw("error getting user data by token",
1✔
109
                        "token", token,
1✔
110
                        "error", err)
1✔
111
                return ErrInvalidAuthToken
1✔
112
        }
1✔
113
        // verify the solution, and if the solution is not correct, return an error
114
        if !c.verifySolution(authTokenData.UserID, authTokenData.BundleID, solution) {
10✔
115
                log.Warnw("challenge code do not match",
2✔
116
                        "userID", authTokenData.UserID,
2✔
117
                        "bundleID", authTokenData.BundleID,
2✔
118
                        "token", token,
2✔
119
                        "solution", solution)
2✔
120
                return ErrChallengeCodeFailure
2✔
121
        }
2✔
122
        // set the token as verified
123
        if err := c.Storage.VerifyCSPAuth(token); err != nil {
6✔
124
                log.Warnw("error verifying token",
×
125
                        "userID", authTokenData.UserID,
×
126
                        "bundleID", authTokenData.BundleID,
×
127
                        "token", token,
×
128
                        "error", err)
×
129
                return ErrStorageFailure
×
130
        }
×
131
        return nil
6✔
132
}
133

134
// generateToken method generates a new authentication token for a user in a
135
// process. It checks if the process is already consumed for this user. It
136
// generates a new challenge secret, challenge token and OTP code for the
137
// secret and the attempt number. It returns the token, the secret and the
138
// code respectively.
139
func (*CSP) generateToken(uID, bID internal.HexBytes) (
140
        internal.HexBytes, string, error,
141
) {
12✔
142
        // generate a new challenge secret and challenge token
12✔
143
        secret := otpSecret(uID, bID)
12✔
144
        // generate the OTP code for the secret and the attempt number
12✔
145
        otp := gotp.NewDefaultHOTP(secret)
12✔
146
        code := otp.At(0)
12✔
147
        // generate a new token and convert it to HexBytes
12✔
148
        bToken, err := uuid.New().MarshalBinary()
12✔
149
        if err != nil {
12✔
150
                log.Warnw("error marshalling token",
×
151
                        "error", err,
×
152
                        "userID", uID,
×
153
                        "bundleID", bID)
×
154
                return nil, "", ErrInvalidAuthToken
×
155
        }
×
156
        return bToken, code, nil
12✔
157
}
158

159
// verifySolution method verifies the solution for a user process. It generates
160
// the OTP code for the process secret and the attempt number and compares it
161
// with the solution. It returns true if the solution is correct, false
162
// otherwise.
163
func (*CSP) verifySolution(uID, bID internal.HexBytes, solution string) bool {
10✔
164
        secret := otpSecret(uID, bID)
10✔
165
        // generate the OTP code for the secret and the attempt number
10✔
166
        otp := gotp.NewDefaultHOTP(secret)
10✔
167
        code := otp.At(0)
10✔
168
        // compare the generated code with the solution
10✔
169
        return code == solution
10✔
170
}
10✔
171

172
// createAuthOnlyToken creates a pre-verified token for auth-only censuses
173
// that don't require challenge verification. It generates a token and immediately
174
// marks it as verified.
175
func (c *CSP) createAuthOnlyToken(bID, uID internal.HexBytes) (internal.HexBytes, error) {
1✔
176
        // get last token for the user and bundle
1✔
177
        lastToken, err := c.Storage.LastCSPAuth(uID, bID)
1✔
178
        if err != nil && err != db.ErrTokenNotFound {
1✔
NEW
179
                log.Warnw("error getting last token",
×
NEW
180
                        "userID", uID,
×
NEW
181
                        "bundleID", bID,
×
NEW
182
                        "error", err)
×
NEW
183
                return nil, ErrStorageFailure
×
NEW
184
        }
×
185
        // check if the last token was created less than the cooldown time
186
        if lastToken != nil && time.Since(lastToken.CreatedAt) < c.notificationCoolDownTime {
1✔
NEW
187
                log.Warnw("cooldown time not reached",
×
NEW
188
                        "userID", uID,
×
NEW
189
                        "bundleID", bID)
×
NEW
190
                return nil, ErrAttemptCoolDownTime
×
NEW
191
        }
×
192

193
        // generate a new token (we don't need the code for auth-only)
194
        bToken, err := uuid.New().MarshalBinary()
1✔
195
        if err != nil {
1✔
NEW
196
                log.Warnw("error marshalling token",
×
NEW
197
                        "error", err,
×
NEW
198
                        "userID", uID,
×
NEW
199
                        "bundleID", bID)
×
NEW
200
                return nil, ErrInvalidAuthToken
×
NEW
201
        }
×
202

203
        // create the new token
204
        if err := c.Storage.SetCSPAuth(bToken, uID, bID); err != nil {
1✔
NEW
205
                log.Warnw("error setting new token",
×
NEW
206
                        "userID", uID,
×
NEW
207
                        "bundleID", bID,
×
NEW
208
                        "error", err)
×
NEW
209
                return nil, ErrStorageFailure
×
NEW
210
        }
×
211

212
        // immediately verify the token since no challenge is needed
213
        if err := c.Storage.VerifyCSPAuth(bToken); err != nil {
1✔
NEW
214
                log.Warnw("error verifying auth-only token",
×
NEW
215
                        "userID", uID,
×
NEW
216
                        "bundleID", bID,
×
NEW
217
                        "token", bToken,
×
NEW
218
                        "error", err)
×
NEW
219
                return nil, ErrStorageFailure
×
NEW
220
        }
×
221

222
        log.Debugw("new auth-only token created and verified",
1✔
223
                "userID", uID,
1✔
224
                "bundleID", bID,
1✔
225
                "token", bToken)
1✔
226

1✔
227
        return bToken, nil
1✔
228
}
229

230
// otpSecret method generates a new OTP secret for a user and a bundle. The
231
// secret is generated by hashing the user ID and the bundle ID with SHA-256.
232
// It returns the secret as HexBytes.
233
func otpSecret(uID, bID internal.HexBytes) string {
25✔
234
        hash := sha256.Sum256(append(uID, bID...))
25✔
235
        // encode the secret in base32 and return it
25✔
236
        return base32.StdEncoding.EncodeToString(hash[:])
25✔
237
}
25✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc