code.gitea.io/gitea@v1.22.3/models/auth/oauth2.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package auth
     5  
     6  import (
     7  	"context"
     8  	"crypto/sha256"
     9  	"encoding/base32"
    10  	"encoding/base64"
    11  	"errors"
    12  	"fmt"
    13  	"net"
    14  	"net/url"
    15  	"strings"
    16  
    17  	"code.gitea.io/gitea/models/db"
    18  	"code.gitea.io/gitea/modules/container"
    19  	"code.gitea.io/gitea/modules/setting"
    20  	"code.gitea.io/gitea/modules/timeutil"
    21  	"code.gitea.io/gitea/modules/util"
    22  
    23  	uuid "github.com/google/uuid"
    24  	"golang.org/x/crypto/bcrypt"
    25  	"xorm.io/builder"
    26  	"xorm.io/xorm"
    27  )
    28  
    29  // OAuth2Application represents an OAuth2 client (RFC 6749)
    30  type OAuth2Application struct {
    31  	ID           int64 `xorm:"pk autoincr"`
    32  	UID          int64 `xorm:"INDEX"`
    33  	Name         string
    34  	ClientID     string `xorm:"unique"`
    35  	ClientSecret string
    36  	// OAuth defines both Confidential and Public client types
    37  	// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1
    38  	// "Authorization servers MUST record the client type in the client registration details"
    39  	// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
    40  	ConfidentialClient bool               `xorm:"NOT NULL DEFAULT TRUE"`
    41  	RedirectURIs       []string           `xorm:"redirect_uris JSON TEXT"`
    42  	CreatedUnix        timeutil.TimeStamp `xorm:"INDEX created"`
    43  	UpdatedUnix        timeutil.TimeStamp `xorm:"INDEX updated"`
    44  }
    45  
    46  func init() {
    47  	db.RegisterModel(new(OAuth2Application))
    48  	db.RegisterModel(new(OAuth2AuthorizationCode))
    49  	db.RegisterModel(new(OAuth2Grant))
    50  }
    51  
    52  type BuiltinOAuth2Application struct {
    53  	ConfigName   string
    54  	DisplayName  string
    55  	RedirectURIs []string
    56  }
    57  
    58  func BuiltinApplications() map[string]*BuiltinOAuth2Application {
    59  	m := make(map[string]*BuiltinOAuth2Application)
    60  	m["a4792ccc-144e-407e-86c9-5e7d8d9c3269"] = &BuiltinOAuth2Application{
    61  		ConfigName:   "git-credential-oauth",
    62  		DisplayName:  "git-credential-oauth",
    63  		RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
    64  	}
    65  	m["e90ee53c-94e2-48ac-9358-a874fb9e0662"] = &BuiltinOAuth2Application{
    66  		ConfigName:   "git-credential-manager",
    67  		DisplayName:  "Git Credential Manager",
    68  		RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
    69  	}
    70  	m["d57cb8c4-630c-4168-8324-ec79935e18d4"] = &BuiltinOAuth2Application{
    71  		ConfigName:   "tea",
    72  		DisplayName:  "tea",
    73  		RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
    74  	}
    75  	return m
    76  }
    77  
    78  func Init(ctx context.Context) error {
    79  	builtinApps := BuiltinApplications()
    80  	var builtinAllClientIDs []string
    81  	for clientID := range builtinApps {
    82  		builtinAllClientIDs = append(builtinAllClientIDs, clientID)
    83  	}
    84  
    85  	var registeredApps []*OAuth2Application
    86  	if err := db.GetEngine(ctx).In("client_id", builtinAllClientIDs).Find(&registeredApps); err != nil {
    87  		return err
    88  	}
    89  
    90  	clientIDsToAdd := container.Set[string]{}
    91  	for _, configName := range setting.OAuth2.DefaultApplications {
    92  		found := false
    93  		for clientID, builtinApp := range builtinApps {
    94  			if builtinApp.ConfigName == configName {
    95  				clientIDsToAdd.Add(clientID) // add all user-configured apps to the "add" list
    96  				found = true
    97  			}
    98  		}
    99  		if !found {
   100  			return fmt.Errorf("unknown oauth2 application: %q", configName)
   101  		}
   102  	}
   103  	clientIDsToDelete := container.Set[string]{}
   104  	for _, app := range registeredApps {
   105  		if !clientIDsToAdd.Contains(app.ClientID) {
   106  			clientIDsToDelete.Add(app.ClientID) // if a registered app is not in the "add" list, it should be deleted
   107  		}
   108  	}
   109  	for _, app := range registeredApps {
   110  		clientIDsToAdd.Remove(app.ClientID) // no need to re-add existing (registered) apps, so remove them from the set
   111  	}
   112  
   113  	for _, app := range registeredApps {
   114  		if clientIDsToDelete.Contains(app.ClientID) {
   115  			if err := deleteOAuth2Application(ctx, app.ID, 0); err != nil {
   116  				return err
   117  			}
   118  		}
   119  	}
   120  	for clientID := range clientIDsToAdd {
   121  		builtinApp := builtinApps[clientID]
   122  		if err := db.Insert(ctx, &OAuth2Application{
   123  			Name:         builtinApp.DisplayName,
   124  			ClientID:     clientID,
   125  			RedirectURIs: builtinApp.RedirectURIs,
   126  		}); err != nil {
   127  			return err
   128  		}
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  // TableName sets the table name to `oauth2_application`
   135  func (app *OAuth2Application) TableName() string {
   136  	return "oauth2_application"
   137  }
   138  
   139  // ContainsRedirectURI checks if redirectURI is allowed for app
   140  func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
   141  	// OAuth2 requires the redirect URI to be an exact match, no dynamic parts are allowed.
   142  	// https://stackoverflow.com/questions/55524480/should-dynamic-query-parameters-be-present-in-the-redirection-uri-for-an-oauth2
   143  	// https://www.rfc-editor.org/rfc/rfc6819#section-5.2.3.3
   144  	// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
   145  	// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1
   146  	contains := func(s string) bool {
   147  		s = strings.TrimSuffix(strings.ToLower(s), "/")
   148  		for _, u := range app.RedirectURIs {
   149  			if strings.TrimSuffix(strings.ToLower(u), "/") == s {
   150  				return true
   151  			}
   152  		}
   153  		return false
   154  	}
   155  	if !app.ConfidentialClient {
   156  		uri, err := url.Parse(redirectURI)
   157  		// ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
   158  		if err == nil && uri.Scheme == "http" && uri.Port() != "" {
   159  			ip := net.ParseIP(uri.Hostname())
   160  			if ip != nil && ip.IsLoopback() {
   161  				// strip port
   162  				uri.Host = uri.Hostname()
   163  				if contains(uri.String()) {
   164  					return true
   165  				}
   166  			}
   167  		}
   168  	}
   169  	return contains(redirectURI)
   170  }
   171  
   172  // Base32 characters, but lowercased.
   173  const lowerBase32Chars = "abcdefghijklmnopqrstuvwxyz234567"
   174  
   175  // base32 encoder that uses lowered characters without padding.
   176  var base32Lower = base32.NewEncoding(lowerBase32Chars).WithPadding(base32.NoPadding)
   177  
   178  // GenerateClientSecret will generate the client secret and returns the plaintext and saves the hash at the database
   179  func (app *OAuth2Application) GenerateClientSecret(ctx context.Context) (string, error) {
   180  	rBytes, err := util.CryptoRandomBytes(32)
   181  	if err != nil {
   182  		return "", err
   183  	}
   184  	// Add a prefix to the base32, this is in order to make it easier
   185  	// for code scanners to grab sensitive tokens.
   186  	clientSecret := "gto_" + base32Lower.EncodeToString(rBytes)
   187  
   188  	hashedSecret, err := bcrypt.GenerateFromPassword([]byte(clientSecret), bcrypt.DefaultCost)
   189  	if err != nil {
   190  		return "", err
   191  	}
   192  	app.ClientSecret = string(hashedSecret)
   193  	if _, err := db.GetEngine(ctx).ID(app.ID).Cols("client_secret").Update(app); err != nil {
   194  		return "", err
   195  	}
   196  	return clientSecret, nil
   197  }
   198  
   199  // ValidateClientSecret validates the given secret by the hash saved in database
   200  func (app *OAuth2Application) ValidateClientSecret(secret []byte) bool {
   201  	return bcrypt.CompareHashAndPassword([]byte(app.ClientSecret), secret) == nil
   202  }
   203  
   204  // GetGrantByUserID returns a OAuth2Grant by its user and application ID
   205  func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64) (grant *OAuth2Grant, err error) {
   206  	grant = new(OAuth2Grant)
   207  	if has, err := db.GetEngine(ctx).Where("user_id = ? AND application_id = ?", userID, app.ID).Get(grant); err != nil {
   208  		return nil, err
   209  	} else if !has {
   210  		return nil, nil
   211  	}
   212  	return grant, nil
   213  }
   214  
   215  // CreateGrant generates a grant for an user
   216  func (app *OAuth2Application) CreateGrant(ctx context.Context, userID int64, scope string) (*OAuth2Grant, error) {
   217  	grant := &OAuth2Grant{
   218  		ApplicationID: app.ID,
   219  		UserID:        userID,
   220  		Scope:         scope,
   221  	}
   222  	err := db.Insert(ctx, grant)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return grant, nil
   227  }
   228  
   229  // GetOAuth2ApplicationByClientID returns the oauth2 application with the given client_id. Returns an error if not found.
   230  func GetOAuth2ApplicationByClientID(ctx context.Context, clientID string) (app *OAuth2Application, err error) {
   231  	app = new(OAuth2Application)
   232  	has, err := db.GetEngine(ctx).Where("client_id = ?", clientID).Get(app)
   233  	if !has {
   234  		return nil, ErrOAuthClientIDInvalid{ClientID: clientID}
   235  	}
   236  	return app, err
   237  }
   238  
   239  // GetOAuth2ApplicationByID returns the oauth2 application with the given id. Returns an error if not found.
   240  func GetOAuth2ApplicationByID(ctx context.Context, id int64) (app *OAuth2Application, err error) {
   241  	app = new(OAuth2Application)
   242  	has, err := db.GetEngine(ctx).ID(id).Get(app)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	if !has {
   247  		return nil, ErrOAuthApplicationNotFound{ID: id}
   248  	}
   249  	return app, nil
   250  }
   251  
   252  // CreateOAuth2ApplicationOptions holds options to create an oauth2 application
   253  type CreateOAuth2ApplicationOptions struct {
   254  	Name               string
   255  	UserID             int64
   256  	ConfidentialClient bool
   257  	RedirectURIs       []string
   258  }
   259  
   260  // CreateOAuth2Application inserts a new oauth2 application
   261  func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOptions) (*OAuth2Application, error) {
   262  	clientID := uuid.New().String()
   263  	app := &OAuth2Application{
   264  		UID:                opts.UserID,
   265  		Name:               opts.Name,
   266  		ClientID:           clientID,
   267  		RedirectURIs:       opts.RedirectURIs,
   268  		ConfidentialClient: opts.ConfidentialClient,
   269  	}
   270  	if err := db.Insert(ctx, app); err != nil {
   271  		return nil, err
   272  	}
   273  	return app, nil
   274  }
   275  
   276  // UpdateOAuth2ApplicationOptions holds options to update an oauth2 application
   277  type UpdateOAuth2ApplicationOptions struct {
   278  	ID                 int64
   279  	Name               string
   280  	UserID             int64
   281  	ConfidentialClient bool
   282  	RedirectURIs       []string
   283  }
   284  
   285  // UpdateOAuth2Application updates an oauth2 application
   286  func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOptions) (*OAuth2Application, error) {
   287  	ctx, committer, err := db.TxContext(ctx)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	defer committer.Close()
   292  
   293  	app, err := GetOAuth2ApplicationByID(ctx, opts.ID)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  	if app.UID != opts.UserID {
   298  		return nil, errors.New("UID mismatch")
   299  	}
   300  	builtinApps := BuiltinApplications()
   301  	if _, builtin := builtinApps[app.ClientID]; builtin {
   302  		return nil, fmt.Errorf("failed to edit OAuth2 application: application is locked: %s", app.ClientID)
   303  	}
   304  
   305  	app.Name = opts.Name
   306  	app.RedirectURIs = opts.RedirectURIs
   307  	app.ConfidentialClient = opts.ConfidentialClient
   308  
   309  	if err = updateOAuth2Application(ctx, app); err != nil {
   310  		return nil, err
   311  	}
   312  	app.ClientSecret = ""
   313  
   314  	return app, committer.Commit()
   315  }
   316  
   317  func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error {
   318  	if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client").Update(app); err != nil {
   319  		return err
   320  	}
   321  	return nil
   322  }
   323  
   324  func deleteOAuth2Application(ctx context.Context, id, userid int64) error {
   325  	sess := db.GetEngine(ctx)
   326  	// the userid could be 0 if the app is instance-wide
   327  	if deleted, err := sess.Where(builder.Eq{"id": id, "uid": userid}).Delete(&OAuth2Application{}); err != nil {
   328  		return err
   329  	} else if deleted == 0 {
   330  		return ErrOAuthApplicationNotFound{ID: id}
   331  	}
   332  	codes := make([]*OAuth2AuthorizationCode, 0)
   333  	// delete correlating auth codes
   334  	if err := sess.Join("INNER", "oauth2_grant",
   335  		"oauth2_authorization_code.grant_id = oauth2_grant.id AND oauth2_grant.application_id = ?", id).Find(&codes); err != nil {
   336  		return err
   337  	}
   338  	codeIDs := make([]int64, 0, len(codes))
   339  	for _, grant := range codes {
   340  		codeIDs = append(codeIDs, grant.ID)
   341  	}
   342  
   343  	if _, err := sess.In("id", codeIDs).Delete(new(OAuth2AuthorizationCode)); err != nil {
   344  		return err
   345  	}
   346  
   347  	if _, err := sess.Where("application_id = ?", id).Delete(new(OAuth2Grant)); err != nil {
   348  		return err
   349  	}
   350  	return nil
   351  }
   352  
   353  // DeleteOAuth2Application deletes the application with the given id and the grants and auth codes related to it. It checks if the userid was the creator of the app.
   354  func DeleteOAuth2Application(ctx context.Context, id, userid int64) error {
   355  	ctx, committer, err := db.TxContext(ctx)
   356  	if err != nil {
   357  		return err
   358  	}
   359  	defer committer.Close()
   360  	app, err := GetOAuth2ApplicationByID(ctx, id)
   361  	if err != nil {
   362  		return err
   363  	}
   364  	builtinApps := BuiltinApplications()
   365  	if _, builtin := builtinApps[app.ClientID]; builtin {
   366  		return fmt.Errorf("failed to delete OAuth2 application: application is locked: %s", app.ClientID)
   367  	}
   368  	if err := deleteOAuth2Application(ctx, id, userid); err != nil {
   369  		return err
   370  	}
   371  	return committer.Commit()
   372  }
   373  
   374  //////////////////////////////////////////////////////
   375  
   376  // OAuth2AuthorizationCode is a code to obtain an access token in combination with the client secret once. It has a limited lifetime.
   377  type OAuth2AuthorizationCode struct {
   378  	ID                  int64        `xorm:"pk autoincr"`
   379  	Grant               *OAuth2Grant `xorm:"-"`
   380  	GrantID             int64
   381  	Code                string `xorm:"INDEX unique"`
   382  	CodeChallenge       string
   383  	CodeChallengeMethod string
   384  	RedirectURI         string
   385  	ValidUntil          timeutil.TimeStamp `xorm:"index"`
   386  }
   387  
   388  // TableName sets the table name to `oauth2_authorization_code`
   389  func (code *OAuth2AuthorizationCode) TableName() string {
   390  	return "oauth2_authorization_code"
   391  }
   392  
   393  // GenerateRedirectURI generates a redirect URI for a successful authorization request. State will be used if not empty.
   394  func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL, error) {
   395  	redirect, err := url.Parse(code.RedirectURI)
   396  	if err != nil {
   397  		return nil, err
   398  	}
   399  	q := redirect.Query()
   400  	if state != "" {
   401  		q.Set("state", state)
   402  	}
   403  	q.Set("code", code.Code)
   404  	redirect.RawQuery = q.Encode()
   405  	return redirect, err
   406  }
   407  
   408  // Invalidate deletes the auth code from the database to invalidate this code
   409  func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error {
   410  	_, err := db.GetEngine(ctx).ID(code.ID).NoAutoCondition().Delete(code)
   411  	return err
   412  }
   413  
   414  // ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation.
   415  func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool {
   416  	switch code.CodeChallengeMethod {
   417  	case "S256":
   418  		// base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6
   419  		h := sha256.Sum256([]byte(verifier))
   420  		hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:])
   421  		return hashedVerifier == code.CodeChallenge
   422  	case "plain":
   423  		return verifier == code.CodeChallenge
   424  	case "":
   425  		return true
   426  	default:
   427  		// unsupported method -> return false
   428  		return false
   429  	}
   430  }
   431  
   432  // GetOAuth2AuthorizationByCode returns an authorization by its code
   433  func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth2AuthorizationCode, err error) {
   434  	auth = new(OAuth2AuthorizationCode)
   435  	if has, err := db.GetEngine(ctx).Where("code = ?", code).Get(auth); err != nil {
   436  		return nil, err
   437  	} else if !has {
   438  		return nil, nil
   439  	}
   440  	auth.Grant = new(OAuth2Grant)
   441  	if has, err := db.GetEngine(ctx).ID(auth.GrantID).Get(auth.Grant); err != nil {
   442  		return nil, err
   443  	} else if !has {
   444  		return nil, nil
   445  	}
   446  	return auth, nil
   447  }
   448  
   449  //////////////////////////////////////////////////////
   450  
   451  // OAuth2Grant represents the permission of an user for a specific application to access resources
   452  type OAuth2Grant struct {
   453  	ID            int64              `xorm:"pk autoincr"`
   454  	UserID        int64              `xorm:"INDEX unique(user_application)"`
   455  	Application   *OAuth2Application `xorm:"-"`
   456  	ApplicationID int64              `xorm:"INDEX unique(user_application)"`
   457  	Counter       int64              `xorm:"NOT NULL DEFAULT 1"`
   458  	Scope         string             `xorm:"TEXT"`
   459  	Nonce         string             `xorm:"TEXT"`
   460  	CreatedUnix   timeutil.TimeStamp `xorm:"created"`
   461  	UpdatedUnix   timeutil.TimeStamp `xorm:"updated"`
   462  }
   463  
   464  // TableName sets the table name to `oauth2_grant`
   465  func (grant *OAuth2Grant) TableName() string {
   466  	return "oauth2_grant"
   467  }
   468  
   469  // GenerateNewAuthorizationCode generates a new authorization code for a grant and saves it to the database
   470  func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redirectURI, codeChallenge, codeChallengeMethod string) (code *OAuth2AuthorizationCode, err error) {
   471  	rBytes, err := util.CryptoRandomBytes(32)
   472  	if err != nil {
   473  		return &OAuth2AuthorizationCode{}, err
   474  	}
   475  	// Add a prefix to the base32, this is in order to make it easier
   476  	// for code scanners to grab sensitive tokens.
   477  	codeSecret := "gta_" + base32Lower.EncodeToString(rBytes)
   478  
   479  	code = &OAuth2AuthorizationCode{
   480  		Grant:               grant,
   481  		GrantID:             grant.ID,
   482  		RedirectURI:         redirectURI,
   483  		Code:                codeSecret,
   484  		CodeChallenge:       codeChallenge,
   485  		CodeChallengeMethod: codeChallengeMethod,
   486  	}
   487  	if err := db.Insert(ctx, code); err != nil {
   488  		return nil, err
   489  	}
   490  	return code, nil
   491  }
   492  
   493  // IncreaseCounter increases the counter and updates the grant
   494  func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error {
   495  	_, err := db.GetEngine(ctx).ID(grant.ID).Incr("counter").Update(new(OAuth2Grant))
   496  	if err != nil {
   497  		return err
   498  	}
   499  	updatedGrant, err := GetOAuth2GrantByID(ctx, grant.ID)
   500  	if err != nil {
   501  		return err
   502  	}
   503  	grant.Counter = updatedGrant.Counter
   504  	return nil
   505  }
   506  
   507  // ScopeContains returns true if the grant scope contains the specified scope
   508  func (grant *OAuth2Grant) ScopeContains(scope string) bool {
   509  	for _, currentScope := range strings.Split(grant.Scope, " ") {
   510  		if scope == currentScope {
   511  			return true
   512  		}
   513  	}
   514  	return false
   515  }
   516  
   517  // SetNonce updates the current nonce value of a grant
   518  func (grant *OAuth2Grant) SetNonce(ctx context.Context, nonce string) error {
   519  	grant.Nonce = nonce
   520  	_, err := db.GetEngine(ctx).ID(grant.ID).Cols("nonce").Update(grant)
   521  	if err != nil {
   522  		return err
   523  	}
   524  	return nil
   525  }
   526  
   527  // GetOAuth2GrantByID returns the grant with the given ID
   528  func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
   529  	grant = new(OAuth2Grant)
   530  	if has, err := db.GetEngine(ctx).ID(id).Get(grant); err != nil {
   531  		return nil, err
   532  	} else if !has {
   533  		return nil, nil
   534  	}
   535  	return grant, err
   536  }
   537  
   538  // GetOAuth2GrantsByUserID lists all grants of a certain user
   539  func GetOAuth2GrantsByUserID(ctx context.Context, uid int64) ([]*OAuth2Grant, error) {
   540  	type joinedOAuth2Grant struct {
   541  		Grant       *OAuth2Grant       `xorm:"extends"`
   542  		Application *OAuth2Application `xorm:"extends"`
   543  	}
   544  	var results *xorm.Rows
   545  	var err error
   546  	if results, err = db.GetEngine(ctx).
   547  		Table("oauth2_grant").
   548  		Where("user_id = ?", uid).
   549  		Join("INNER", "oauth2_application", "application_id = oauth2_application.id").
   550  		Rows(new(joinedOAuth2Grant)); err != nil {
   551  		return nil, err
   552  	}
   553  	defer results.Close()
   554  	grants := make([]*OAuth2Grant, 0)
   555  	for results.Next() {
   556  		joinedGrant := new(joinedOAuth2Grant)
   557  		if err := results.Scan(joinedGrant); err != nil {
   558  			return nil, err
   559  		}
   560  		joinedGrant.Grant.Application = joinedGrant.Application
   561  		grants = append(grants, joinedGrant.Grant)
   562  	}
   563  	return grants, nil
   564  }
   565  
   566  // RevokeOAuth2Grant deletes the grant with grantID and userID
   567  func RevokeOAuth2Grant(ctx context.Context, grantID, userID int64) error {
   568  	_, err := db.GetEngine(ctx).Where(builder.Eq{"id": grantID, "user_id": userID}).Delete(&OAuth2Grant{})
   569  	return err
   570  }
   571  
   572  // ErrOAuthClientIDInvalid will be thrown if client id cannot be found
   573  type ErrOAuthClientIDInvalid struct {
   574  	ClientID string
   575  }
   576  
   577  // IsErrOauthClientIDInvalid checks if an error is a ErrOAuthClientIDInvalid.
   578  func IsErrOauthClientIDInvalid(err error) bool {
   579  	_, ok := err.(ErrOAuthClientIDInvalid)
   580  	return ok
   581  }
   582  
   583  // Error returns the error message
   584  func (err ErrOAuthClientIDInvalid) Error() string {
   585  	return fmt.Sprintf("Client ID invalid [Client ID: %s]", err.ClientID)
   586  }
   587  
   588  // Unwrap unwraps this as a ErrNotExist err
   589  func (err ErrOAuthClientIDInvalid) Unwrap() error {
   590  	return util.ErrNotExist
   591  }
   592  
   593  // ErrOAuthApplicationNotFound will be thrown if id cannot be found
   594  type ErrOAuthApplicationNotFound struct {
   595  	ID int64
   596  }
   597  
   598  // IsErrOAuthApplicationNotFound checks if an error is a ErrReviewNotExist.
   599  func IsErrOAuthApplicationNotFound(err error) bool {
   600  	_, ok := err.(ErrOAuthApplicationNotFound)
   601  	return ok
   602  }
   603  
   604  // Error returns the error message
   605  func (err ErrOAuthApplicationNotFound) Error() string {
   606  	return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID)
   607  }
   608  
   609  // Unwrap unwraps this as a ErrNotExist err
   610  func (err ErrOAuthApplicationNotFound) Unwrap() error {
   611  	return util.ErrNotExist
   612  }
   613  
   614  // GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name
   615  func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) {
   616  	authSource := new(Source)
   617  	has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
   618  	if err != nil {
   619  		return nil, err
   620  	}
   621  
   622  	if !has {
   623  		return nil, fmt.Errorf("oauth2 source not found, name: %q", name)
   624  	}
   625  
   626  	return authSource, nil
   627  }
   628  
   629  func DeleteOAuth2RelictsByUserID(ctx context.Context, userID int64) error {
   630  	deleteCond := builder.Select("id").From("oauth2_grant").Where(builder.Eq{"oauth2_grant.user_id": userID})
   631  
   632  	if _, err := db.GetEngine(ctx).In("grant_id", deleteCond).
   633  		Delete(&OAuth2AuthorizationCode{}); err != nil {
   634  		return err
   635  	}
   636  
   637  	if err := db.DeleteBeans(ctx,
   638  		&OAuth2Application{UID: userID},
   639  		&OAuth2Grant{UserID: userID},
   640  	); err != nil {
   641  		return fmt.Errorf("DeleteBeans: %w", err)
   642  	}
   643  
   644  	return nil
   645  }