Skip to content
/ gitea Public
  • Sponsor go-gitea/gitea

  • Notifications You must be signed in to change notification settings
  • Fork 5.8k
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 74dfef3

Browse files
committedNov 20, 2024·
grant additional scope and integration tests
1 parent 9e1f9c4 commit 74dfef3

File tree

2 files changed

+465
-0
lines changed

2 files changed

+465
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package oauth2_provider //nolint
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestGrantAdditionalScopes(t *testing.T) {
13+
tests := []struct {
14+
grantScopes string
15+
expectedScopes string
16+
}{
17+
{"openid profile email", "all"},
18+
{"openid profile email groups", "all"},
19+
{"openid profile email all", "all"},
20+
{"openid profile email read:user all", "all"},
21+
{"openid profile email groups read:user", "read:user"},
22+
{"read:user read:repository", "read:repository,read:user"},
23+
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
24+
{"openid profile email read:user", "read:user"},
25+
{"read:invalid_scope", "all"},
26+
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
27+
}
28+
29+
for _, test := range tests {
30+
t.Run(test.grantScopes, func(t *testing.T) {
31+
result := GrantAdditionalScopes(test.grantScopes)
32+
assert.Equal(t, test.expectedScopes, string(result))
33+
})
34+
}
35+
}

‎tests/integration/oauth_test.go

+430
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,25 @@ package integration
55

66
import (
77
"bytes"
8+
"encoding/base64"
9+
"fmt"
810
"io"
911
"net/http"
12+
"strings"
1013
"testing"
1114

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"
1219
"code.gitea.io/gitea/modules/json"
1320
"code.gitea.io/gitea/modules/setting"
21+
api "code.gitea.io/gitea/modules/structs"
1422
oauth2_provider "code.gitea.io/gitea/services/oauth2_provider"
1523
"code.gitea.io/gitea/tests"
1624

1725
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
1827
)
1928

2029
func TestAuthorizeNoClientID(t *testing.T) {
@@ -477,3 +486,424 @@ func TestOAuthIntrospection(t *testing.T) {
477486
resp = MakeRequest(t, req, http.StatusUnauthorized)
478487
assert.Contains(t, resp.Body.String(), "no valid authorization")
479488
}
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], "&amp")[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], "&amp")[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], "&amp")[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], "&amp")[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

Comments
 (0)
Please sign in to comment.