Merge pull request 'Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access' (#4449) from marcellmars/forgejo:forgejo into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4449 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
		
				commit
				
					
						24eb401a0a
					
				
			
		
					 8 changed files with 662 additions and 30 deletions
				
			
		| 
						 | 
				
			
			@ -92,23 +92,25 @@ func parseScopes(sec ConfigSection, name string) []string {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
var OAuth2 = struct {
 | 
			
		||||
	Enabled                    bool
 | 
			
		||||
	AccessTokenExpirationTime  int64
 | 
			
		||||
	RefreshTokenExpirationTime int64
 | 
			
		||||
	InvalidateRefreshTokens    bool
 | 
			
		||||
	JWTSigningAlgorithm        string `ini:"JWT_SIGNING_ALGORITHM"`
 | 
			
		||||
	JWTSigningPrivateKeyFile   string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
 | 
			
		||||
	MaxTokenLength             int
 | 
			
		||||
	DefaultApplications        []string
 | 
			
		||||
	Enabled                     bool
 | 
			
		||||
	AccessTokenExpirationTime   int64
 | 
			
		||||
	RefreshTokenExpirationTime  int64
 | 
			
		||||
	InvalidateRefreshTokens     bool
 | 
			
		||||
	JWTSigningAlgorithm         string `ini:"JWT_SIGNING_ALGORITHM"`
 | 
			
		||||
	JWTSigningPrivateKeyFile    string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
 | 
			
		||||
	MaxTokenLength              int
 | 
			
		||||
	DefaultApplications         []string
 | 
			
		||||
	EnableAdditionalGrantScopes bool
 | 
			
		||||
}{
 | 
			
		||||
	Enabled:                    true,
 | 
			
		||||
	AccessTokenExpirationTime:  3600,
 | 
			
		||||
	RefreshTokenExpirationTime: 730,
 | 
			
		||||
	InvalidateRefreshTokens:    true,
 | 
			
		||||
	JWTSigningAlgorithm:        "RS256",
 | 
			
		||||
	JWTSigningPrivateKeyFile:   "jwt/private.pem",
 | 
			
		||||
	MaxTokenLength:             math.MaxInt16,
 | 
			
		||||
	DefaultApplications:        []string{"git-credential-oauth", "git-credential-manager", "tea"},
 | 
			
		||||
	Enabled:                     true,
 | 
			
		||||
	AccessTokenExpirationTime:   3600,
 | 
			
		||||
	RefreshTokenExpirationTime:  730,
 | 
			
		||||
	InvalidateRefreshTokens:     true,
 | 
			
		||||
	JWTSigningAlgorithm:         "RS256",
 | 
			
		||||
	JWTSigningPrivateKeyFile:    "jwt/private.pem",
 | 
			
		||||
	MaxTokenLength:              math.MaxInt16,
 | 
			
		||||
	DefaultApplications:         []string{"git-credential-oauth", "git-credential-manager", "tea"},
 | 
			
		||||
	EnableAdditionalGrantScopes: false,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func loadOAuth2From(rootCfg ConfigProvider) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -244,7 +244,9 @@ func newAccessTokenResponse(ctx go_context.Context, grant *auth.OAuth2Grant, ser
 | 
			
		|||
			idToken.EmailVerified = user.IsActive
 | 
			
		||||
		}
 | 
			
		||||
		if grant.ScopeContains("groups") {
 | 
			
		||||
			groups, err := getOAuthGroupsForUser(ctx, user)
 | 
			
		||||
			onlyPublicGroups := ifOnlyPublicGroups(grant.Scope)
 | 
			
		||||
 | 
			
		||||
			groups, err := getOAuthGroupsForUser(ctx, user, onlyPublicGroups)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("Error getting groups: %v", err)
 | 
			
		||||
				return nil, &AccessTokenError{
 | 
			
		||||
| 
						 | 
				
			
			@ -279,7 +281,18 @@ type userInfoResponse struct {
 | 
			
		|||
	Username string   `json:"preferred_username"`
 | 
			
		||||
	Email    string   `json:"email"`
 | 
			
		||||
	Picture  string   `json:"picture"`
 | 
			
		||||
	Groups   []string `json:"groups"`
 | 
			
		||||
	Groups   []string `json:"groups,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ifOnlyPublicGroups(scopes string) bool {
 | 
			
		||||
	scopes = strings.ReplaceAll(scopes, ",", " ")
 | 
			
		||||
	scopesList := strings.Fields(scopes)
 | 
			
		||||
	for _, scope := range scopesList {
 | 
			
		||||
		if scope == "all" || scope == "read:organization" || scope == "read:admin" {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InfoOAuth manages request for userinfo endpoint
 | 
			
		||||
| 
						 | 
				
			
			@ -298,7 +311,18 @@ func InfoOAuth(ctx *context.Context) {
 | 
			
		|||
		Picture:  ctx.Doer.AvatarLink(ctx),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	groups, err := getOAuthGroupsForUser(ctx, ctx.Doer)
 | 
			
		||||
	var token string
 | 
			
		||||
	if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
 | 
			
		||||
		auths := strings.Fields(auHead)
 | 
			
		||||
		if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
 | 
			
		||||
			token = auths[1]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, grantScopes := auth_service.CheckOAuthAccessToken(ctx, token)
 | 
			
		||||
	onlyPublicGroups := ifOnlyPublicGroups(grantScopes)
 | 
			
		||||
 | 
			
		||||
	groups, err := getOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("Oauth groups for user", err)
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -310,7 +334,7 @@ func InfoOAuth(ctx *context.Context) {
 | 
			
		|||
 | 
			
		||||
// returns a list of "org" and "org:team" strings,
 | 
			
		||||
// that the given user is a part of.
 | 
			
		||||
func getOAuthGroupsForUser(ctx go_context.Context, user *user_model.User) ([]string, error) {
 | 
			
		||||
func getOAuthGroupsForUser(ctx go_context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
 | 
			
		||||
	orgs, err := org_model.GetUserOrgsList(ctx, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("GetUserOrgList: %w", err)
 | 
			
		||||
| 
						 | 
				
			
			@ -318,6 +342,15 @@ func getOAuthGroupsForUser(ctx go_context.Context, user *user_model.User) ([]str
 | 
			
		|||
 | 
			
		||||
	var groups []string
 | 
			
		||||
	for _, org := range orgs {
 | 
			
		||||
		if setting.OAuth2.EnableAdditionalGrantScopes {
 | 
			
		||||
			if onlyPublicGroups {
 | 
			
		||||
				public, err := org_model.IsPublicMembership(ctx, org.ID, user.ID)
 | 
			
		||||
				if !public && err == nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		groups = append(groups, org.Name)
 | 
			
		||||
		teams, err := org.LoadTeams(ctx)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -110,5 +110,6 @@ func loadApplicationsData(ctx *context.Context) {
 | 
			
		|||
			ctx.ServerError("GetOAuth2GrantsByUserID", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Data["EnableAdditionalGrantScopes"] = setting.OAuth2.EnableAdditionalGrantScopes
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										32
									
								
								services/auth/additional_scopes_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								services/auth/additional_scopes_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGrantAdditionalScopes(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		grantScopes    string
 | 
			
		||||
		expectedScopes string
 | 
			
		||||
	}{
 | 
			
		||||
		{"openid profile email", ""},
 | 
			
		||||
		{"openid profile email groups", ""},
 | 
			
		||||
		{"openid profile email all", "all"},
 | 
			
		||||
		{"openid profile email read:user all", "read:user,all"},
 | 
			
		||||
		{"openid profile email groups read:user", "read:user"},
 | 
			
		||||
		{"read:user read:repository", "read:user,read:repository"},
 | 
			
		||||
		{"read:user write:issue public-only", "read:user,write:issue,public-only"},
 | 
			
		||||
		{"openid profile email read:user", "read:user"},
 | 
			
		||||
		{"read:invalid_scope", ""},
 | 
			
		||||
		{"read:invalid_scope,write:scope_invalid,just-plain-wrong", ""},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.grantScopes, func(t *testing.T) {
 | 
			
		||||
			result := grantAdditionalScopes(test.grantScopes)
 | 
			
		||||
			assert.Equal(t, test.expectedScopes, result)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +72,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	// check oauth2 token
 | 
			
		||||
	uid := CheckOAuthAccessToken(req.Context(), authToken)
 | 
			
		||||
	uid, _ := CheckOAuthAccessToken(req.Context(), authToken)
 | 
			
		||||
	if uid != 0 {
 | 
			
		||||
		log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ package auth
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -25,28 +26,69 @@ var (
 | 
			
		|||
	_ Method = &OAuth2{}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// grantAdditionalScopes returns valid scopes coming from grant
 | 
			
		||||
func grantAdditionalScopes(grantScopes string) string {
 | 
			
		||||
	// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
 | 
			
		||||
	scopesSupported := []string{
 | 
			
		||||
		"openid",
 | 
			
		||||
		"profile",
 | 
			
		||||
		"email",
 | 
			
		||||
		"groups",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var apiTokenScopes []string
 | 
			
		||||
	for _, apiTokenScope := range strings.Split(grantScopes, " ") {
 | 
			
		||||
		if slices.Index(scopesSupported, apiTokenScope) == -1 {
 | 
			
		||||
			apiTokenScopes = append(apiTokenScopes, apiTokenScope)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(apiTokenScopes) == 0 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var additionalGrantScopes []string
 | 
			
		||||
	allScopes := auth_model.AccessTokenScope("all")
 | 
			
		||||
 | 
			
		||||
	for _, apiTokenScope := range apiTokenScopes {
 | 
			
		||||
		grantScope := auth_model.AccessTokenScope(apiTokenScope)
 | 
			
		||||
		if ok, _ := allScopes.HasScope(grantScope); ok {
 | 
			
		||||
			additionalGrantScopes = append(additionalGrantScopes, apiTokenScope)
 | 
			
		||||
		} else if apiTokenScope == "public-only" {
 | 
			
		||||
			additionalGrantScopes = append(additionalGrantScopes, apiTokenScope)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(additionalGrantScopes) > 0 {
 | 
			
		||||
		return strings.Join(additionalGrantScopes, ",")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CheckOAuthAccessToken returns uid of user from oauth token
 | 
			
		||||
func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
 | 
			
		||||
// + non default openid scopes requested
 | 
			
		||||
func CheckOAuthAccessToken(ctx context.Context, accessToken string) (int64, string) {
 | 
			
		||||
	// JWT tokens require a "."
 | 
			
		||||
	if !strings.Contains(accessToken, ".") {
 | 
			
		||||
		return 0
 | 
			
		||||
		return 0, ""
 | 
			
		||||
	}
 | 
			
		||||
	token, err := oauth2.ParseToken(accessToken, oauth2.DefaultSigningKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Trace("oauth2.ParseToken: %v", err)
 | 
			
		||||
		return 0
 | 
			
		||||
		return 0, ""
 | 
			
		||||
	}
 | 
			
		||||
	var grant *auth_model.OAuth2Grant
 | 
			
		||||
	if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
 | 
			
		||||
		return 0
 | 
			
		||||
		return 0, ""
 | 
			
		||||
	}
 | 
			
		||||
	if token.Type != oauth2.TypeAccessToken {
 | 
			
		||||
		return 0
 | 
			
		||||
		return 0, ""
 | 
			
		||||
	}
 | 
			
		||||
	if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
 | 
			
		||||
		return 0
 | 
			
		||||
		return 0, ""
 | 
			
		||||
	}
 | 
			
		||||
	return grant.UserID
 | 
			
		||||
	grantScopes := grantAdditionalScopes(grant.Scope)
 | 
			
		||||
	return grant.UserID, grantScopes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OAuth2 implements the Auth interface and authenticates requests
 | 
			
		||||
| 
						 | 
				
			
			@ -92,10 +134,15 @@ func parseToken(req *http.Request) (string, bool) {
 | 
			
		|||
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
 | 
			
		||||
	// Let's see if token is valid.
 | 
			
		||||
	if strings.Contains(tokenSHA, ".") {
 | 
			
		||||
		uid := CheckOAuthAccessToken(ctx, tokenSHA)
 | 
			
		||||
		uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA)
 | 
			
		||||
 | 
			
		||||
		if uid != 0 {
 | 
			
		||||
			store.GetData()["IsApiToken"] = true
 | 
			
		||||
			store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
 | 
			
		||||
			if grantScopes != "" {
 | 
			
		||||
				store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScope(grantScopes)
 | 
			
		||||
			} else {
 | 
			
		||||
				store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return uid
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@
 | 
			
		|||
					<b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br>
 | 
			
		||||
					{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}
 | 
			
		||||
				</p>
 | 
			
		||||
				<p>With scopes: {{.Scope}}.</p>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="ui attached segment">
 | 
			
		||||
				<p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML}}</p>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,13 +12,16 @@ import (
 | 
			
		|||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/routers/web/auth"
 | 
			
		||||
	"code.gitea.io/gitea/tests"
 | 
			
		||||
| 
						 | 
				
			
			@ -799,3 +802,516 @@ func TestOAuthIntrospection(t *testing.T) {
 | 
			
		|||
		assert.Contains(t, resp.Body.String(), "no valid authorization")
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestOAuth_GrantScopesReadUser(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
	appBody := api.CreateOAuth2ApplicationOptions{
 | 
			
		||||
		Name: "oauth-provider-scopes-test",
 | 
			
		||||
		RedirectURIs: []string{
 | 
			
		||||
			"a",
 | 
			
		||||
		},
 | 
			
		||||
		ConfidentialClient: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
 | 
			
		||||
		AddBasicAuth(user.Name)
 | 
			
		||||
	resp := MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	var app *api.OAuth2Application
 | 
			
		||||
	DecodeJSON(t, resp, &app)
 | 
			
		||||
 | 
			
		||||
	grant := &auth_model.OAuth2Grant{
 | 
			
		||||
		ApplicationID: app.ID,
 | 
			
		||||
		UserID:        user.ID,
 | 
			
		||||
		Scope:         "openid profile email read:user",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := db.Insert(db.DefaultContext, grant)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.Contains(t, grant.Scope, "openid profile email read:user")
 | 
			
		||||
 | 
			
		||||
	ctx := loginUserWithPasswordRemember(t, user.Name, "password", true)
 | 
			
		||||
 | 
			
		||||
	authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
 | 
			
		||||
	authorizeReq := NewRequest(t, "GET", authorizeURL)
 | 
			
		||||
	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 | 
			
		||||
 | 
			
		||||
	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 | 
			
		||||
	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
 | 
			
		||||
		"_csrf":        htmlDoc.GetCSRF(),
 | 
			
		||||
		"client_id":    app.ClientID,
 | 
			
		||||
		"redirect_uri": "a",
 | 
			
		||||
		"state":        "thestate",
 | 
			
		||||
		"granted":      "true",
 | 
			
		||||
	})
 | 
			
		||||
	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
 | 
			
		||||
	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
 | 
			
		||||
 | 
			
		||||
	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
 | 
			
		||||
		"_csrf":         htmlDocGrant.GetCSRF(),
 | 
			
		||||
		"grant_type":    "authorization_code",
 | 
			
		||||
		"client_id":     app.ClientID,
 | 
			
		||||
		"client_secret": app.ClientSecret,
 | 
			
		||||
		"redirect_uri":  "a",
 | 
			
		||||
		"code":          authcode,
 | 
			
		||||
	})
 | 
			
		||||
	accessTokenResp := ctx.MakeRequest(t, accessTokenReq, 200)
 | 
			
		||||
	type response struct {
 | 
			
		||||
		AccessToken  string `json:"access_token"`
 | 
			
		||||
		TokenType    string `json:"token_type"`
 | 
			
		||||
		ExpiresIn    int64  `json:"expires_in"`
 | 
			
		||||
		RefreshToken string `json:"refresh_token"`
 | 
			
		||||
	}
 | 
			
		||||
	parsed := new(response)
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
 | 
			
		||||
	userReq := NewRequest(t, "GET", "/api/v1/user")
 | 
			
		||||
	userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
 | 
			
		||||
	userResp := MakeRequest(t, userReq, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	// assert.Contains(t, string(userResp.Body.Bytes()), "blah")
 | 
			
		||||
	type userResponse struct {
 | 
			
		||||
		Login string `json:"login"`
 | 
			
		||||
		Email string `json:"email"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userParsed := new(userResponse)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), userParsed))
 | 
			
		||||
	assert.Contains(t, userParsed.Email, "user2@example.com")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestOAuth_GrantScopesFailReadRepository(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
	appBody := api.CreateOAuth2ApplicationOptions{
 | 
			
		||||
		Name: "oauth-provider-scopes-test",
 | 
			
		||||
		RedirectURIs: []string{
 | 
			
		||||
			"a",
 | 
			
		||||
		},
 | 
			
		||||
		ConfidentialClient: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
 | 
			
		||||
		AddBasicAuth(user.Name)
 | 
			
		||||
	resp := MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	var app *api.OAuth2Application
 | 
			
		||||
	DecodeJSON(t, resp, &app)
 | 
			
		||||
 | 
			
		||||
	grant := &auth_model.OAuth2Grant{
 | 
			
		||||
		ApplicationID: app.ID,
 | 
			
		||||
		UserID:        user.ID,
 | 
			
		||||
		Scope:         "openid profile email read:user",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := db.Insert(db.DefaultContext, grant)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.Contains(t, grant.Scope, "openid profile email read:user")
 | 
			
		||||
 | 
			
		||||
	ctx := loginUserWithPasswordRemember(t, user.Name, "password", true)
 | 
			
		||||
 | 
			
		||||
	authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
 | 
			
		||||
	authorizeReq := NewRequest(t, "GET", authorizeURL)
 | 
			
		||||
	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 | 
			
		||||
 | 
			
		||||
	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 | 
			
		||||
	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
 | 
			
		||||
		"_csrf":        htmlDoc.GetCSRF(),
 | 
			
		||||
		"client_id":    app.ClientID,
 | 
			
		||||
		"redirect_uri": "a",
 | 
			
		||||
		"state":        "thestate",
 | 
			
		||||
		"granted":      "true",
 | 
			
		||||
	})
 | 
			
		||||
	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
 | 
			
		||||
	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
 | 
			
		||||
 | 
			
		||||
	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
 | 
			
		||||
		"_csrf":         htmlDocGrant.GetCSRF(),
 | 
			
		||||
		"grant_type":    "authorization_code",
 | 
			
		||||
		"client_id":     app.ClientID,
 | 
			
		||||
		"client_secret": app.ClientSecret,
 | 
			
		||||
		"redirect_uri":  "a",
 | 
			
		||||
		"code":          authcode,
 | 
			
		||||
	})
 | 
			
		||||
	accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
 | 
			
		||||
	type response struct {
 | 
			
		||||
		AccessToken  string `json:"access_token"`
 | 
			
		||||
		TokenType    string `json:"token_type"`
 | 
			
		||||
		ExpiresIn    int64  `json:"expires_in"`
 | 
			
		||||
		RefreshToken string `json:"refresh_token"`
 | 
			
		||||
	}
 | 
			
		||||
	parsed := new(response)
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
 | 
			
		||||
	userReq := NewRequest(t, "GET", "/api/v1/users/user2/repos")
 | 
			
		||||
	userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
 | 
			
		||||
	userResp := MakeRequest(t, userReq, http.StatusForbidden)
 | 
			
		||||
 | 
			
		||||
	type userResponse struct {
 | 
			
		||||
		Message string `json:"message"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userParsed := new(userResponse)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), userParsed))
 | 
			
		||||
	assert.Contains(t, userParsed.Message, "token does not have at least one of required scope(s): [read:repository]")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestOAuth_GrantScopesReadRepository(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
	appBody := api.CreateOAuth2ApplicationOptions{
 | 
			
		||||
		Name: "oauth-provider-scopes-test",
 | 
			
		||||
		RedirectURIs: []string{
 | 
			
		||||
			"a",
 | 
			
		||||
		},
 | 
			
		||||
		ConfidentialClient: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
 | 
			
		||||
		AddBasicAuth(user.Name)
 | 
			
		||||
	resp := MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	var app *api.OAuth2Application
 | 
			
		||||
	DecodeJSON(t, resp, &app)
 | 
			
		||||
 | 
			
		||||
	grant := &auth_model.OAuth2Grant{
 | 
			
		||||
		ApplicationID: app.ID,
 | 
			
		||||
		UserID:        user.ID,
 | 
			
		||||
		Scope:         "openid profile email read:user read:repository",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := db.Insert(db.DefaultContext, grant)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.Contains(t, grant.Scope, "openid profile email read:user read:repository")
 | 
			
		||||
 | 
			
		||||
	ctx := loginUserWithPasswordRemember(t, user.Name, "password", true)
 | 
			
		||||
 | 
			
		||||
	authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
 | 
			
		||||
	authorizeReq := NewRequest(t, "GET", authorizeURL)
 | 
			
		||||
	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 | 
			
		||||
 | 
			
		||||
	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 | 
			
		||||
	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
 | 
			
		||||
		"_csrf":        htmlDoc.GetCSRF(),
 | 
			
		||||
		"client_id":    app.ClientID,
 | 
			
		||||
		"redirect_uri": "a",
 | 
			
		||||
		"state":        "thestate",
 | 
			
		||||
		"granted":      "true",
 | 
			
		||||
	})
 | 
			
		||||
	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
 | 
			
		||||
	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
 | 
			
		||||
 | 
			
		||||
	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
 | 
			
		||||
		"_csrf":         htmlDocGrant.GetCSRF(),
 | 
			
		||||
		"grant_type":    "authorization_code",
 | 
			
		||||
		"client_id":     app.ClientID,
 | 
			
		||||
		"client_secret": app.ClientSecret,
 | 
			
		||||
		"redirect_uri":  "a",
 | 
			
		||||
		"code":          authcode,
 | 
			
		||||
	})
 | 
			
		||||
	accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
 | 
			
		||||
	type response struct {
 | 
			
		||||
		AccessToken  string `json:"access_token"`
 | 
			
		||||
		TokenType    string `json:"token_type"`
 | 
			
		||||
		ExpiresIn    int64  `json:"expires_in"`
 | 
			
		||||
		RefreshToken string `json:"refresh_token"`
 | 
			
		||||
	}
 | 
			
		||||
	parsed := new(response)
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
 | 
			
		||||
	userReq := NewRequest(t, "GET", "/api/v1/users/user2/repos")
 | 
			
		||||
	userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
 | 
			
		||||
	userResp := MakeRequest(t, userReq, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	type repos struct {
 | 
			
		||||
		FullRepoName string `json:"full_name"`
 | 
			
		||||
	}
 | 
			
		||||
	var userResponse []*repos
 | 
			
		||||
	require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), &userResponse))
 | 
			
		||||
	if assert.NotEmpty(t, userResponse) {
 | 
			
		||||
		assert.Contains(t, userResponse[0].FullRepoName, "user2/repo1")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestOAuth_GrantScopesReadPrivateGroups(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	// setting.OAuth2.EnableAdditionalGrantScopes = true
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user5"})
 | 
			
		||||
 | 
			
		||||
	appBody := api.CreateOAuth2ApplicationOptions{
 | 
			
		||||
		Name: "oauth-provider-scopes-test",
 | 
			
		||||
		RedirectURIs: []string{
 | 
			
		||||
			"a",
 | 
			
		||||
		},
 | 
			
		||||
		ConfidentialClient: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	appReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
 | 
			
		||||
		AddBasicAuth(user.Name)
 | 
			
		||||
	appResp := MakeRequest(t, appReq, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	var app *api.OAuth2Application
 | 
			
		||||
	DecodeJSON(t, appResp, &app)
 | 
			
		||||
 | 
			
		||||
	grant := &auth_model.OAuth2Grant{
 | 
			
		||||
		ApplicationID: app.ID,
 | 
			
		||||
		UserID:        user.ID,
 | 
			
		||||
		Scope:         "openid profile email groups read:user",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := db.Insert(db.DefaultContext, grant)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.Contains(t, grant.Scope, "openid profile email groups read:user")
 | 
			
		||||
 | 
			
		||||
	ctx := loginUserWithPasswordRemember(t, user.Name, "password", true)
 | 
			
		||||
 | 
			
		||||
	authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
 | 
			
		||||
	authorizeReq := NewRequest(t, "GET", authorizeURL)
 | 
			
		||||
	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 | 
			
		||||
 | 
			
		||||
	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 | 
			
		||||
	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
 | 
			
		||||
		"_csrf":        htmlDoc.GetCSRF(),
 | 
			
		||||
		"client_id":    app.ClientID,
 | 
			
		||||
		"redirect_uri": "a",
 | 
			
		||||
		"state":        "thestate",
 | 
			
		||||
		"granted":      "true",
 | 
			
		||||
	})
 | 
			
		||||
	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
 | 
			
		||||
	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
 | 
			
		||||
 | 
			
		||||
	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
 | 
			
		||||
		"_csrf":         htmlDocGrant.GetCSRF(),
 | 
			
		||||
		"grant_type":    "authorization_code",
 | 
			
		||||
		"client_id":     app.ClientID,
 | 
			
		||||
		"client_secret": app.ClientSecret,
 | 
			
		||||
		"redirect_uri":  "a",
 | 
			
		||||
		"code":          authcode,
 | 
			
		||||
	})
 | 
			
		||||
	accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
 | 
			
		||||
	type response struct {
 | 
			
		||||
		AccessToken  string `json:"access_token"`
 | 
			
		||||
		TokenType    string `json:"token_type"`
 | 
			
		||||
		ExpiresIn    int64  `json:"expires_in"`
 | 
			
		||||
		RefreshToken string `json:"refresh_token"`
 | 
			
		||||
		IDToken      string `json:"id_token,omitempty"`
 | 
			
		||||
	}
 | 
			
		||||
	parsed := new(response)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
 | 
			
		||||
	parts := strings.Split(parsed.IDToken, ".")
 | 
			
		||||
 | 
			
		||||
	payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
 | 
			
		||||
	type IDTokenClaims struct {
 | 
			
		||||
		Groups []string `json:"groups"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	claims := new(IDTokenClaims)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(payload, claims))
 | 
			
		||||
	for _, group := range []string{"limited_org36", "limited_org36:team20writepackage", "org6", "org6:owners", "org7", "org7:owners", "privated_org", "privated_org:team14writeauth"} {
 | 
			
		||||
		assert.Contains(t, claims.Groups, group)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestOAuth_GrantScopesReadOnlyPublicGroups(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	setting.OAuth2.EnableAdditionalGrantScopes = true
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user5"})
 | 
			
		||||
 | 
			
		||||
	appBody := api.CreateOAuth2ApplicationOptions{
 | 
			
		||||
		Name: "oauth-provider-scopes-test",
 | 
			
		||||
		RedirectURIs: []string{
 | 
			
		||||
			"a",
 | 
			
		||||
		},
 | 
			
		||||
		ConfidentialClient: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	appReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
 | 
			
		||||
		AddBasicAuth(user.Name)
 | 
			
		||||
	appResp := MakeRequest(t, appReq, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	var app *api.OAuth2Application
 | 
			
		||||
	DecodeJSON(t, appResp, &app)
 | 
			
		||||
 | 
			
		||||
	grant := &auth_model.OAuth2Grant{
 | 
			
		||||
		ApplicationID: app.ID,
 | 
			
		||||
		UserID:        user.ID,
 | 
			
		||||
		Scope:         "openid profile email groups read:user",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := db.Insert(db.DefaultContext, grant)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.Contains(t, grant.Scope, "openid profile email groups read:user")
 | 
			
		||||
 | 
			
		||||
	ctx := loginUserWithPasswordRemember(t, user.Name, "password", true)
 | 
			
		||||
 | 
			
		||||
	authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
 | 
			
		||||
	authorizeReq := NewRequest(t, "GET", authorizeURL)
 | 
			
		||||
	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 | 
			
		||||
 | 
			
		||||
	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 | 
			
		||||
	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
 | 
			
		||||
		"_csrf":        htmlDoc.GetCSRF(),
 | 
			
		||||
		"client_id":    app.ClientID,
 | 
			
		||||
		"redirect_uri": "a",
 | 
			
		||||
		"state":        "thestate",
 | 
			
		||||
		"granted":      "true",
 | 
			
		||||
	})
 | 
			
		||||
	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
 | 
			
		||||
	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
 | 
			
		||||
 | 
			
		||||
	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
 | 
			
		||||
		"_csrf":         htmlDocGrant.GetCSRF(),
 | 
			
		||||
		"grant_type":    "authorization_code",
 | 
			
		||||
		"client_id":     app.ClientID,
 | 
			
		||||
		"client_secret": app.ClientSecret,
 | 
			
		||||
		"redirect_uri":  "a",
 | 
			
		||||
		"code":          authcode,
 | 
			
		||||
	})
 | 
			
		||||
	accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
 | 
			
		||||
	type response struct {
 | 
			
		||||
		AccessToken  string `json:"access_token"`
 | 
			
		||||
		TokenType    string `json:"token_type"`
 | 
			
		||||
		ExpiresIn    int64  `json:"expires_in"`
 | 
			
		||||
		RefreshToken string `json:"refresh_token"`
 | 
			
		||||
		IDToken      string `json:"id_token,omitempty"`
 | 
			
		||||
	}
 | 
			
		||||
	parsed := new(response)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
 | 
			
		||||
	parts := strings.Split(parsed.IDToken, ".")
 | 
			
		||||
 | 
			
		||||
	payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
 | 
			
		||||
	type IDTokenClaims struct {
 | 
			
		||||
		Groups []string `json:"groups"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	claims := new(IDTokenClaims)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(payload, claims))
 | 
			
		||||
	for _, privOrg := range []string{"org7", "org7:owners", "privated_org", "privated_org:team14writeauth"} {
 | 
			
		||||
		assert.NotContains(t, claims.Groups, privOrg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userReq := NewRequest(t, "GET", "/login/oauth/userinfo")
 | 
			
		||||
	userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
 | 
			
		||||
	userResp := MakeRequest(t, userReq, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	type userinfo struct {
 | 
			
		||||
		Groups []string `json:"groups"`
 | 
			
		||||
	}
 | 
			
		||||
	parsedUserInfo := new(userinfo)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), parsedUserInfo))
 | 
			
		||||
 | 
			
		||||
	for _, privOrg := range []string{"org7", "org7:owners", "privated_org", "privated_org:team14writeauth"} {
 | 
			
		||||
		assert.NotContains(t, parsedUserInfo.Groups, privOrg)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestOAuth_GrantScopesReadPublicGroupsWithTheReadScope(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	setting.OAuth2.EnableAdditionalGrantScopes = true
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user5"})
 | 
			
		||||
 | 
			
		||||
	appBody := api.CreateOAuth2ApplicationOptions{
 | 
			
		||||
		Name: "oauth-provider-scopes-test",
 | 
			
		||||
		RedirectURIs: []string{
 | 
			
		||||
			"a",
 | 
			
		||||
		},
 | 
			
		||||
		ConfidentialClient: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	appReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
 | 
			
		||||
		AddBasicAuth(user.Name)
 | 
			
		||||
	appResp := MakeRequest(t, appReq, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	var app *api.OAuth2Application
 | 
			
		||||
	DecodeJSON(t, appResp, &app)
 | 
			
		||||
 | 
			
		||||
	grant := &auth_model.OAuth2Grant{
 | 
			
		||||
		ApplicationID: app.ID,
 | 
			
		||||
		UserID:        user.ID,
 | 
			
		||||
		Scope:         "openid profile email groups read:user read:organization",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := db.Insert(db.DefaultContext, grant)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.Contains(t, grant.Scope, "openid profile email groups read:user read:organization")
 | 
			
		||||
 | 
			
		||||
	ctx := loginUserWithPasswordRemember(t, user.Name, "password", true)
 | 
			
		||||
 | 
			
		||||
	authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
 | 
			
		||||
	authorizeReq := NewRequest(t, "GET", authorizeURL)
 | 
			
		||||
	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 | 
			
		||||
 | 
			
		||||
	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 | 
			
		||||
	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
 | 
			
		||||
		"_csrf":        htmlDoc.GetCSRF(),
 | 
			
		||||
		"client_id":    app.ClientID,
 | 
			
		||||
		"redirect_uri": "a",
 | 
			
		||||
		"state":        "thestate",
 | 
			
		||||
		"granted":      "true",
 | 
			
		||||
	})
 | 
			
		||||
	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
 | 
			
		||||
	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
 | 
			
		||||
 | 
			
		||||
	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
 | 
			
		||||
		"_csrf":         htmlDocGrant.GetCSRF(),
 | 
			
		||||
		"grant_type":    "authorization_code",
 | 
			
		||||
		"client_id":     app.ClientID,
 | 
			
		||||
		"client_secret": app.ClientSecret,
 | 
			
		||||
		"redirect_uri":  "a",
 | 
			
		||||
		"code":          authcode,
 | 
			
		||||
	})
 | 
			
		||||
	accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
 | 
			
		||||
	type response struct {
 | 
			
		||||
		AccessToken  string `json:"access_token"`
 | 
			
		||||
		TokenType    string `json:"token_type"`
 | 
			
		||||
		ExpiresIn    int64  `json:"expires_in"`
 | 
			
		||||
		RefreshToken string `json:"refresh_token"`
 | 
			
		||||
		IDToken      string `json:"id_token,omitempty"`
 | 
			
		||||
	}
 | 
			
		||||
	parsed := new(response)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
 | 
			
		||||
	parts := strings.Split(parsed.IDToken, ".")
 | 
			
		||||
 | 
			
		||||
	payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
 | 
			
		||||
	type IDTokenClaims struct {
 | 
			
		||||
		Groups []string `json:"groups"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	claims := new(IDTokenClaims)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(payload, claims))
 | 
			
		||||
	for _, privOrg := range []string{"org7", "org7:owners", "privated_org", "privated_org:team14writeauth"} {
 | 
			
		||||
		assert.Contains(t, claims.Groups, privOrg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userReq := NewRequest(t, "GET", "/login/oauth/userinfo")
 | 
			
		||||
	userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
 | 
			
		||||
	userResp := MakeRequest(t, userReq, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	type userinfo struct {
 | 
			
		||||
		Groups []string `json:"groups"`
 | 
			
		||||
	}
 | 
			
		||||
	parsedUserInfo := new(userinfo)
 | 
			
		||||
	require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), parsedUserInfo))
 | 
			
		||||
	for _, privOrg := range []string{"org7", "org7:owners", "privated_org", "privated_org:team14writeauth"} {
 | 
			
		||||
		assert.Contains(t, parsedUserInfo.Groups, privOrg)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue