@@ -14,6 +14,7 @@ import (
1414 "net/http"
1515 "net/http/httptest"
1616 "net/url"
17+ "strconv"
1718 "strings"
1819 "testing"
1920
@@ -33,6 +34,7 @@ import (
3334 "gitea.dev/tests"
3435
3536 "github.com/PuerkitoBio/goquery"
37+ jwt "github.com/golang-jwt/jwt/v5"
3638 "github.com/markbates/goth"
3739 "github.com/markbates/goth/gothic"
3840 "github.com/stretchr/testify/assert"
@@ -122,6 +124,7 @@ func TestOAuth2(t *testing.T) {
122124 t .Run ("RefreshTokenInvalidation" , testRefreshTokenInvalidation )
123125 t .Run ("RefreshTokenCrossClientUsage" , testRefreshTokenCrossClientUsage )
124126 t .Run ("OAuthIntrospection" , testOAuthIntrospection )
127+ t .Run ("OAuthIntrospectionCrossClientIsolation" , testOAuthIntrospectionCrossClientIsolation )
125128 t .Run ("OAuthGrantScopesReadUserFailRepos" , testOAuthGrantScopesReadUserFailRepos )
126129 t .Run ("OAuthGrantScopesBasicRespectsWriteUser" , testOAuthGrantScopesBasicRespectsWriteUser )
127130 t .Run ("OAuthGrantScopesReadRepositoryFailOrganization" , testOAuthGrantScopesReadRepositoryFailOrganization )
@@ -705,6 +708,79 @@ func testOAuthIntrospection(t *testing.T) {
705708 assert .Contains (t , resp .Body .String (), "no valid authorization" )
706709}
707710
711+ func testOAuthIntrospectionCrossClientIsolation (t * testing.T ) {
712+ resourceOwner := unittest .AssertExistsAndLoadBean (t , & user_model.User {ID : 2 })
713+ clientA := createOAuthTestApplication (t , "user1" , "introspection-primary-client" , []string {"https://primary.example/oauth/callback" })
714+ clientB := createOAuthTestApplication (t , "user2" , "introspection-secondary-client" , []string {"https://secondary.example/oauth/callback" })
715+ code , verifier := issueOAuthAuthorizationCode (t , resourceOwner , clientA , clientA .RedirectURIs [0 ], "openid profile" )
716+
717+ req := NewRequestWithValues (t , "POST" , "/login/oauth/access_token" , map [string ]string {
718+ "grant_type" : "authorization_code" ,
719+ "client_id" : clientA .ClientID ,
720+ "client_secret" : clientA .ClientSecret ,
721+ "redirect_uri" : clientA .RedirectURIs [0 ],
722+ "code" : code ,
723+ "code_verifier" : verifier ,
724+ })
725+ resp := MakeRequest (t , req , http .StatusOK )
726+ type tokenResponse struct {
727+ AccessToken string `json:"access_token"`
728+ RefreshToken string `json:"refresh_token"`
729+ }
730+ tokenParsed := new (tokenResponse )
731+ require .NoError (t , json .Unmarshal (resp .Body .Bytes (), tokenParsed ))
732+ require .NotEmpty (t , tokenParsed .AccessToken )
733+ require .NotEmpty (t , tokenParsed .RefreshToken )
734+
735+ type introspectResponse struct {
736+ Active bool `json:"active"`
737+ Scope string `json:"scope,omitempty"`
738+ Username string `json:"username,omitempty"`
739+ jwt.RegisteredClaims
740+ }
741+
742+ assertBlockedIntrospection := func (token string ) {
743+ t .Helper ()
744+
745+ req = NewRequestWithValues (t , "POST" , "/login/oauth/introspect" , map [string ]string {
746+ "token" : token ,
747+ })
748+ req .SetBasicAuth (clientB .ClientID , clientB .ClientSecret )
749+ resp = MakeRequest (t , req , http .StatusOK )
750+
751+ blocked := new (introspectResponse )
752+ require .NoError (t , json .Unmarshal (resp .Body .Bytes (), blocked ))
753+ assert .False (t , blocked .Active )
754+ assert .Empty (t , blocked .Scope )
755+ assert .Empty (t , blocked .Username )
756+ assert .Empty (t , blocked .Subject )
757+ assert .Empty (t , blocked .Audience )
758+ }
759+
760+ assertAllowedIntrospection := func (token string ) {
761+ t .Helper ()
762+
763+ req = NewRequestWithValues (t , "POST" , "/login/oauth/introspect" , map [string ]string {
764+ "token" : token ,
765+ })
766+ req .SetBasicAuth (clientA .ClientID , clientA .ClientSecret )
767+ resp = MakeRequest (t , req , http .StatusOK )
768+
769+ allowed := new (introspectResponse )
770+ require .NoError (t , json .Unmarshal (resp .Body .Bytes (), allowed ))
771+ assert .True (t , allowed .Active )
772+ assert .Equal (t , "openid profile" , allowed .Scope )
773+ assert .Equal (t , resourceOwner .Name , allowed .Username )
774+ assert .Equal (t , strconv .FormatInt (resourceOwner .ID , 10 ), allowed .Subject )
775+ assert .Equal (t , jwt.ClaimStrings {clientA .ClientID }, allowed .Audience )
776+ }
777+
778+ assertBlockedIntrospection (tokenParsed .AccessToken )
779+ assertAllowedIntrospection (tokenParsed .AccessToken )
780+ assertBlockedIntrospection (tokenParsed .RefreshToken )
781+ assertAllowedIntrospection (tokenParsed .RefreshToken )
782+ }
783+
708784func testOAuthGrantScopesReadUserFailRepos (t * testing.T ) {
709785 user := unittest .AssertExistsAndLoadBean (t , & user_model.User {ID : 2 })
710786 accessToken := issueOAuthAccessTokenForScope (t , user , "openid read:user" )
0 commit comments