• 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

53.87
/csp/handlers/handlers.go
1
// Package handlers provides HTTP handlers for the CSP
2
// API endpoints, managing authentication, token verification, and cryptographic
3
// signing operations for process bundles in the Vocdoni voting platform.
4
package handlers
5

6
import (
7
        "bytes"
8
        "encoding/json"
9
        "fmt"
10
        "net/http"
11
        "strconv"
12
        "strings"
13

14
        "github.com/ethereum/go-ethereum/common"
15
        "github.com/go-chi/chi/v5"
16
        "github.com/vocdoni/saas-backend/api/apicommon"
17
        "github.com/vocdoni/saas-backend/csp"
18
        "github.com/vocdoni/saas-backend/csp/notifications"
19
        "github.com/vocdoni/saas-backend/csp/signers"
20
        "github.com/vocdoni/saas-backend/db"
21
        "github.com/vocdoni/saas-backend/errors"
22
        "github.com/vocdoni/saas-backend/internal"
23
        "go.vocdoni.io/dvote/log"
24
        "go.vocdoni.io/dvote/vochain/state"
25
)
26

27
// CSPHandlers is a struct that contains an instance of the CSP and the main
28
// database (where the bundle and census data is stored). It is used to handle
29
// the CSP API requests such as the authentication and signing of the bundle
30
// processes.
31
type CSPHandlers struct {
32
        csp    *csp.CSP
33
        mainDB *db.MongoStorage
34
}
35

36
// New creates a new instance of the CSP handlers instance. It receives the CSP
37
// instance and the main database instance as parameters.
38
func New(c *csp.CSP, mainDB *db.MongoStorage) *CSPHandlers {
1✔
39
        return &CSPHandlers{
1✔
40
                csp:    c,
1✔
41
                mainDB: mainDB,
1✔
42
        }
1✔
43
}
1✔
44

45
// parseBundleID parses the bundle ID from the URL parameters
46
func parseBundleID(w http.ResponseWriter, r *http.Request) (*internal.HexBytes, bool) {
27✔
47
        bundleID := new(internal.HexBytes)
27✔
48
        if err := bundleID.ParseString(chi.URLParam(r, "bundleId")); err != nil {
27✔
49
                errors.ErrMalformedURLParam.Withf("invalid bundle ID").Write(w)
×
50
                return nil, false
×
51
        }
×
52
        return bundleID, true
27✔
53
}
54

55
// parseAuthStep parses the authentication step from the URL parameters
56
func parseAuthStep(w http.ResponseWriter, r *http.Request) (int, bool) {
22✔
57
        stepString := chi.URLParam(r, "step")
22✔
58
        step, err := strconv.Atoi(stepString)
22✔
59
        if err != nil || (step != 0 && step != 1) {
22✔
60
                errors.ErrMalformedURLParam.Withf("wrong step ID").Write(w)
×
61
                return 0, false
×
62
        }
×
63
        return step, true
22✔
64
}
65

66
// getBundle retrieves the bundle from the database
67
func (c *CSPHandlers) getBundle(w http.ResponseWriter, bundleID internal.HexBytes) (*db.ProcessesBundle, bool) {
27✔
68
        bundle, err := c.mainDB.ProcessBundle(bundleID)
27✔
69
        if err != nil {
27✔
70
                if err == db.ErrNotFound {
×
71
                        errors.ErrMalformedURLParam.Withf("bundle not found").Write(w)
×
72
                        return nil, false
×
73
                }
×
74
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
75
                return nil, false
×
76
        }
77

78
        // Check if the bundle has processes
79
        if len(bundle.Processes) == 0 {
27✔
80
                errors.ErrInvalidOrganizationData.Withf("bundle has no processes").Write(w)
×
81
                return nil, false
×
82
        }
×
83

84
        return bundle, true
27✔
85
}
86

87
// handleAuthStep handles the authentication step and writes the response
88
func (c *CSPHandlers) handleAuthStep(w http.ResponseWriter, r *http.Request,
89
        step int, bundleID internal.HexBytes, censusID string,
90
) {
22✔
91
        var authToken internal.HexBytes
22✔
92
        var err error
22✔
93

22✔
94
        if step == 0 {
37✔
95
                authToken, err = c.authFirstStep(r, bundleID, censusID)
15✔
96
        } else {
22✔
97
                authToken, err = c.authSecondStep(r)
7✔
98
        }
7✔
99

100
        if err != nil {
30✔
101
                if apiErr, ok := err.(errors.Error); ok {
16✔
102
                        apiErr.Write(w)
8✔
103
                        return
8✔
104
                }
8✔
105
                errors.ErrUnauthorized.WithErr(err).Write(w)
×
106
                return
×
107
        }
108

109
        apicommon.HTTPWriteJSON(w, &AuthResponse{AuthToken: authToken})
14✔
110
}
111

112
// BundleAuthHandler godoc
113
//
114
//        @Summary                Authenticate for a process bundle
115
//        @Description        Handle authentication for a process bundle. There are two steps in the authentication process:
116
//        @Description        - Step 0: The user sends the participant ID and contact information (email or phone).
117
//        @Description        If valid, the server sends a challenge to the user with a token.
118
//        @Description        - Step 1: The user sends the token and challenge solution back to the server.
119
//        @Description        If valid, the token is marked as verified and returned.
120
//        @Tags                        process
121
//        @Accept                        json
122
//        @Produce                json
123
//        @Param                        bundleId        path                string                true        "Bundle ID"
124
//        @Param                        step                path                string                true        "Authentication step (0 or 1)"
125
//        @Param                        request                body                interface{}        true        "Authentication request (varies by step)"
126
//        @Success                200                        {object}        handlers.AuthResponse
127
//        @Failure                400                        {object}        errors.Error        "Invalid input data"
128
//        @Failure                401                        {object}        errors.Error        "Unauthorized"
129
//        @Failure                404                        {object}        errors.Error        "Bundle not found"
130
//        @Failure                500                        {object}        errors.Error        "Internal server error"
131
//        @Router                        /process/bundle/{bundleId}/auth/{step} [post]
132
func (c *CSPHandlers) BundleAuthHandler(w http.ResponseWriter, r *http.Request) {
22✔
133
        // Parse the bundle ID and authentication step
22✔
134
        bundleID, ok := parseBundleID(w, r)
22✔
135
        if !ok {
22✔
136
                return
×
137
        }
×
138

139
        step, ok := parseAuthStep(w, r)
22✔
140
        if !ok {
22✔
141
                return
×
142
        }
×
143

144
        // Get the bundle
145
        bundle, ok := c.getBundle(w, *bundleID)
22✔
146
        if !ok {
22✔
147
                return
×
148
        }
×
149

150
        // Handle the authentication step
151
        c.handleAuthStep(w, r, step, *bundleID, bundle.Census.ID.Hex())
22✔
152
}
153

154
// parseSignRequest parses the sign request from the request body
155
func parseSignRequest(w http.ResponseWriter, r *http.Request) (*SignRequest, bool) {
5✔
156
        var req SignRequest
5✔
157
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
5✔
158
                errors.ErrMalformedBody.Write(w)
×
159
                return nil, false
×
160
        }
×
161

162
        // Check that the request contains the auth token
163
        if req.AuthToken == nil {
5✔
164
                errors.ErrUnauthorized.Withf("missing auth token").Write(w)
×
165
                return nil, false
×
166
        }
×
167

168
        return &req, true
5✔
169
}
170

171
// getAuthInfo retrieves the authentication information from the token
172
func (c *CSPHandlers) getAuthInfo(w http.ResponseWriter, authToken internal.HexBytes) (*db.CSPAuth, bool) {
5✔
173
        auth, err := c.csp.Storage.CSPAuth(authToken)
5✔
174
        if err != nil {
5✔
175
                errors.ErrUnauthorized.WithErr(err).Write(w)
×
176
                return nil, false
×
177
        }
×
178
        return auth, true
5✔
179
}
180

181
// findProcessInBundle checks if the process is part of the bundle
182
func findProcessInBundle(bundle *db.ProcessesBundle, processID internal.HexBytes) (internal.HexBytes, bool) {
5✔
183
        for _, pID := range bundle.Processes {
10✔
184
                if bytes.Equal(pID, processID) {
10✔
185
                        return pID, true
5✔
186
                }
5✔
187
        }
188
        return nil, false
×
189
}
190

191
// checkCensusParticipant checks if the participant is in the census
192
func (c *CSPHandlers) checkCensusParticipant(w http.ResponseWriter, censusID string, userID string) bool {
5✔
193
        // Get census information
5✔
194
        census, err := c.mainDB.Census(censusID)
5✔
195
        if err != nil {
5✔
196
                if err == db.ErrNotFound {
×
197
                        return false
×
198
                }
×
199
                return false
×
200
        }
201
        if _, err := c.mainDB.CensusParticipantByMemberNumber(censusID, userID, census.OrgAddress); err != nil {
5✔
202
                if err == db.ErrNotFound {
×
203
                        errors.ErrUnauthorized.Withf("participant not found in the census").Write(w)
×
204
                        return false
×
205
                }
×
206
                log.Warnw("error getting census participant", "error", err)
×
207
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
208
                return false
×
209
        }
210
        return true
5✔
211
}
212

213
// parseAddress parses the address from the payload
214
func parseAddress(w http.ResponseWriter, payload string) (*internal.HexBytes, bool) {
5✔
215
        address := new(internal.HexBytes)
5✔
216
        if err := address.ParseString(payload); err != nil {
5✔
217
                errors.ErrMalformedBody.WithErr(err).Write(w)
×
218
                return nil, false
×
219
        }
×
220
        return address, true
5✔
221
}
222

223
// signAndRespond signs the request and sends the response
224
func (c *CSPHandlers) signAndRespond(w http.ResponseWriter, authToken, address, processID internal.HexBytes) {
5✔
225
        log.Debugw("new CSP sign request", "address", address, "procId", processID)
5✔
226

5✔
227
        signature, err := c.csp.Sign(authToken, address, processID, signers.SignerTypeECDSASalted)
5✔
228
        if err != nil {
6✔
229
                errors.ErrUnauthorized.WithErr(err).Write(w)
1✔
230
                return
1✔
231
        }
1✔
232

233
        apicommon.HTTPWriteJSON(w, &AuthResponse{Signature: signature})
4✔
234
}
235

236
// BundleSignHandler godoc
237
//
238
//        @Summary                Sign a process in a bundle
239
//        @Description        Sign a process in a bundle. Requires a verified token. The server signs the address with the user data
240
//        @Description        and returns the signature. Once signed, the process is marked as consumed and cannot be signed again.
241
//        @Tags                        process
242
//        @Accept                        json
243
//        @Produce                json
244
//        @Param                        bundleId        path                string                                        true        "Bundle ID"
245
//        @Param                        request                body                handlers.SignRequest        true        "Sign request with process ID, auth token, and payload"
246
//        @Success                200                        {object}        handlers.AuthResponse
247
//        @Failure                400                        {object}        errors.Error        "Invalid input data"
248
//        @Failure                401                        {object}        errors.Error        "Unauthorized or invalid token"
249
//        @Failure                404                        {object}        errors.Error        "Bundle not found"
250
//        @Failure                500                        {object}        errors.Error        "Internal server error"
251
//        @Router                        /process/bundle/{bundleId}/sign [post]
252
func (c *CSPHandlers) BundleSignHandler(w http.ResponseWriter, r *http.Request) {
5✔
253
        // Parse the bundle ID
5✔
254
        bundleID, ok := parseBundleID(w, r)
5✔
255
        if !ok {
5✔
256
                return
×
257
        }
×
258

259
        // Get the bundle
260
        bundle, ok := c.getBundle(w, *bundleID)
5✔
261
        if !ok {
5✔
262
                return
×
263
        }
×
264

265
        // Parse the sign request
266
        req, ok := parseSignRequest(w, r)
5✔
267
        if !ok {
5✔
268
                return
×
269
        }
×
270

271
        // Get the authentication information
272
        auth, ok := c.getAuthInfo(w, req.AuthToken)
5✔
273
        if !ok {
5✔
274
                return
×
275
        }
×
276

277
        // Find the process in the bundle
278
        processID, found := findProcessInBundle(bundle, req.ProcessID)
5✔
279
        if !found {
5✔
280
                errors.ErrUnauthorized.Withf("process not found in bundle").Write(w)
×
281
                return
×
282
        }
×
283

284
        // Check if the participant is in the census
285
        if !c.checkCensusParticipant(w, bundle.Census.ID.Hex(), string(auth.UserID)) {
5✔
286
                return
×
287
        }
×
288

289
        // Parse the address from the payload
290
        address, ok := parseAddress(w, req.Payload)
5✔
291
        if !ok {
5✔
292
                return
×
293
        }
×
294

295
        // Sign the request and send the response
296
        c.signAndRespond(w, req.AuthToken, *address, processID)
5✔
297
}
298

299
// ConsumedAddressHandler godoc
300
//
301
//        @Summary                Get the address used to sign a process
302
//        @Description        Get the address used to sign a process. Requires a verified token. Returns the address, nullifier,
303
//        @Description        and timestamp of the consumption.
304
//        @Tags                        process
305
//        @Accept                        json
306
//        @Produce                json
307
//        @Param                        processId        path                string                                                        true        "Process ID"
308
//        @Param                        request                body                handlers.ConsumedAddressRequest        true        "Request with auth token"
309
//        @Success                200                        {object}        handlers.ConsumedAddressResponse
310
//        @Failure                400                        {object}        errors.Error        "Invalid input data"
311
//        @Failure                401                        {object}        errors.Error        "Unauthorized or invalid token"
312
//        @Failure                404                        {object}        errors.Error        "Process not found"
313
//        @Failure                500                        {object}        errors.Error        "Internal server error"
314
//        @Router                        /process/{processId}/sign-info [post]
315
func (c *CSPHandlers) ConsumedAddressHandler(w http.ResponseWriter, r *http.Request) {
×
316
        // get the bundle ID from the URL parameters
×
317
        processID := new(internal.HexBytes)
×
318
        if err := processID.ParseString(chi.URLParam(r, "processId")); err != nil {
×
319
                errors.ErrMalformedURLParam.WithErr(csp.ErrNoBundleID).Write(w)
×
320
                return
×
321
        }
×
322
        // parse the request from the body
323
        var req ConsumedAddressRequest
×
324
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
×
325
                errors.ErrMalformedBody.Write(w)
×
326
                return
×
327
        }
×
328
        // get the user data from the token
329
        auth, err := c.csp.Storage.CSPAuth(req.AuthToken)
×
330
        if err != nil {
×
331
                log.Warnw("error getting user data by token",
×
332
                        "error", err,
×
333
                        "token", req.AuthToken)
×
334
                errors.ErrUnauthorized.WithErr(err).Write(w)
×
335
                return
×
336
        }
×
337
        // check if the token is verified
338
        if !auth.Verified {
×
339
                errors.ErrUnauthorized.WithErr(csp.ErrAuthTokenNotVerified).Write(w)
×
340
                return
×
341
        }
×
342
        cspProcess, err := c.csp.Storage.CSPProcess(auth.Token, *processID)
×
343
        if err != nil {
×
344
                log.Warnw("error getting user data by token",
×
345
                        "error", err,
×
346
                        "token", req.AuthToken)
×
347
                errors.ErrUnauthorized.WithErr(err).Write(w)
×
348
                return
×
349
        }
×
350
        // check if the process has been consumed and return error if not
351
        if !cspProcess.Consumed {
×
352
                errors.ErrUserNoVoted.Write(w)
×
353
                return
×
354
        }
×
355
        // return the address used to sign the process and the nullifier
356
        apicommon.HTTPWriteJSON(w, &ConsumedAddressResponse{
×
357
                Address:   cspProcess.ConsumedAddress,
×
358
                Nullifier: state.GenerateNullifier(common.BytesToAddress(cspProcess.ConsumedAddress), *processID),
×
359
                At:        cspProcess.ConsumedAt,
×
360
        })
×
361
}
362

363
// validateParticipantID checks if the participant ID is provided
UNCOV
364
func validateParticipantID(participantID string) error {
×
UNCOV
365
        if len(participantID) == 0 {
×
366
                return errors.ErrInvalidUserData.Withf("participant ID not provided")
×
367
        }
×
UNCOV
368
        return nil
×
369
}
370

371
// validateContactInfo checks if at least one contact method is provided
372
func validateContactInfo(email, phone string) error {
14✔
373
        if len(email) == 0 && len(phone) == 0 {
15✔
374
                return errors.ErrInvalidUserData.Withf("no contact information provided (email or phone)")
1✔
375
        }
1✔
376
        return nil
13✔
377
}
378

379
// validateEmail validates the email format if provided
380
func validateEmail(email string) error {
14✔
381
        if len(email) > 0 && !internal.ValidEmail(email) {
14✔
382
                return errors.ErrInvalidUserData.Withf("invalid email format")
×
383
        }
×
384
        return nil
14✔
385
}
386

387
// validateAuthRequest validates the authentication request data
388
func validateAuthRequest(req *AuthRequest, census *db.Census) error {
15✔
389
        // Check request participant ID
15✔
390
        // TODO: Add correct validations
15✔
391

15✔
392
        // Only require contact information if the census has two-factor fields
15✔
393
        if len(census.TwoFaFields) > 0 {
29✔
394
                err := validateContactInfo(req.Email, req.Phone.String())
14✔
395
                if err != nil {
15✔
396
                        return err
1✔
397
                }
1✔
398
        }
399

400
        // Validate email if provided
401
        err := validateEmail(req.Email)
14✔
402
        if err != nil {
14✔
403
                return err
×
404
        }
×
405
        return nil
14✔
406
}
407

408
// verifyPassword checks if the provided password matches the member's hashed password
UNCOV
409
func (c *CSPHandlers) verifyPassword(password string, hashedPass []byte) error {
×
UNCOV
410
        if password != "" && !bytes.Equal(internal.HashPassword(c.csp.PasswordSalt, password), hashedPass) {
×
411
                return fmt.Errorf("invalid user data")
×
412
        }
×
UNCOV
413
        return nil
×
414
}
415

416
// verifyEmail checks if the provided email matches the member's stored email
417
func verifyEmail(email string, storedEmail string) error {
6✔
418
        if !strings.EqualFold(email, storedEmail) {
6✔
UNCOV
419
                return errors.ErrUnauthorized.Withf("invalid user email")
×
UNCOV
420
        }
×
421
        return nil
6✔
422
}
423

424
// handleEmailContact verifies the email and returns the appropriate contact method
425
func handleEmailContact(
426
        email string,
427
        storedEmail string,
428
) (string, notifications.ChallengeType, error) {
6✔
429
        if err := verifyEmail(email, storedEmail); err != nil {
6✔
UNCOV
430
                return "", "", err
×
UNCOV
431
        }
×
432
        return email, notifications.EmailChallenge, nil
6✔
433
}
434

435
// handlePhoneContact verifies the phone and returns the appropriate contact method
436
func handlePhoneContact(
437
        orgAddress common.Address,
438
        phone string,
439
        memberPhone *db.Phone,
440
) (string, notifications.ChallengeType, error) {
1✔
441
        if err := memberPhone.Matches(phone, orgAddress); err != nil {
1✔
442
                return "", "", err
×
443
        }
×
444
        return phone, notifications.SMSChallenge, nil
1✔
445
}
446

447
// determineContactMethod determines the contact method based on the census type and request data
448
func determineContactMethod(
449
        census *db.Census,
450
        req *AuthRequest,
451
        member *db.OrgMember,
452
) (string, notifications.ChallengeType, error) {
8✔
453
        switch census.Type {
8✔
454
        case db.CensusTypeMail:
6✔
455
                return handleEmailContact(req.Email, member.Email)
6✔
456

457
        case db.CensusTypeSMS:
1✔
458
                return handlePhoneContact(census.OrgAddress, req.Phone.String(), member.Phone)
1✔
459

460
        case db.CensusTypeSMSorMail:
×
461
                if req.Email != "" {
×
462
                        return handleEmailContact(req.Email, member.Email)
×
463
                }
×
464

NEW
465
                if !req.Phone.IsEmpty() {
×
NEW
466
                        return handlePhoneContact(census.OrgAddress, req.Phone.String(), member.Phone)
×
UNCOV
467
                }
×
468

469
                // If neither email nor phone is provided for SMS or Mail census
NEW
470
                return "", "", errors.ErrInvalidUserData.Withf("no valid contact method provided")
×
471
        case db.CensusTypeAuthOnly:
1✔
472
                // For auth-only censuses, no contact method or challenge is needed
1✔
473
                return "", "", nil
1✔
NEW
474
        default:
×
NEW
475
                return "", "", errors.ErrNotSupported.Withf("invalid census type")
×
476
        }
477
}
478

479
// authFirstStep is the first step of the authentication process. It receives
480
// the request, the bundle ID and the census ID as parameters. It checks the
481
// request data (participant ID, email and phone) against the census data.
482
// If the data is valid, it generates a token with the bundle ID, the
483
// participant ID as the user ID, the contact information as the
484
// destination and the challenge type. It returns the token and an error if
485
// any. It sends the challenge to the user (email or SMS) to verify the user
486
// token in the second step.
487
func (c *CSPHandlers) authFirstStep(
488
        r *http.Request,
489
        bundleID internal.HexBytes,
490
        censusID string,
491
) (internal.HexBytes, error) {
15✔
492
        // Parse request
15✔
493
        var req AuthRequest
15✔
494
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
15✔
495
                return nil, errors.ErrMalformedBody.Withf("invalid JSON request")
×
496
        }
×
497

498
        // Get census information first (needed for validation)
499
        census, err := c.mainDB.Census(censusID)
15✔
500
        if err != nil {
15✔
NEW
501
                if err == db.ErrNotFound {
×
NEW
502
                        return nil, errors.ErrCensusNotFound
×
NEW
503
                }
×
NEW
504
                return nil, errors.ErrGenericInternalServerError.WithErr(err)
×
505
        }
506

507
        // Validate request with census information
508
        if err := validateAuthRequest(&req, census); err != nil {
16✔
509
                return nil, err
1✔
510
        }
1✔
511

512
        // create an empty member and assign the input data where applicable
513
        inputMember := &db.OrgMember{
14✔
514
                OrgAddress:   census.OrgAddress,
14✔
515
                Name:         req.Name,
14✔
516
                Surname:      req.Surname,
14✔
517
                MemberNumber: req.MemberNumber,
14✔
518
                NationalID:   req.NationalID,
14✔
519
                BirthDate:    req.BirthDate,
14✔
520
                Email:        req.Email,
14✔
521
                Phone:        req.Phone,
14✔
522
        }
14✔
523

14✔
524
        // Get participant information using the already retrieved census
14✔
525
        loginHash := db.HashAuthTwoFaFields(*inputMember, census.AuthFields, census.TwoFaFields)
14✔
526

14✔
527
        // Check the participant is in the census
14✔
528
        censuParticipant, err := c.mainDB.CensusParticipantByLoginHash(censusID, loginHash)
14✔
529
        if err != nil {
20✔
530
                if err == db.ErrNotFound {
12✔
531
                        return nil, errors.ErrUnauthorized.Withf("participant not found in the census")
6✔
532
                }
6✔
NEW
533
                return nil, errors.ErrGenericInternalServerError.WithErr(err)
×
534
        }
535

536
        // Fetch the corresponding org member using the participant ID (which is the ObjectID hex string)
537
        orgMember, err := c.mainDB.OrgMember(census.OrgAddress, censuParticipant.ParticipantID)
8✔
538
        if err != nil {
8✔
NEW
539
                return nil, fmt.Errorf("failed to get org member: %w", err)
×
UNCOV
540
        }
×
541

542
        // Determine contact method based on census type
543
        toDestinations, challengeType, err := determineContactMethod(census, &req, orgMember)
8✔
544
        if err != nil {
8✔
UNCOV
545
                return nil, err
×
UNCOV
546
        }
×
547

548
        // Generate the token
549
        return c.csp.BundleAuthToken(bundleID, internal.HexBytes(orgMember.MemberNumber), toDestinations, challengeType)
8✔
550
}
551

552
// authSecondStep is the second step of the authentication process. It
553
// receives the request and checks the token and the challenge solution
554
// against the server data. If the data is valid, it returns the token and
555
// an error if any. It the solution is valid, the token is marked as verified
556
// and returned to the user. The user can use the token to sign the bundle
557
// processes.
558
func (c *CSPHandlers) authSecondStep(r *http.Request) (internal.HexBytes, error) {
7✔
559
        var req AuthChallengeRequest
7✔
560
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
7✔
561
                return nil, errors.ErrMalformedBody.Withf("invalid JSON request")
×
562
        }
×
563

564
        // For tokens that require challenge verification, check if AuthData is provided
565
        if len(req.AuthData) == 0 {
8✔
566
                // Check if this is an auth-only token that's already verified
1✔
567
                auth, err := c.csp.Storage.CSPAuth(req.AuthToken)
1✔
568
                if err != nil {
1✔
NEW
569
                        return nil, errors.ErrUnauthorized.WithErr(err)
×
NEW
570
                }
×
571

572
                // Only allow pre-verified tokens if they're from auth-only censuses
573
                if auth.Verified {
2✔
574
                        return req.AuthToken, nil
1✔
575
                }
1✔
576

NEW
577
                return nil, errors.ErrInvalidUserData.Withf("challenge solution required")
×
578
        }
579

580
        switch err := c.csp.VerifyBundleAuthToken(req.AuthToken, req.AuthData[0]); err {
6✔
581
        case nil:
5✔
582
                return req.AuthToken, nil
5✔
583
        case csp.ErrInvalidAuthToken, csp.ErrInvalidSolution, csp.ErrChallengeCodeFailure:
1✔
584
                return nil, errors.ErrUnauthorized.WithErr(err)
1✔
585
        case csp.ErrUserUnknown, csp.ErrUserNotBelongsToBundle:
×
586
                return nil, errors.ErrUserNotFound.WithErr(err)
×
587
        case csp.ErrStorageFailure:
×
588
                return nil, errors.ErrInternalStorageError.WithErr(err)
×
589
        default:
×
590
                return nil, errors.ErrGenericInternalServerError.WithErr(err)
×
591
        }
592
}
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