1
1
package controller
2
2
3
3
import (
4
- "context"
4
+ "bytes"
5
+ "encoding/base64"
5
6
"encoding/json"
6
7
"errors"
7
8
"fmt"
8
- "github.com/gravitl/netmaker/db"
9
+ "github.com/pquerna/otp"
10
+ "image/png"
9
11
"net/http"
10
12
"reflect"
11
13
"time"
@@ -20,6 +22,7 @@ import (
20
22
"github.com/gravitl/netmaker/mq"
21
23
"github.com/gravitl/netmaker/schema"
22
24
"github.com/gravitl/netmaker/servercfg"
25
+ "github.com/pquerna/otp/totp"
23
26
"golang.org/x/exp/slog"
24
27
)
25
28
@@ -35,6 +38,9 @@ func userHandlers(r *mux.Router) {
35
38
r .HandleFunc ("/api/users/adm/transfersuperadmin/{username}" , logic .SecurityCheck (true , http .HandlerFunc (transferSuperAdmin ))).
36
39
Methods (http .MethodPost )
37
40
r .HandleFunc ("/api/users/adm/authenticate" , authenticateUser ).Methods (http .MethodPost )
41
+ r .HandleFunc ("/api/users/{username}/auth/init-totp" , logic .SecurityCheck (false , logic .ContinueIfUserMatch (http .HandlerFunc (initiateTOTPSetup )))).Methods (http .MethodPost )
42
+ r .HandleFunc ("/api/users/{username}/auth/complete-totp" , logic .SecurityCheck (false , logic .ContinueIfUserMatch (http .HandlerFunc (completeTOTPSetup )))).Methods (http .MethodPost )
43
+ r .HandleFunc ("/api/users/{username}/auth/verify-totp" , logic .PreAuthCheck (logic .ContinueIfUserMatch (http .HandlerFunc (verifyTOTP )))).Methods (http .MethodPost )
38
44
r .HandleFunc ("/api/users/{username}" , logic .SecurityCheck (true , http .HandlerFunc (updateUser ))).Methods (http .MethodPut )
39
45
r .HandleFunc ("/api/users/{username}" , logic .SecurityCheck (true , checkFreeTierLimits (limitChoiceUsers , http .HandlerFunc (createUser )))).Methods (http .MethodPost )
40
46
r .HandleFunc ("/api/users/{username}" , logic .SecurityCheck (true , http .HandlerFunc (deleteUser ))).Methods (http .MethodDelete )
@@ -356,14 +362,28 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
356
362
return
357
363
}
358
364
359
- var successResponse = models.SuccessResponse {
360
- Code : http .StatusOK ,
361
- Message : "W1R3: Device " + username + " Authorized" ,
362
- Response : models.SuccessfulUserLoginResponse {
363
- AuthToken : jwt ,
364
- UserName : username ,
365
- },
365
+ var successResponse models.SuccessResponse
366
+
367
+ if user .IsMFAEnabled {
368
+ successResponse = models.SuccessResponse {
369
+ Code : http .StatusOK ,
370
+ Message : "W1R3: TOTP required" ,
371
+ Response : models.PartialUserLoginResponse {
372
+ UserName : username ,
373
+ PreAuthToken : jwt ,
374
+ },
375
+ }
376
+ } else {
377
+ successResponse = models.SuccessResponse {
378
+ Code : http .StatusOK ,
379
+ Message : "W1R3: Device " + username + " Authorized" ,
380
+ Response : models.SuccessfulUserLoginResponse {
381
+ UserName : username ,
382
+ AuthToken : jwt ,
383
+ },
384
+ }
366
385
}
386
+
367
387
// Send back the JWT
368
388
successJSONResponse , jsonError := json .Marshal (successResponse )
369
389
if jsonError != nil {
@@ -414,6 +434,201 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
414
434
}()
415
435
}
416
436
437
+ // @Summary Initiate setting up TOTP 2FA for a user.
438
+ // @Router /api/users/auth/init-totp [post]
439
+ // @Tags Auth
440
+ // @Success 200 {object} models.SuccessResponse
441
+ // @Failure 400 {object} models.ErrorResponse
442
+ // @Failure 500 {object} models.ErrorResponse
443
+ func initiateTOTPSetup (w http.ResponseWriter , r * http.Request ) {
444
+ username := r .Header .Get ("user" )
445
+
446
+ user , err := logic .GetUser (username )
447
+ if err != nil {
448
+ logger .Log (0 , "failed to get user: " , err .Error ())
449
+ err = fmt .Errorf ("user not found: %v" , err )
450
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
451
+ return
452
+ }
453
+
454
+ if user .AuthType == models .OAuth {
455
+ err = fmt .Errorf ("auth type is %s, cannot process totp setup" , user .AuthType )
456
+ logger .Log (0 , err .Error ())
457
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
458
+ return
459
+ }
460
+
461
+ key , err := totp .Generate (totp.GenerateOpts {
462
+ Issuer : "Netmaker" ,
463
+ AccountName : username ,
464
+ })
465
+ if err != nil {
466
+ err = fmt .Errorf ("failed to generate totp key: %v" , err )
467
+ logger .Log (0 , err .Error ())
468
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "internal" ))
469
+ return
470
+ }
471
+
472
+ qrCodeImg , err := key .Image (200 , 200 )
473
+ if err != nil {
474
+ err = fmt .Errorf ("failed to generate totp key: %v" , err )
475
+ logger .Log (0 , err .Error ())
476
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "internal" ))
477
+ return
478
+ }
479
+
480
+ var qrCodePng bytes.Buffer
481
+ err = png .Encode (& qrCodePng , qrCodeImg )
482
+ if err != nil {
483
+ err = fmt .Errorf ("failed to generate totp key: %v" , err )
484
+ logger .Log (0 , err .Error ())
485
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "internal" ))
486
+ return
487
+ }
488
+
489
+ qrCode := "data:image/png;base64," + base64 .StdEncoding .EncodeToString (qrCodePng .Bytes ())
490
+
491
+ logic .ReturnSuccessResponseWithJson (w , r , models.TOTPInitiateResponse {
492
+ OTPAuthURL : key .URL (),
493
+ OTPAuthURLSignature : logic .GenerateOTPAuthURLSignature (key .URL ()),
494
+ QRCode : qrCode ,
495
+ }, "totp setup initiated" )
496
+ }
497
+
498
+ // @Summary Verify and complete setting up TOTP 2FA for a user.
499
+ // @Router /api/users/auth/complete-totp [post]
500
+ // @Tags Auth
501
+ // @Param body body models.UserTOTPVerificationParams true "TOTP verification parameters"
502
+ // @Success 200 {object} models.SuccessResponse
503
+ // @Failure 400 {object} models.ErrorResponse
504
+ // @Failure 500 {object} models.ErrorResponse
505
+ func completeTOTPSetup (w http.ResponseWriter , r * http.Request ) {
506
+ username := r .Header .Get ("user" )
507
+
508
+ var req models.UserTOTPVerificationParams
509
+ err := json .NewDecoder (r .Body ).Decode (& req )
510
+ if err != nil {
511
+ logger .Log (0 , "failed to decode request body: " , err .Error ())
512
+ err = fmt .Errorf ("invalid request body: %v" , err )
513
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
514
+ return
515
+ }
516
+
517
+ if ! logic .VerifyOTPAuthURL (req .OTPAuthURL , req .OTPAuthURLSignature ) {
518
+ err = fmt .Errorf ("otp auth url signature mismatch" )
519
+ logger .Log (0 , err .Error ())
520
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
521
+ return
522
+ }
523
+
524
+ user , err := logic .GetUser (username )
525
+ if err != nil {
526
+ logger .Log (0 , "failed to get user: " , err .Error ())
527
+ err = fmt .Errorf ("user not found: %v" , err )
528
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
529
+ return
530
+ }
531
+
532
+ if user .AuthType == models .OAuth {
533
+ err = fmt .Errorf ("auth type is %s, cannot process totp setup" , user .AuthType )
534
+ logger .Log (0 , err .Error ())
535
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
536
+ return
537
+ }
538
+
539
+ otpAuthURL , err := otp .NewKeyFromURL (req .OTPAuthURL )
540
+ if err != nil {
541
+ err = fmt .Errorf ("error parsing otp auth url: %v" , err )
542
+ logger .Log (0 , err .Error ())
543
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
544
+ return
545
+ }
546
+
547
+ totpSecret := otpAuthURL .Secret ()
548
+
549
+ if totp .Validate (req .TOTP , totpSecret ) {
550
+ user .IsMFAEnabled = true
551
+ user .TOTPSecret = totpSecret
552
+ err = logic .UpsertUser (* user )
553
+ if err != nil {
554
+ err = fmt .Errorf ("error upserting user: %v" , err )
555
+ logger .Log (0 , err .Error ())
556
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "internal" ))
557
+ return
558
+ }
559
+
560
+ logic .ReturnSuccessResponse (w , r , fmt .Sprintf ("totp setup complete for user %s" , username ))
561
+ } else {
562
+ err = fmt .Errorf ("cannot setup totp for user %s: invalid otp" , username )
563
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
564
+ }
565
+ }
566
+
567
+ // @Summary Verify a user's TOTP token.
568
+ // @Router /api/users/auth/verify-totp [post]
569
+ // @Tags Auth
570
+ // @Accept json
571
+ // @Param body body models.UserTOTPVerificationParams true "TOTP verification parameters"
572
+ // @Success 200 {object} models.SuccessResponse
573
+ // @Failure 400 {object} models.ErrorResponse
574
+ // @Failure 401 {object} models.ErrorResponse
575
+ // @Failure 500 {object} models.ErrorResponse
576
+ func verifyTOTP (w http.ResponseWriter , r * http.Request ) {
577
+ username := r .Header .Get ("user" )
578
+
579
+ var req models.UserTOTPVerificationParams
580
+ err := json .NewDecoder (r .Body ).Decode (& req )
581
+ if err != nil {
582
+ logger .Log (0 , "failed to decode request body: " , err .Error ())
583
+ err = fmt .Errorf ("invalid request body: %v" , err )
584
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
585
+ return
586
+ }
587
+
588
+ user , err := logic .GetUser (username )
589
+ if err != nil {
590
+ logger .Log (0 , "failed to get user: " , err .Error ())
591
+ err = fmt .Errorf ("user not found: %v" , err )
592
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
593
+ return
594
+ }
595
+
596
+ if ! user .IsMFAEnabled {
597
+ err = fmt .Errorf ("mfa is disabled for user(%s), cannot process totp verification" , username )
598
+ logger .Log (0 , err .Error ())
599
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "badrequest" ))
600
+ return
601
+ }
602
+
603
+ if totp .Validate (req .TOTP , user .TOTPSecret ) {
604
+ jwt , err := logic .CreateUserJWT (user .UserName , user .PlatformRoleID )
605
+ if err != nil {
606
+ err = fmt .Errorf ("error creating token: %v" , err )
607
+ logger .Log (0 , err .Error ())
608
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "internal" ))
609
+ return
610
+ }
611
+
612
+ // update last login time
613
+ user .LastLoginTime = time .Now ().UTC ()
614
+ err = logic .UpsertUser (* user )
615
+ if err != nil {
616
+ err = fmt .Errorf ("error upserting user: %v" , err )
617
+ logger .Log (0 , err .Error ())
618
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "internal" ))
619
+ return
620
+ }
621
+
622
+ logic .ReturnSuccessResponseWithJson (w , r , models.SuccessfulUserLoginResponse {
623
+ UserName : username ,
624
+ AuthToken : jwt ,
625
+ }, "W1R3: User " + username + " Authorized" )
626
+ } else {
627
+ err = fmt .Errorf ("invalid otp" )
628
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "unauthorized" ))
629
+ }
630
+ }
631
+
417
632
// @Summary Check if the server has a super admin
418
633
// @Router /api/users/adm/hassuperadmin [get]
419
634
// @Tags Users
@@ -586,18 +801,6 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
586
801
return
587
802
}
588
803
589
- for i , user := range users {
590
- // only setting num_access_tokens here, because only UI needs it.
591
- user .NumAccessTokens , err = (& schema.UserAccessToken {
592
- UserName : user .UserName ,
593
- }).CountByUser (db .WithContext (context .TODO ()))
594
- if err != nil {
595
- continue
596
- }
597
-
598
- users [i ] = user
599
- }
600
-
601
804
logic .SortUsers (users [:])
602
805
logger .Log (2 , r .Header .Get ("user" ), "fetched users" )
603
806
json .NewEncoder (w ).Encode (users )
@@ -884,6 +1087,14 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
884
1087
return
885
1088
886
1089
}
1090
+
1091
+ if logic .IsMFAEnforced () && user .IsMFAEnabled && ! userchange .IsMFAEnabled {
1092
+ err = errors .New ("mfa is enforced, user cannot unset their own mfa" )
1093
+ slog .Error ("failed to update user" , "caller" , caller .UserName , "attempted to update user" , username , "error" , err )
1094
+ logic .ReturnErrorResponse (w , r , logic .FormatError (err , "forbidden" ))
1095
+ return
1096
+ }
1097
+
887
1098
if servercfg .IsPro {
888
1099
// user cannot update his own roles and groups
889
1100
if len (user .NetworkRoles ) != len (userchange .NetworkRoles ) || ! reflect .DeepEqual (user .NetworkRoles , userchange .NetworkRoles ) {
@@ -900,7 +1111,6 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
900
1111
return
901
1112
}
902
1113
}
903
-
904
1114
}
905
1115
if ismaster {
906
1116
if user .PlatformRoleID != models .SuperAdminRole && userchange .PlatformRoleID == models .SuperAdminRole {
@@ -920,6 +1130,11 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
920
1130
(& schema.UserAccessToken {UserName : user .UserName }).DeleteAllUserTokens (r .Context ())
921
1131
}
922
1132
oldUser := * user
1133
+ if ismaster {
1134
+ caller = & models.User {
1135
+ UserName : logic .MasterUser ,
1136
+ }
1137
+ }
923
1138
e := models.Event {
924
1139
Action : models .Update ,
925
1140
Source : models.Subject {
0 commit comments