@@ -5,16 +5,25 @@ package integration
5
5
6
6
import (
7
7
"bytes"
8
+ "encoding/base64"
9
+ "fmt"
8
10
"io"
9
11
"net/http"
12
+ "strings"
10
13
"testing"
11
14
15
+ auth_model "code.gitea.io/gitea/models/auth"
16
+ "code.gitea.io/gitea/models/db"
17
+ "code.gitea.io/gitea/models/unittest"
18
+ user_model "code.gitea.io/gitea/models/user"
12
19
"code.gitea.io/gitea/modules/json"
13
20
"code.gitea.io/gitea/modules/setting"
21
+ api "code.gitea.io/gitea/modules/structs"
14
22
oauth2_provider "code.gitea.io/gitea/services/oauth2_provider"
15
23
"code.gitea.io/gitea/tests"
16
24
17
25
"github.com/stretchr/testify/assert"
26
+ "github.com/stretchr/testify/require"
18
27
)
19
28
20
29
func TestAuthorizeNoClientID (t * testing.T ) {
@@ -477,3 +486,424 @@ func TestOAuthIntrospection(t *testing.T) {
477
486
resp = MakeRequest (t , req , http .StatusUnauthorized )
478
487
assert .Contains (t , resp .Body .String (), "no valid authorization" )
479
488
}
489
+
490
+ func TestOAuth_GrantScopesReadUserFailRepos (t * testing.T ) {
491
+ defer tests .PrepareTestEnv (t )()
492
+
493
+ user := unittest .AssertExistsAndLoadBean (t , & user_model.User {ID : 2 })
494
+ appBody := api.CreateOAuth2ApplicationOptions {
495
+ Name : "oauth-provider-scopes-test" ,
496
+ RedirectURIs : []string {
497
+ "a" ,
498
+ },
499
+ ConfidentialClient : true ,
500
+ }
501
+
502
+ req := NewRequestWithJSON (t , "POST" , "/api/v1/user/applications/oauth2" , & appBody ).
503
+ AddBasicAuth (user .Name )
504
+ resp := MakeRequest (t , req , http .StatusCreated )
505
+
506
+ var app * api.OAuth2Application
507
+ DecodeJSON (t , resp , & app )
508
+
509
+ grant := & auth_model.OAuth2Grant {
510
+ ApplicationID : app .ID ,
511
+ UserID : user .ID ,
512
+ Scope : "openid read:user" ,
513
+ }
514
+
515
+ err := db .Insert (db .DefaultContext , grant )
516
+ require .NoError (t , err )
517
+
518
+ assert .Contains (t , grant .Scope , "openid read:user" )
519
+
520
+ ctx := loginUser (t , user .Name )
521
+
522
+ authorizeURL := fmt .Sprintf ("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate" , app .ClientID )
523
+ authorizeReq := NewRequest (t , "GET" , authorizeURL )
524
+ authorizeResp := ctx .MakeRequest (t , authorizeReq , http .StatusSeeOther )
525
+
526
+ authcode := strings .Split (strings .Split (authorizeResp .Body .String (), "?code=" )[1 ], "&" )[0 ]
527
+
528
+ accessTokenReq := NewRequestWithValues (t , "POST" , "/login/oauth/access_token" , map [string ]string {
529
+ "grant_type" : "authorization_code" ,
530
+ "client_id" : app .ClientID ,
531
+ "client_secret" : app .ClientSecret ,
532
+ "redirect_uri" : "a" ,
533
+ "code" : authcode ,
534
+ })
535
+ accessTokenResp := ctx .MakeRequest (t , accessTokenReq , 200 )
536
+ type response struct {
537
+ AccessToken string `json:"access_token"`
538
+ TokenType string `json:"token_type"`
539
+ ExpiresIn int64 `json:"expires_in"`
540
+ RefreshToken string `json:"refresh_token"`
541
+ }
542
+ parsed := new (response )
543
+
544
+ require .NoError (t , json .Unmarshal (accessTokenResp .Body .Bytes (), parsed ))
545
+ userReq := NewRequest (t , "GET" , "/api/v1/user" )
546
+ userReq .SetHeader ("Authorization" , "Bearer " + parsed .AccessToken )
547
+ userResp := MakeRequest (t , userReq , http .StatusOK )
548
+
549
+ type userResponse struct {
550
+ Login string `json:"login"`
551
+ Email string `json:"email"`
552
+ }
553
+
554
+ userParsed := new (userResponse )
555
+ require .NoError (t , json .Unmarshal (userResp .Body .Bytes (), userParsed ))
556
+ assert .Contains (t , userParsed .Email , "user2@example.com" )
557
+
558
+ errorReq := NewRequest (t , "GET" , "/api/v1/users/user2/repos" )
559
+ errorReq .SetHeader ("Authorization" , "Bearer " + parsed .AccessToken )
560
+ errorResp := MakeRequest (t , errorReq , http .StatusForbidden )
561
+
562
+ type errorResponse struct {
563
+ Message string `json:"message"`
564
+ }
565
+
566
+ errorParsed := new (errorResponse )
567
+ require .NoError (t , json .Unmarshal (errorResp .Body .Bytes (), errorParsed ))
568
+ assert .Contains (t , errorParsed .Message , "token does not have at least one of required scope(s): [read:repository]" )
569
+ }
570
+
571
+ func TestOAuth_GrantScopesReadRepositoryFailOrganization (t * testing.T ) {
572
+ defer tests .PrepareTestEnv (t )()
573
+
574
+ user := unittest .AssertExistsAndLoadBean (t , & user_model.User {ID : 2 })
575
+ appBody := api.CreateOAuth2ApplicationOptions {
576
+ Name : "oauth-provider-scopes-test" ,
577
+ RedirectURIs : []string {
578
+ "a" ,
579
+ },
580
+ ConfidentialClient : true ,
581
+ }
582
+
583
+ req := NewRequestWithJSON (t , "POST" , "/api/v1/user/applications/oauth2" , & appBody ).
584
+ AddBasicAuth (user .Name )
585
+ resp := MakeRequest (t , req , http .StatusCreated )
586
+
587
+ var app * api.OAuth2Application
588
+ DecodeJSON (t , resp , & app )
589
+
590
+ grant := & auth_model.OAuth2Grant {
591
+ ApplicationID : app .ID ,
592
+ UserID : user .ID ,
593
+ Scope : "openid read:user read:repository" ,
594
+ }
595
+
596
+ err := db .Insert (db .DefaultContext , grant )
597
+ require .NoError (t , err )
598
+
599
+ assert .Contains (t , grant .Scope , "openid read:user read:repository" )
600
+
601
+ ctx := loginUser (t , user .Name )
602
+
603
+ authorizeURL := fmt .Sprintf ("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate" , app .ClientID )
604
+ authorizeReq := NewRequest (t , "GET" , authorizeURL )
605
+ authorizeResp := ctx .MakeRequest (t , authorizeReq , http .StatusSeeOther )
606
+
607
+ authcode := strings .Split (strings .Split (authorizeResp .Body .String (), "?code=" )[1 ], "&" )[0 ]
608
+ accessTokenReq := NewRequestWithValues (t , "POST" , "/login/oauth/access_token" , map [string ]string {
609
+ "grant_type" : "authorization_code" ,
610
+ "client_id" : app .ClientID ,
611
+ "client_secret" : app .ClientSecret ,
612
+ "redirect_uri" : "a" ,
613
+ "code" : authcode ,
614
+ })
615
+ accessTokenResp := ctx .MakeRequest (t , accessTokenReq , http .StatusOK )
616
+ type response struct {
617
+ AccessToken string `json:"access_token"`
618
+ TokenType string `json:"token_type"`
619
+ ExpiresIn int64 `json:"expires_in"`
620
+ RefreshToken string `json:"refresh_token"`
621
+ }
622
+ parsed := new (response )
623
+
624
+ require .NoError (t , json .Unmarshal (accessTokenResp .Body .Bytes (), parsed ))
625
+ userReq := NewRequest (t , "GET" , "/api/v1/users/user2/repos" )
626
+ userReq .SetHeader ("Authorization" , "Bearer " + parsed .AccessToken )
627
+ userResp := MakeRequest (t , userReq , http .StatusOK )
628
+
629
+ type repo struct {
630
+ FullRepoName string `json:"full_name"`
631
+ Private bool `json:"private"`
632
+ }
633
+
634
+ var reposCaptured []repo
635
+ require .NoError (t , json .Unmarshal (userResp .Body .Bytes (), & reposCaptured ))
636
+
637
+ reposExpected := []repo {
638
+ {
639
+ FullRepoName : "user2/repo1" ,
640
+ Private : false ,
641
+ },
642
+ {
643
+ FullRepoName : "user2/repo2" ,
644
+ Private : true ,
645
+ },
646
+ {
647
+ FullRepoName : "user2/repo15" ,
648
+ Private : true ,
649
+ },
650
+ {
651
+ FullRepoName : "user2/repo16" ,
652
+ Private : true ,
653
+ },
654
+ {
655
+ FullRepoName : "user2/repo20" ,
656
+ Private : true ,
657
+ },
658
+ {
659
+ FullRepoName : "user2/utf8" ,
660
+ Private : false ,
661
+ },
662
+ {
663
+ FullRepoName : "user2/commits_search_test" ,
664
+ Private : false ,
665
+ },
666
+ {
667
+ FullRepoName : "user2/git_hooks_test" ,
668
+ Private : false ,
669
+ },
670
+ {
671
+ FullRepoName : "user2/glob" ,
672
+ Private : false ,
673
+ },
674
+ {
675
+ FullRepoName : "user2/lfs" ,
676
+ Private : true ,
677
+ },
678
+ {
679
+ FullRepoName : "user2/scoped_label" ,
680
+ Private : true ,
681
+ },
682
+ {
683
+ FullRepoName : "user2/readme-test" ,
684
+ Private : true ,
685
+ },
686
+ {
687
+ FullRepoName : "user2/repo-release" ,
688
+ Private : false ,
689
+ },
690
+ {
691
+ FullRepoName : "user2/commitsonpr" ,
692
+ Private : false ,
693
+ },
694
+ {
695
+ FullRepoName : "user2/test_commit_revert" ,
696
+ Private : true ,
697
+ },
698
+ }
699
+ assert .Equal (t , reposExpected , reposCaptured )
700
+
701
+ errorReq := NewRequest (t , "GET" , "/api/v1/users/user2/orgs" )
702
+ errorReq .SetHeader ("Authorization" , "Bearer " + parsed .AccessToken )
703
+ errorResp := MakeRequest (t , errorReq , http .StatusForbidden )
704
+
705
+ type errorResponse struct {
706
+ Message string `json:"message"`
707
+ }
708
+
709
+ errorParsed := new (errorResponse )
710
+ require .NoError (t , json .Unmarshal (errorResp .Body .Bytes (), errorParsed ))
711
+ assert .Contains (t , errorParsed .Message , "token does not have at least one of required scope(s): [read:user read:organization]" )
712
+ }
713
+
714
+ func TestOAuth_GrantScopesClaimPublicOnlyGroups (t * testing.T ) {
715
+ defer tests .PrepareTestEnv (t )()
716
+
717
+ user := unittest .AssertExistsAndLoadBean (t , & user_model.User {Name : "user2" })
718
+
719
+ appBody := api.CreateOAuth2ApplicationOptions {
720
+ Name : "oauth-provider-scopes-test" ,
721
+ RedirectURIs : []string {
722
+ "a" ,
723
+ },
724
+ ConfidentialClient : true ,
725
+ }
726
+
727
+ appReq := NewRequestWithJSON (t , "POST" , "/api/v1/user/applications/oauth2" , & appBody ).
728
+ AddBasicAuth (user .Name )
729
+ appResp := MakeRequest (t , appReq , http .StatusCreated )
730
+
731
+ var app * api.OAuth2Application
732
+ DecodeJSON (t , appResp , & app )
733
+
734
+ grant := & auth_model.OAuth2Grant {
735
+ ApplicationID : app .ID ,
736
+ UserID : user .ID ,
737
+ Scope : "openid groups read:user public-only" ,
738
+ }
739
+
740
+ err := db .Insert (db .DefaultContext , grant )
741
+ require .NoError (t , err )
742
+
743
+ assert .ElementsMatch (t , []string {"openid" , "groups" , "read:user" , "public-only" }, strings .Split (grant .Scope , " " ))
744
+
745
+ ctx := loginUser (t , user .Name )
746
+
747
+ authorizeURL := fmt .Sprintf ("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate" , app .ClientID )
748
+ authorizeReq := NewRequest (t , "GET" , authorizeURL )
749
+ authorizeResp := ctx .MakeRequest (t , authorizeReq , http .StatusSeeOther )
750
+
751
+ authcode := strings .Split (strings .Split (authorizeResp .Body .String (), "?code=" )[1 ], "&" )[0 ]
752
+
753
+ accessTokenReq := NewRequestWithValues (t , "POST" , "/login/oauth/access_token" , map [string ]string {
754
+ "grant_type" : "authorization_code" ,
755
+ "client_id" : app .ClientID ,
756
+ "client_secret" : app .ClientSecret ,
757
+ "redirect_uri" : "a" ,
758
+ "code" : authcode ,
759
+ })
760
+ accessTokenResp := ctx .MakeRequest (t , accessTokenReq , http .StatusOK )
761
+ type response struct {
762
+ AccessToken string `json:"access_token"`
763
+ TokenType string `json:"token_type"`
764
+ ExpiresIn int64 `json:"expires_in"`
765
+ RefreshToken string `json:"refresh_token"`
766
+ IDToken string `json:"id_token,omitempty"`
767
+ }
768
+ parsed := new (response )
769
+ require .NoError (t , json .Unmarshal (accessTokenResp .Body .Bytes (), parsed ))
770
+ parts := strings .Split (parsed .IDToken , "." )
771
+
772
+ payload , _ := base64 .RawURLEncoding .DecodeString (parts [1 ])
773
+ type IDTokenClaims struct {
774
+ Groups []string `json:"groups"`
775
+ }
776
+
777
+ claims := new (IDTokenClaims )
778
+ require .NoError (t , json .Unmarshal (payload , claims ))
779
+
780
+ userinfoReq := NewRequest (t , "GET" , "/login/oauth/userinfo" )
781
+ userinfoReq .SetHeader ("Authorization" , "Bearer " + parsed .AccessToken )
782
+ userinfoResp := MakeRequest (t , userinfoReq , http .StatusOK )
783
+
784
+ type userinfoResponse struct {
785
+ Login string `json:"login"`
786
+ Email string `json:"email"`
787
+ Groups []string `json:"groups"`
788
+ }
789
+
790
+ userinfoParsed := new (userinfoResponse )
791
+ require .NoError (t , json .Unmarshal (userinfoResp .Body .Bytes (), userinfoParsed ))
792
+ assert .Contains (t , userinfoParsed .Email , "user2@example.com" )
793
+
794
+ // test both id_token and call to /login/oauth/userinfo
795
+ for _ , publicGroup := range []string {
796
+ "org17" ,
797
+ "org17:test_team" ,
798
+ "org3" ,
799
+ "org3:owners" ,
800
+ "org3:team1" ,
801
+ "org3:teamcreaterepo" ,
802
+ } {
803
+ assert .Contains (t , claims .Groups , publicGroup )
804
+ assert .Contains (t , userinfoParsed .Groups , publicGroup )
805
+ }
806
+ for _ , privateGroup := range []string {
807
+ "private_org35" ,
808
+ "private_org35_team24" ,
809
+ } {
810
+ assert .NotContains (t , claims .Groups , privateGroup )
811
+ assert .NotContains (t , userinfoParsed .Groups , privateGroup )
812
+ }
813
+ }
814
+
815
+ func TestOAuth_GrantScopesClaimAllGroups (t * testing.T ) {
816
+ defer tests .PrepareTestEnv (t )()
817
+
818
+ user := unittest .AssertExistsAndLoadBean (t , & user_model.User {Name : "user2" })
819
+
820
+ appBody := api.CreateOAuth2ApplicationOptions {
821
+ Name : "oauth-provider-scopes-test" ,
822
+ RedirectURIs : []string {
823
+ "a" ,
824
+ },
825
+ ConfidentialClient : true ,
826
+ }
827
+
828
+ appReq := NewRequestWithJSON (t , "POST" , "/api/v1/user/applications/oauth2" , & appBody ).
829
+ AddBasicAuth (user .Name )
830
+ appResp := MakeRequest (t , appReq , http .StatusCreated )
831
+
832
+ var app * api.OAuth2Application
833
+ DecodeJSON (t , appResp , & app )
834
+
835
+ grant := & auth_model.OAuth2Grant {
836
+ ApplicationID : app .ID ,
837
+ UserID : user .ID ,
838
+ Scope : "openid groups" ,
839
+ }
840
+
841
+ err := db .Insert (db .DefaultContext , grant )
842
+ require .NoError (t , err )
843
+
844
+ assert .ElementsMatch (t , []string {"openid" , "groups" }, strings .Split (grant .Scope , " " ))
845
+
846
+ ctx := loginUser (t , user .Name )
847
+
848
+ authorizeURL := fmt .Sprintf ("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate" , app .ClientID )
849
+ authorizeReq := NewRequest (t , "GET" , authorizeURL )
850
+ authorizeResp := ctx .MakeRequest (t , authorizeReq , http .StatusSeeOther )
851
+
852
+ authcode := strings .Split (strings .Split (authorizeResp .Body .String (), "?code=" )[1 ], "&" )[0 ]
853
+
854
+ accessTokenReq := NewRequestWithValues (t , "POST" , "/login/oauth/access_token" , map [string ]string {
855
+ "grant_type" : "authorization_code" ,
856
+ "client_id" : app .ClientID ,
857
+ "client_secret" : app .ClientSecret ,
858
+ "redirect_uri" : "a" ,
859
+ "code" : authcode ,
860
+ })
861
+ accessTokenResp := ctx .MakeRequest (t , accessTokenReq , http .StatusOK )
862
+ type response struct {
863
+ AccessToken string `json:"access_token"`
864
+ TokenType string `json:"token_type"`
865
+ ExpiresIn int64 `json:"expires_in"`
866
+ RefreshToken string `json:"refresh_token"`
867
+ IDToken string `json:"id_token,omitempty"`
868
+ }
869
+ parsed := new (response )
870
+ require .NoError (t , json .Unmarshal (accessTokenResp .Body .Bytes (), parsed ))
871
+ parts := strings .Split (parsed .IDToken , "." )
872
+
873
+ payload , _ := base64 .RawURLEncoding .DecodeString (parts [1 ])
874
+ type IDTokenClaims struct {
875
+ Groups []string `json:"groups"`
876
+ }
877
+
878
+ claims := new (IDTokenClaims )
879
+ require .NoError (t , json .Unmarshal (payload , claims ))
880
+
881
+ userinfoReq := NewRequest (t , "GET" , "/login/oauth/userinfo" )
882
+ userinfoReq .SetHeader ("Authorization" , "Bearer " + parsed .AccessToken )
883
+ userinfoResp := MakeRequest (t , userinfoReq , http .StatusOK )
884
+
885
+ type userinfoResponse struct {
886
+ Login string `json:"login"`
887
+ Email string `json:"email"`
888
+ Groups []string `json:"groups"`
889
+ }
890
+
891
+ userinfoParsed := new (userinfoResponse )
892
+ require .NoError (t , json .Unmarshal (userinfoResp .Body .Bytes (), userinfoParsed ))
893
+ assert .Contains (t , userinfoParsed .Email , "user2@example.com" )
894
+
895
+ // test both id_token and call to /login/oauth/userinfo
896
+ for _ , group := range []string {
897
+ "org17" ,
898
+ "org17:test_team" ,
899
+ "org3" ,
900
+ "org3:owners" ,
901
+ "org3:team1" ,
902
+ "org3:teamcreaterepo" ,
903
+ "private_org35" ,
904
+ "private_org35:team24" ,
905
+ } {
906
+ assert .Contains (t , claims .Groups , group )
907
+ assert .Contains (t , userinfoParsed .Groups , group )
908
+ }
909
+ }
0 commit comments