2024-10-01 18:03:19 -06:00
|
|
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
package oauth2_provider //nolint
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access (#32573)
Resolve #31609
This PR was initiated following my personal research to find the
lightest possible Single Sign-On solution for self-hosted setups. The
existing solutions often seemed too enterprise-oriented, involving many
moving parts and services, demanding significant resources while
promising planetary-scale capabilities. Others were adequate in
supporting basic OAuth2 flows but lacked proper user management
features, such as a change password UI.
Gitea hits the sweet spot for me, provided it supports more granular
access permissions for resources under users who accept the OAuth2
application.
This PR aims to introduce granularity in handling user resources as
nonintrusively and simply as possible. It allows third parties to inform
users about their intent to not ask for the full access and instead
request a specific, reduced scope. If the provided scopes are **only**
the typical ones for OIDC/OAuth2—`openid`, `profile`, `email`, and
`groups`—everything remains unchanged (currently full access to user's
resources). Additionally, this PR supports processing scopes already
introduced with [personal
tokens](https://docs.gitea.com/development/oauth2-provider#scopes) (e.g.
`read:user`, `write:issue`, `read:group`, `write:repository`...)
Personal tokens define scopes around specific resources: user info,
repositories, issues, packages, organizations, notifications,
miscellaneous, admin, and activitypub, with access delineated by read
and/or write permissions.
The initial case I wanted to address was to have Gitea act as an OAuth2
Identity Provider. To achieve that, with this PR, I would only add
`openid public-only` to provide access token to the third party to
authenticate the Gitea's user but no further access to the API and users
resources.
Another example: if a third party wanted to interact solely with Issues,
it would need to add `read:user` (for authorization) and
`read:issue`/`write:issue` to manage Issues.
My approach is based on my understanding of how scopes can be utilized,
supported by examples like [Sample Use Cases: Scopes and
Claims](https://auth0.com/docs/get-started/apis/scopes/sample-use-cases-scopes-and-claims)
on auth0.com.
I renamed `CheckOAuthAccessToken` to `GetOAuthAccessTokenScopeAndUserID`
so now it returns AccessTokenScope and user's ID. In the case of
additional scopes in `userIDFromToken` the default `all` would be
reduced to whatever was asked via those scopes. The main difference is
the opportunity to reduce the permissions from `all`, as is currently
the case, to what is provided by the additional scopes described above.
Screenshots:
![Screenshot_20241121_121405](https://github.com/user-attachments/assets/29deaed7-4333-4b02-8898-b822e6f2463e)
![Screenshot_20241121_120211](https://github.com/user-attachments/assets/7a4a4ef7-409c-4116-9d5f-2fe00eb37167)
![Screenshot_20241121_120119](https://github.com/user-attachments/assets/aa52c1a2-212d-4e64-bcdf-7122cee49eb6)
![Screenshot_20241121_120018](https://github.com/user-attachments/assets/9eac318c-e381-4ea9-9e2c-3a3f60319e47)
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-11-21 21:06:41 -07:00
|
|
|
"slices"
|
|
|
|
"strings"
|
2024-10-01 18:03:19 -06:00
|
|
|
|
|
|
|
auth "code.gitea.io/gitea/models/auth"
|
2024-11-13 22:31:47 -07:00
|
|
|
"code.gitea.io/gitea/models/db"
|
2024-10-01 18:03:19 -06:00
|
|
|
org_model "code.gitea.io/gitea/models/organization"
|
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
|
|
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
)
|
|
|
|
|
|
|
|
// AccessTokenErrorCode represents an error code specified in RFC 6749
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
|
|
|
type AccessTokenErrorCode string
|
|
|
|
|
|
|
|
const (
|
|
|
|
// AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
|
|
|
|
AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
|
|
|
|
// AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
|
|
|
|
AccessTokenErrorCodeInvalidClient = "invalid_client"
|
|
|
|
// AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
|
|
|
|
AccessTokenErrorCodeInvalidGrant = "invalid_grant"
|
|
|
|
// AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
|
|
|
|
AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
|
|
|
|
// AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
|
|
|
|
AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
|
|
|
|
// AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
|
|
|
|
AccessTokenErrorCodeInvalidScope = "invalid_scope"
|
|
|
|
)
|
|
|
|
|
|
|
|
// AccessTokenError represents an error response specified in RFC 6749
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
|
|
|
type AccessTokenError struct {
|
|
|
|
ErrorCode AccessTokenErrorCode `json:"error" form:"error"`
|
|
|
|
ErrorDescription string `json:"error_description"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error returns the error message
|
|
|
|
func (err AccessTokenError) Error() string {
|
|
|
|
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TokenType specifies the kind of token
|
|
|
|
type TokenType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
// TokenTypeBearer represents a token type specified in RFC 6749
|
|
|
|
TokenTypeBearer TokenType = "bearer"
|
|
|
|
// TokenTypeMAC represents a token type specified in RFC 6749
|
|
|
|
TokenTypeMAC = "mac"
|
|
|
|
)
|
|
|
|
|
|
|
|
// AccessTokenResponse represents a successful access token response
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2
|
|
|
|
type AccessTokenResponse struct {
|
|
|
|
AccessToken string `json:"access_token"`
|
|
|
|
TokenType TokenType `json:"token_type"`
|
|
|
|
ExpiresIn int64 `json:"expires_in"`
|
|
|
|
RefreshToken string `json:"refresh_token"`
|
|
|
|
IDToken string `json:"id_token,omitempty"`
|
|
|
|
}
|
|
|
|
|
Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access (#32573)
Resolve #31609
This PR was initiated following my personal research to find the
lightest possible Single Sign-On solution for self-hosted setups. The
existing solutions often seemed too enterprise-oriented, involving many
moving parts and services, demanding significant resources while
promising planetary-scale capabilities. Others were adequate in
supporting basic OAuth2 flows but lacked proper user management
features, such as a change password UI.
Gitea hits the sweet spot for me, provided it supports more granular
access permissions for resources under users who accept the OAuth2
application.
This PR aims to introduce granularity in handling user resources as
nonintrusively and simply as possible. It allows third parties to inform
users about their intent to not ask for the full access and instead
request a specific, reduced scope. If the provided scopes are **only**
the typical ones for OIDC/OAuth2—`openid`, `profile`, `email`, and
`groups`—everything remains unchanged (currently full access to user's
resources). Additionally, this PR supports processing scopes already
introduced with [personal
tokens](https://docs.gitea.com/development/oauth2-provider#scopes) (e.g.
`read:user`, `write:issue`, `read:group`, `write:repository`...)
Personal tokens define scopes around specific resources: user info,
repositories, issues, packages, organizations, notifications,
miscellaneous, admin, and activitypub, with access delineated by read
and/or write permissions.
The initial case I wanted to address was to have Gitea act as an OAuth2
Identity Provider. To achieve that, with this PR, I would only add
`openid public-only` to provide access token to the third party to
authenticate the Gitea's user but no further access to the API and users
resources.
Another example: if a third party wanted to interact solely with Issues,
it would need to add `read:user` (for authorization) and
`read:issue`/`write:issue` to manage Issues.
My approach is based on my understanding of how scopes can be utilized,
supported by examples like [Sample Use Cases: Scopes and
Claims](https://auth0.com/docs/get-started/apis/scopes/sample-use-cases-scopes-and-claims)
on auth0.com.
I renamed `CheckOAuthAccessToken` to `GetOAuthAccessTokenScopeAndUserID`
so now it returns AccessTokenScope and user's ID. In the case of
additional scopes in `userIDFromToken` the default `all` would be
reduced to whatever was asked via those scopes. The main difference is
the opportunity to reduce the permissions from `all`, as is currently
the case, to what is provided by the additional scopes described above.
Screenshots:
![Screenshot_20241121_121405](https://github.com/user-attachments/assets/29deaed7-4333-4b02-8898-b822e6f2463e)
![Screenshot_20241121_120211](https://github.com/user-attachments/assets/7a4a4ef7-409c-4116-9d5f-2fe00eb37167)
![Screenshot_20241121_120119](https://github.com/user-attachments/assets/aa52c1a2-212d-4e64-bcdf-7122cee49eb6)
![Screenshot_20241121_120018](https://github.com/user-attachments/assets/9eac318c-e381-4ea9-9e2c-3a3f60319e47)
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-11-21 21:06:41 -07:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// since version 1.22, access tokens grant full access to the API
|
|
|
|
// with this access is reduced only if additional scopes are provided
|
|
|
|
accessTokenScope := auth.AccessTokenScope(strings.Join(tokenScopes, ","))
|
|
|
|
if accessTokenWithAdditionalScopes, err := accessTokenScope.Normalize(); err == nil && len(tokenScopes) > 0 {
|
|
|
|
return accessTokenWithAdditionalScopes
|
|
|
|
}
|
|
|
|
return auth.AccessTokenScopeAll
|
|
|
|
}
|
|
|
|
|
2024-10-01 18:03:19 -06:00
|
|
|
func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
|
|
|
|
if setting.OAuth2.InvalidateRefreshTokens {
|
|
|
|
if err := grant.IncreaseCounter(ctx); err != nil {
|
|
|
|
return nil, &AccessTokenError{
|
|
|
|
ErrorCode: AccessTokenErrorCodeInvalidGrant,
|
|
|
|
ErrorDescription: "cannot increase the grant counter",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// generate access token to access the API
|
|
|
|
expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
|
|
|
|
accessToken := &Token{
|
|
|
|
GrantID: grant.ID,
|
|
|
|
Kind: KindAccessToken,
|
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
signedAccessToken, err := accessToken.SignToken(serverKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &AccessTokenError{
|
|
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
|
|
ErrorDescription: "cannot sign token",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate refresh token to request an access token after it expired later
|
|
|
|
refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime()
|
|
|
|
refreshToken := &Token{
|
|
|
|
GrantID: grant.ID,
|
|
|
|
Counter: grant.Counter,
|
|
|
|
Kind: KindRefreshToken,
|
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
ExpiresAt: jwt.NewNumericDate(refreshExpirationDate),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
signedRefreshToken, err := refreshToken.SignToken(serverKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &AccessTokenError{
|
|
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
|
|
ErrorDescription: "cannot sign token",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate OpenID Connect id_token
|
|
|
|
signedIDToken := ""
|
|
|
|
if grant.ScopeContains("openid") {
|
|
|
|
app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &AccessTokenError{
|
|
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
|
|
ErrorDescription: "cannot find application",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
user, err := user_model.GetUserByID(ctx, grant.UserID)
|
|
|
|
if err != nil {
|
|
|
|
if user_model.IsErrUserNotExist(err) {
|
|
|
|
return nil, &AccessTokenError{
|
|
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
|
|
ErrorDescription: "cannot find user",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log.Error("Error loading user: %v", err)
|
|
|
|
return nil, &AccessTokenError{
|
|
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
|
|
ErrorDescription: "server error",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
idToken := &OIDCToken{
|
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
|
|
|
|
Issuer: setting.AppURL,
|
|
|
|
Audience: []string{app.ClientID},
|
|
|
|
Subject: fmt.Sprint(grant.UserID),
|
|
|
|
},
|
|
|
|
Nonce: grant.Nonce,
|
|
|
|
}
|
|
|
|
if grant.ScopeContains("profile") {
|
2024-11-18 04:24:17 -07:00
|
|
|
idToken.Name = user.DisplayName()
|
2024-10-01 18:03:19 -06:00
|
|
|
idToken.PreferredUsername = user.Name
|
|
|
|
idToken.Profile = user.HTMLURL()
|
|
|
|
idToken.Picture = user.AvatarLink(ctx)
|
|
|
|
idToken.Website = user.Website
|
|
|
|
idToken.Locale = user.Language
|
|
|
|
idToken.UpdatedAt = user.UpdatedUnix
|
|
|
|
}
|
|
|
|
if grant.ScopeContains("email") {
|
|
|
|
idToken.Email = user.Email
|
|
|
|
idToken.EmailVerified = user.IsActive
|
|
|
|
}
|
|
|
|
if grant.ScopeContains("groups") {
|
Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access (#32573)
Resolve #31609
This PR was initiated following my personal research to find the
lightest possible Single Sign-On solution for self-hosted setups. The
existing solutions often seemed too enterprise-oriented, involving many
moving parts and services, demanding significant resources while
promising planetary-scale capabilities. Others were adequate in
supporting basic OAuth2 flows but lacked proper user management
features, such as a change password UI.
Gitea hits the sweet spot for me, provided it supports more granular
access permissions for resources under users who accept the OAuth2
application.
This PR aims to introduce granularity in handling user resources as
nonintrusively and simply as possible. It allows third parties to inform
users about their intent to not ask for the full access and instead
request a specific, reduced scope. If the provided scopes are **only**
the typical ones for OIDC/OAuth2—`openid`, `profile`, `email`, and
`groups`—everything remains unchanged (currently full access to user's
resources). Additionally, this PR supports processing scopes already
introduced with [personal
tokens](https://docs.gitea.com/development/oauth2-provider#scopes) (e.g.
`read:user`, `write:issue`, `read:group`, `write:repository`...)
Personal tokens define scopes around specific resources: user info,
repositories, issues, packages, organizations, notifications,
miscellaneous, admin, and activitypub, with access delineated by read
and/or write permissions.
The initial case I wanted to address was to have Gitea act as an OAuth2
Identity Provider. To achieve that, with this PR, I would only add
`openid public-only` to provide access token to the third party to
authenticate the Gitea's user but no further access to the API and users
resources.
Another example: if a third party wanted to interact solely with Issues,
it would need to add `read:user` (for authorization) and
`read:issue`/`write:issue` to manage Issues.
My approach is based on my understanding of how scopes can be utilized,
supported by examples like [Sample Use Cases: Scopes and
Claims](https://auth0.com/docs/get-started/apis/scopes/sample-use-cases-scopes-and-claims)
on auth0.com.
I renamed `CheckOAuthAccessToken` to `GetOAuthAccessTokenScopeAndUserID`
so now it returns AccessTokenScope and user's ID. In the case of
additional scopes in `userIDFromToken` the default `all` would be
reduced to whatever was asked via those scopes. The main difference is
the opportunity to reduce the permissions from `all`, as is currently
the case, to what is provided by the additional scopes described above.
Screenshots:
![Screenshot_20241121_121405](https://github.com/user-attachments/assets/29deaed7-4333-4b02-8898-b822e6f2463e)
![Screenshot_20241121_120211](https://github.com/user-attachments/assets/7a4a4ef7-409c-4116-9d5f-2fe00eb37167)
![Screenshot_20241121_120119](https://github.com/user-attachments/assets/aa52c1a2-212d-4e64-bcdf-7122cee49eb6)
![Screenshot_20241121_120018](https://github.com/user-attachments/assets/9eac318c-e381-4ea9-9e2c-3a3f60319e47)
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-11-21 21:06:41 -07:00
|
|
|
accessTokenScope := GrantAdditionalScopes(grant.Scope)
|
|
|
|
|
|
|
|
// since version 1.22 does not verify if groups should be public-only,
|
|
|
|
// onlyPublicGroups will be set only if 'public-only' is included in a valid scope
|
|
|
|
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
|
|
|
|
|
|
|
|
groups, err := GetOAuthGroupsForUser(ctx, user, onlyPublicGroups)
|
2024-10-01 18:03:19 -06:00
|
|
|
if err != nil {
|
|
|
|
log.Error("Error getting groups: %v", err)
|
|
|
|
return nil, &AccessTokenError{
|
|
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
|
|
ErrorDescription: "server error",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
idToken.Groups = groups
|
|
|
|
}
|
|
|
|
|
|
|
|
signedIDToken, err = idToken.SignToken(clientKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &AccessTokenError{
|
|
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
|
|
ErrorDescription: "cannot sign token",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &AccessTokenResponse{
|
|
|
|
AccessToken: signedAccessToken,
|
|
|
|
TokenType: TokenTypeBearer,
|
|
|
|
ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
|
|
|
|
RefreshToken: signedRefreshToken,
|
|
|
|
IDToken: signedIDToken,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// returns a list of "org" and "org:team" strings,
|
|
|
|
// that the given user is a part of.
|
Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access (#32573)
Resolve #31609
This PR was initiated following my personal research to find the
lightest possible Single Sign-On solution for self-hosted setups. The
existing solutions often seemed too enterprise-oriented, involving many
moving parts and services, demanding significant resources while
promising planetary-scale capabilities. Others were adequate in
supporting basic OAuth2 flows but lacked proper user management
features, such as a change password UI.
Gitea hits the sweet spot for me, provided it supports more granular
access permissions for resources under users who accept the OAuth2
application.
This PR aims to introduce granularity in handling user resources as
nonintrusively and simply as possible. It allows third parties to inform
users about their intent to not ask for the full access and instead
request a specific, reduced scope. If the provided scopes are **only**
the typical ones for OIDC/OAuth2—`openid`, `profile`, `email`, and
`groups`—everything remains unchanged (currently full access to user's
resources). Additionally, this PR supports processing scopes already
introduced with [personal
tokens](https://docs.gitea.com/development/oauth2-provider#scopes) (e.g.
`read:user`, `write:issue`, `read:group`, `write:repository`...)
Personal tokens define scopes around specific resources: user info,
repositories, issues, packages, organizations, notifications,
miscellaneous, admin, and activitypub, with access delineated by read
and/or write permissions.
The initial case I wanted to address was to have Gitea act as an OAuth2
Identity Provider. To achieve that, with this PR, I would only add
`openid public-only` to provide access token to the third party to
authenticate the Gitea's user but no further access to the API and users
resources.
Another example: if a third party wanted to interact solely with Issues,
it would need to add `read:user` (for authorization) and
`read:issue`/`write:issue` to manage Issues.
My approach is based on my understanding of how scopes can be utilized,
supported by examples like [Sample Use Cases: Scopes and
Claims](https://auth0.com/docs/get-started/apis/scopes/sample-use-cases-scopes-and-claims)
on auth0.com.
I renamed `CheckOAuthAccessToken` to `GetOAuthAccessTokenScopeAndUserID`
so now it returns AccessTokenScope and user's ID. In the case of
additional scopes in `userIDFromToken` the default `all` would be
reduced to whatever was asked via those scopes. The main difference is
the opportunity to reduce the permissions from `all`, as is currently
the case, to what is provided by the additional scopes described above.
Screenshots:
![Screenshot_20241121_121405](https://github.com/user-attachments/assets/29deaed7-4333-4b02-8898-b822e6f2463e)
![Screenshot_20241121_120211](https://github.com/user-attachments/assets/7a4a4ef7-409c-4116-9d5f-2fe00eb37167)
![Screenshot_20241121_120119](https://github.com/user-attachments/assets/aa52c1a2-212d-4e64-bcdf-7122cee49eb6)
![Screenshot_20241121_120018](https://github.com/user-attachments/assets/9eac318c-e381-4ea9-9e2c-3a3f60319e47)
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-11-21 21:06:41 -07:00
|
|
|
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
|
2024-11-13 22:31:47 -07:00
|
|
|
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
|
|
|
|
UserID: user.ID,
|
Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access (#32573)
Resolve #31609
This PR was initiated following my personal research to find the
lightest possible Single Sign-On solution for self-hosted setups. The
existing solutions often seemed too enterprise-oriented, involving many
moving parts and services, demanding significant resources while
promising planetary-scale capabilities. Others were adequate in
supporting basic OAuth2 flows but lacked proper user management
features, such as a change password UI.
Gitea hits the sweet spot for me, provided it supports more granular
access permissions for resources under users who accept the OAuth2
application.
This PR aims to introduce granularity in handling user resources as
nonintrusively and simply as possible. It allows third parties to inform
users about their intent to not ask for the full access and instead
request a specific, reduced scope. If the provided scopes are **only**
the typical ones for OIDC/OAuth2—`openid`, `profile`, `email`, and
`groups`—everything remains unchanged (currently full access to user's
resources). Additionally, this PR supports processing scopes already
introduced with [personal
tokens](https://docs.gitea.com/development/oauth2-provider#scopes) (e.g.
`read:user`, `write:issue`, `read:group`, `write:repository`...)
Personal tokens define scopes around specific resources: user info,
repositories, issues, packages, organizations, notifications,
miscellaneous, admin, and activitypub, with access delineated by read
and/or write permissions.
The initial case I wanted to address was to have Gitea act as an OAuth2
Identity Provider. To achieve that, with this PR, I would only add
`openid public-only` to provide access token to the third party to
authenticate the Gitea's user but no further access to the API and users
resources.
Another example: if a third party wanted to interact solely with Issues,
it would need to add `read:user` (for authorization) and
`read:issue`/`write:issue` to manage Issues.
My approach is based on my understanding of how scopes can be utilized,
supported by examples like [Sample Use Cases: Scopes and
Claims](https://auth0.com/docs/get-started/apis/scopes/sample-use-cases-scopes-and-claims)
on auth0.com.
I renamed `CheckOAuthAccessToken` to `GetOAuthAccessTokenScopeAndUserID`
so now it returns AccessTokenScope and user's ID. In the case of
additional scopes in `userIDFromToken` the default `all` would be
reduced to whatever was asked via those scopes. The main difference is
the opportunity to reduce the permissions from `all`, as is currently
the case, to what is provided by the additional scopes described above.
Screenshots:
![Screenshot_20241121_121405](https://github.com/user-attachments/assets/29deaed7-4333-4b02-8898-b822e6f2463e)
![Screenshot_20241121_120211](https://github.com/user-attachments/assets/7a4a4ef7-409c-4116-9d5f-2fe00eb37167)
![Screenshot_20241121_120119](https://github.com/user-attachments/assets/aa52c1a2-212d-4e64-bcdf-7122cee49eb6)
![Screenshot_20241121_120018](https://github.com/user-attachments/assets/9eac318c-e381-4ea9-9e2c-3a3f60319e47)
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-11-21 21:06:41 -07:00
|
|
|
IncludePrivate: !onlyPublicGroups,
|
2024-11-13 22:31:47 -07:00
|
|
|
})
|
2024-10-01 18:03:19 -06:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("GetUserOrgList: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var groups []string
|
|
|
|
for _, org := range orgs {
|
|
|
|
groups = append(groups, org.Name)
|
|
|
|
teams, err := org.LoadTeams(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("LoadTeams: %w", err)
|
|
|
|
}
|
|
|
|
for _, team := range teams {
|
|
|
|
if team.IsMember(ctx, user.ID) {
|
|
|
|
groups = append(groups, org.Name+":"+team.LowerName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return groups, nil
|
|
|
|
}
|