oauth2 additional scopes

pull/32180/head
Marcell Mars 1 month ago
parent 9116665e9c
commit 4885397d18
  1. 2
      modules/setting/oauth2.go
  2. 1
      options/locale/locale_en-US.ini
  3. 12
      routers/web/auth/oauth2_provider.go
  4. 1
      routers/web/user/setting/applications.go
  5. 2
      services/auth/basic.go
  6. 24
      services/auth/oauth2.go
  7. 45
      services/oauth2_provider/access_token.go
  8. 36
      services/oauth2_provider/additional_scopes_test.go
  9. 3
      templates/user/auth/grant.tmpl
  10. 28
      tests/integration/integration_test.go
  11. 389
      tests/integration/oauth_test.go

@ -98,6 +98,7 @@ var OAuth2 = struct {
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
MaxTokenLength int MaxTokenLength int
DefaultApplications []string DefaultApplications []string
EnableAdditionalGrantScopes bool
}{ }{
Enabled: true, Enabled: true,
AccessTokenExpirationTime: 3600, AccessTokenExpirationTime: 3600,
@ -107,6 +108,7 @@ var OAuth2 = struct {
JWTSigningPrivateKeyFile: "jwt/private.pem", JWTSigningPrivateKeyFile: "jwt/private.pem",
MaxTokenLength: math.MaxInt16, MaxTokenLength: math.MaxInt16,
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"}, DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"},
EnableAdditionalGrantScopes: false,
} }
func loadOAuth2From(rootCfg ConfigProvider) { func loadOAuth2From(rootCfg ConfigProvider) {

@ -453,6 +453,7 @@ authorize_application = Authorize Application
authorize_redirect_notice = You will be redirected to %s if you authorize this application. authorize_redirect_notice = You will be redirected to %s if you authorize this application.
authorize_application_created_by = This application was created by %s. authorize_application_created_by = This application was created by %s.
authorize_application_description = If you grant the access, it will be able to access and write to all your account information, including private repos and organisations. authorize_application_description = If you grant the access, it will be able to access and write to all your account information, including private repos and organisations.
authorize_application_with_scopes = With scopes: %s
authorize_title = Authorize "%s" to access your account? authorize_title = Authorize "%s" to access your account?
authorization_failed = Authorization failed authorization_failed = Authorization failed
authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you have tried to authorize. authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you have tried to authorize.

@ -85,7 +85,7 @@ type userInfoResponse struct {
Username string `json:"preferred_username"` Username string `json:"preferred_username"`
Email string `json:"email"` Email string `json:"email"`
Picture string `json:"picture"` Picture string `json:"picture"`
Groups []string `json:"groups"` Groups []string `json:"groups,omitempty"`
} }
// InfoOAuth manages request for userinfo endpoint // InfoOAuth manages request for userinfo endpoint
@ -104,7 +104,8 @@ func InfoOAuth(ctx *context.Context) {
Picture: ctx.Doer.AvatarLink(ctx), Picture: ctx.Doer.AvatarLink(ctx),
} }
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer) onlyPublicGroups := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)
if err != nil { if err != nil {
ctx.ServerError("Oauth groups for user", err) ctx.ServerError("Oauth groups for user", err)
return return
@ -304,6 +305,13 @@ func AuthorizeOAuth(ctx *context.Context) {
return return
} }
// check if additional scopes
if oauth2_provider.GrantAdditionalScopes(form.Scope) == auth.AccessTokenScopeAll {
ctx.Data["AdditionalScopes"] = false
} else {
ctx.Data["AdditionalScopes"] = true
}
// show authorize page to grant access // show authorize page to grant access
ctx.Data["Application"] = app ctx.Data["Application"] = app
ctx.Data["RedirectURI"] = form.RedirectURI ctx.Data["RedirectURI"] = form.RedirectURI

@ -113,5 +113,6 @@ func loadApplicationsData(ctx *context.Context) {
ctx.ServerError("GetOAuth2GrantsByUserID", err) ctx.ServerError("GetOAuth2GrantsByUserID", err)
return return
} }
ctx.Data["EnableAdditionalGrantScopes"] = setting.OAuth2.EnableAdditionalGrantScopes
} }
} }

@ -77,7 +77,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
} }
// check oauth2 token // check oauth2 token
uid := CheckOAuthAccessToken(req.Context(), authToken) uid, _ := CheckOAuthAccessToken(req.Context(), authToken)
if uid != 0 { if uid != 0 {
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid) log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)

@ -17,7 +17,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/oauth2_provider" oauth2_provider "code.gitea.io/gitea/services/oauth2_provider"
) )
// Ensure the struct implements the interface. // Ensure the struct implements the interface.
@ -26,27 +26,30 @@ var (
) )
// CheckOAuthAccessToken returns uid of user from oauth token // 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, auth_model.AccessTokenScope) {
var accessTokenScope auth_model.AccessTokenScope
// JWT tokens require a "." // JWT tokens require a "."
if !strings.Contains(accessToken, ".") { if !strings.Contains(accessToken, ".") {
return 0 return 0, accessTokenScope
} }
token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey) token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey)
if err != nil { if err != nil {
log.Trace("oauth2.ParseToken: %v", err) log.Trace("oauth2.ParseToken: %v", err)
return 0 return 0, accessTokenScope
} }
var grant *auth_model.OAuth2Grant var grant *auth_model.OAuth2Grant
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil { if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
return 0 return 0, accessTokenScope
} }
if token.Kind != oauth2_provider.KindAccessToken { if token.Kind != oauth2_provider.KindAccessToken {
return 0 return 0, accessTokenScope
} }
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) { if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
return 0 return 0, accessTokenScope
} }
return grant.UserID accessTokenScope = oauth2_provider.GrantAdditionalScopes(grant.Scope)
return grant.UserID, accessTokenScope
} }
// OAuth2 implements the Auth interface and authenticates requests // OAuth2 implements the Auth interface and authenticates requests
@ -92,10 +95,11 @@ func parseToken(req *http.Request) (string, bool) {
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 { func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
// Let's see if token is valid. // Let's see if token is valid.
if strings.Contains(tokenSHA, ".") { if strings.Contains(tokenSHA, ".") {
uid := CheckOAuthAccessToken(ctx, tokenSHA) uid, accessTokenScope := CheckOAuthAccessToken(ctx, tokenSHA)
if uid != 0 { if uid != 0 {
store.GetData()["IsApiToken"] = true store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all store.GetData()["ApiTokenScope"] = accessTokenScope
} }
return uid return uid
} }

@ -6,6 +6,8 @@ package oauth2_provider //nolint
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings"
auth "code.gitea.io/gitea/models/auth" auth "code.gitea.io/gitea/models/auth"
org_model "code.gitea.io/gitea/models/organization" org_model "code.gitea.io/gitea/models/organization"
@ -68,6 +70,30 @@ type AccessTokenResponse struct {
IDToken string `json:"id_token,omitempty"` IDToken string `json:"id_token,omitempty"`
} }
// GrantAdditionalScopes returns valid scopes coming from grant
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
scopesSupported := []string{
"openid",
"profile",
"email",
"groups",
}
var tokenScopes []string
for _, tokenScope := range strings.Split(grantScopes, " ") {
if slices.Index(scopesSupported, tokenScope) == -1 {
tokenScopes = append(tokenScopes, tokenScope)
}
}
accessTokenScope := auth.AccessTokenScope(strings.Join(tokenScopes, ","))
if accessTokenWithAdditionalScopes, err := accessTokenScope.Normalize(); err == nil && len(tokenScopes) > 0 {
return accessTokenWithAdditionalScopes
}
return auth.AccessTokenScopeAll
}
func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) { func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens { if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(ctx); err != nil { if err := grant.IncreaseCounter(ctx); err != nil {
@ -160,7 +186,10 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
idToken.EmailVerified = user.IsActive idToken.EmailVerified = user.IsActive
} }
if grant.ScopeContains("groups") { if grant.ScopeContains("groups") {
groups, err := GetOAuthGroupsForUser(ctx, user) accessTokenScope := GrantAdditionalScopes(grant.Scope)
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
groups, err := GetOAuthGroupsForUser(ctx, user, onlyPublicGroups)
if err != nil { if err != nil {
log.Error("Error getting groups: %v", err) log.Error("Error getting groups: %v", err)
return nil, &AccessTokenError{ return nil, &AccessTokenError{
@ -191,7 +220,7 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
// returns a list of "org" and "org:team" strings, // returns a list of "org" and "org:team" strings,
// that the given user is a part of. // that the given user is a part of.
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) { func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
orgs, err := org_model.GetUserOrgsList(ctx, user) orgs, err := org_model.GetUserOrgsList(ctx, user)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetUserOrgList: %w", err) return nil, fmt.Errorf("GetUserOrgList: %w", err)
@ -199,6 +228,18 @@ func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string
var groups []string var groups []string
for _, org := range orgs { for _, org := range orgs {
// process additional scopes only if enabled in settings
// this could be removed once additional scopes get accepted
if setting.OAuth2.EnableAdditionalGrantScopes {
if onlyPublicGroups {
if public, err := org_model.IsPublicMembership(ctx, org.ID, user.ID); err == nil {
if !public || !org.Visibility.IsPublic() {
continue
}
}
}
}
groups = append(groups, org.Name) groups = append(groups, org.Name)
teams, err := org.LoadTeams(ctx) teams, err := org.LoadTeams(ctx)
if err != nil { if err != nil {

@ -0,0 +1,36 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2_provider //nolint
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGrantAdditionalScopes(t *testing.T) {
setting.OAuth2.EnableAdditionalGrantScopes = true
tests := []struct {
grantScopes string
expectedScopes string
}{
{"openid profile email", "all"},
{"openid profile email groups", "all"},
{"openid profile email all", "all"},
{"openid profile email read:user all", "all"},
{"openid profile email groups read:user", "read:user"},
{"read:user read:repository", "read:repository,read:user"},
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
{"openid profile email read:user", "read:user"},
{"read:invalid_scope", "all"},
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
}
for _, test := range tests {
t.Run(test.grantScopes, func(t *testing.T) {
result := GrantAdditionalScopes(test.grantScopes)
assert.Equal(t, test.expectedScopes, string(result))
})
}
}

@ -8,8 +8,11 @@
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<p> <p>
{{if not .AdditionalScopes}}
<b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br> <b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br>
{{end}}
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}} {{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}
<p>{{ctx.Locale.Tr "auth.authorize_application_with_scopes" .Scope}}</p>
</p> </p>
</div> </div>
<div class="ui attached segment"> <div class="ui attached segment">

@ -17,6 +17,7 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
@ -502,3 +503,30 @@ func GetAnonymousCSRFToken(t testing.TB, session *TestSession) string {
require.NotEmpty(t, csrfToken) require.NotEmpty(t, csrfToken)
return csrfToken return csrfToken
} }
func loginUserWithPasswordRemember(t testing.TB, userName, password string, rememberMe bool) *TestSession {
t.Helper()
req := NewRequest(t, "GET", "/user/login")
resp := MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
"_csrf": doc.GetCSRF(),
"user_name": userName,
"password": password,
"remember": strconv.FormatBool(rememberMe),
})
resp = MakeRequest(t, req, http.StatusSeeOther)
ch := http.Header{}
ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
cr := http.Request{Header: ch}
session := emptyTestSession(t)
baseURL, err := url.Parse(setting.AppURL)
require.NoError(t, err)
session.jar.SetCookies(baseURL, cr.Cookies())
return session
}

@ -5,16 +5,25 @@ package integration
import ( import (
"bytes" "bytes"
"encoding/base64"
"fmt"
"io" "io"
"net/http" "net/http"
"strings"
"testing" "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/json"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
oauth2_provider "code.gitea.io/gitea/services/oauth2_provider" oauth2_provider "code.gitea.io/gitea/services/oauth2_provider"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAuthorizeNoClientID(t *testing.T) { func TestAuthorizeNoClientID(t *testing.T) {
@ -477,3 +486,383 @@ func TestOAuthIntrospection(t *testing.T) {
resp = MakeRequest(t, req, http.StatusUnauthorized) resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), "no valid authorization") assert.Contains(t, resp.Body.String(), "no valid authorization")
} }
func TestOAuth_GrantScopesReadUserFailRepos(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], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"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)
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")
errorReq := NewRequest(t, "GET", "/api/v1/users/user2/repos")
errorReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
errorResp := MakeRequest(t, errorReq, http.StatusForbidden)
type errorResponse struct {
Message string `json:"message"`
}
errorParsed := new(errorResponse)
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:repository]")
}
func TestOAuth_GrantScopesReadRepositoryFailOrganization(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], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"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 repo struct {
FullRepoName string `json:"full_name"`
Private bool `json:"private"`
}
var reposCaptured []repo
require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), &reposCaptured))
reposExpected := []repo{
{
FullRepoName: "user2/repo1",
Private: false,
},
{
FullRepoName: "user2/repo2",
Private: true,
},
{
FullRepoName: "user2/repo15",
Private: true,
},
{
FullRepoName: "user2/repo16",
Private: true,
},
{
FullRepoName: "user2/repo20",
Private: true,
},
{
FullRepoName: "user2/utf8",
Private: false,
},
{
FullRepoName: "user2/commits_search_test",
Private: false,
},
{
FullRepoName: "user2/git_hooks_test",
Private: false,
},
{
FullRepoName: "user2/glob",
Private: false,
},
{
FullRepoName: "user2/lfs",
Private: true,
},
{
FullRepoName: "user2/scoped_label",
Private: true,
},
{
FullRepoName: "user2/readme-test",
Private: true,
},
{
FullRepoName: "user2/repo-release",
Private: false,
},
{
FullRepoName: "user2/commitsonpr",
Private: false,
},
{
FullRepoName: "user2/test_commit_revert",
Private: true,
},
}
assert.Equal(t, reposExpected, reposCaptured)
errorReq := NewRequest(t, "GET", "/api/v1/users/user2/orgs")
errorReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
errorResp := MakeRequest(t, errorReq, http.StatusForbidden)
type errorResponse struct {
Message string `json:"message"`
}
errorParsed := new(errorResponse)
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:user read:organization]")
}
func TestOAuth_GrantScopesClaimGroupsAll(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
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:organization",
}
err := db.Insert(db.DefaultContext, grant)
require.NoError(t, err)
assert.ElementsMatch(t, []string{"openid", "profile", "email", "groups", "read:organization"}, strings.Split(grant.Scope, " "))
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], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"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{
"org17",
"org17:test_team",
"org3",
"org3:owners",
"org3:team1",
"org3:teamcreaterepo",
"private_org35",
"private_org35:team24",
} {
assert.Contains(t, claims.Groups, group)
}
}
func TestOAuth_GrantScopesEnabledClaimGroups(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
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",
}
err := db.Insert(db.DefaultContext, grant)
require.NoError(t, err)
assert.Contains(t, grant.Scope, "openid profile email groups")
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], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"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{
"org17",
"org17:test_team",
"org3",
"org3:owners",
"org3:team1",
"org3:teamcreaterepo",
} {
assert.Contains(t, claims.Groups, group)
}
}

Loading…
Cancel
Save