mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 23:54:25 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			215 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			215 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package oauth2_provider //nolint
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"fmt"
 | 
						|
 | 
						|
	auth "code.gitea.io/gitea/models/auth"
 | 
						|
	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"`
 | 
						|
}
 | 
						|
 | 
						|
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") {
 | 
						|
			idToken.Name = user.GetDisplayName()
 | 
						|
			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") {
 | 
						|
			groups, err := GetOAuthGroupsForUser(ctx, user)
 | 
						|
			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.
 | 
						|
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) {
 | 
						|
	orgs, err := org_model.GetUserOrgsList(ctx, user)
 | 
						|
	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
 | 
						|
}
 |