summaryrefslogtreecommitdiffstats
path: root/models/auth/oauth2.go
diff options
context:
space:
mode:
Diffstat (limited to 'models/auth/oauth2.go')
-rw-r--r--models/auth/oauth2.go676
1 files changed, 676 insertions, 0 deletions
diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go
new file mode 100644
index 0000000..125d64b
--- /dev/null
+++ b/models/auth/oauth2.go
@@ -0,0 +1,676 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/base32"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "net"
+ "net/url"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/container"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+
+ uuid "github.com/google/uuid"
+ "golang.org/x/crypto/bcrypt"
+ "xorm.io/builder"
+ "xorm.io/xorm"
+)
+
+// OAuth2Application represents an OAuth2 client (RFC 6749)
+type OAuth2Application struct {
+ ID int64 `xorm:"pk autoincr"`
+ UID int64 `xorm:"INDEX"`
+ Name string
+ ClientID string `xorm:"unique"`
+ ClientSecret string
+ // OAuth defines both Confidential and Public client types
+ // https://datatracker.ietf.org/doc/html/rfc6749#section-2.1
+ // "Authorization servers MUST record the client type in the client registration details"
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
+ ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"`
+ RedirectURIs []string `xorm:"redirect_uris JSON TEXT"`
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+}
+
+func init() {
+ db.RegisterModel(new(OAuth2Application))
+ db.RegisterModel(new(OAuth2AuthorizationCode))
+ db.RegisterModel(new(OAuth2Grant))
+}
+
+type BuiltinOAuth2Application struct {
+ ConfigName string
+ DisplayName string
+ RedirectURIs []string
+}
+
+func BuiltinApplications() map[string]*BuiltinOAuth2Application {
+ m := make(map[string]*BuiltinOAuth2Application)
+ m["a4792ccc-144e-407e-86c9-5e7d8d9c3269"] = &BuiltinOAuth2Application{
+ ConfigName: "git-credential-oauth",
+ DisplayName: "git-credential-oauth",
+ RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
+ }
+ m["e90ee53c-94e2-48ac-9358-a874fb9e0662"] = &BuiltinOAuth2Application{
+ ConfigName: "git-credential-manager",
+ DisplayName: "Git Credential Manager",
+ RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
+ }
+ m["d57cb8c4-630c-4168-8324-ec79935e18d4"] = &BuiltinOAuth2Application{
+ ConfigName: "tea",
+ DisplayName: "tea",
+ RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
+ }
+ return m
+}
+
+func BuiltinApplicationsClientIDs() (clientIDs []string) {
+ for clientID := range BuiltinApplications() {
+ clientIDs = append(clientIDs, clientID)
+ }
+ return clientIDs
+}
+
+func Init(ctx context.Context) error {
+ builtinApps := BuiltinApplications()
+ var builtinAllClientIDs []string
+ for clientID := range builtinApps {
+ builtinAllClientIDs = append(builtinAllClientIDs, clientID)
+ }
+
+ var registeredApps []*OAuth2Application
+ if err := db.GetEngine(ctx).In("client_id", builtinAllClientIDs).Find(&registeredApps); err != nil {
+ return err
+ }
+
+ clientIDsToAdd := container.Set[string]{}
+ for _, configName := range setting.OAuth2.DefaultApplications {
+ found := false
+ for clientID, builtinApp := range builtinApps {
+ if builtinApp.ConfigName == configName {
+ clientIDsToAdd.Add(clientID) // add all user-configured apps to the "add" list
+ found = true
+ }
+ }
+ if !found {
+ return fmt.Errorf("unknown oauth2 application: %q", configName)
+ }
+ }
+ clientIDsToDelete := container.Set[string]{}
+ for _, app := range registeredApps {
+ if !clientIDsToAdd.Contains(app.ClientID) {
+ clientIDsToDelete.Add(app.ClientID) // if a registered app is not in the "add" list, it should be deleted
+ }
+ }
+ for _, app := range registeredApps {
+ clientIDsToAdd.Remove(app.ClientID) // no need to re-add existing (registered) apps, so remove them from the set
+ }
+
+ for _, app := range registeredApps {
+ if clientIDsToDelete.Contains(app.ClientID) {
+ if err := deleteOAuth2Application(ctx, app.ID, 0); err != nil {
+ return err
+ }
+ }
+ }
+ for clientID := range clientIDsToAdd {
+ builtinApp := builtinApps[clientID]
+ if err := db.Insert(ctx, &OAuth2Application{
+ Name: builtinApp.DisplayName,
+ ClientID: clientID,
+ RedirectURIs: builtinApp.RedirectURIs,
+ }); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// TableName sets the table name to `oauth2_application`
+func (app *OAuth2Application) TableName() string {
+ return "oauth2_application"
+}
+
+// ContainsRedirectURI checks if redirectURI is allowed for app
+func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
+ // OAuth2 requires the redirect URI to be an exact match, no dynamic parts are allowed.
+ // https://stackoverflow.com/questions/55524480/should-dynamic-query-parameters-be-present-in-the-redirection-uri-for-an-oauth2
+ // https://www.rfc-editor.org/rfc/rfc6819#section-5.2.3.3
+ // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1
+ contains := func(s string) bool {
+ s = strings.TrimSuffix(strings.ToLower(s), "/")
+ for _, u := range app.RedirectURIs {
+ if strings.TrimSuffix(strings.ToLower(u), "/") == s {
+ return true
+ }
+ }
+ return false
+ }
+ if !app.ConfidentialClient {
+ uri, err := url.Parse(redirectURI)
+ // ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
+ if err == nil && uri.Scheme == "http" && uri.Port() != "" {
+ ip := net.ParseIP(uri.Hostname())
+ if ip != nil && ip.IsLoopback() {
+ // strip port
+ uri.Host = uri.Hostname()
+ if contains(uri.String()) {
+ return true
+ }
+ }
+ }
+ }
+ return contains(redirectURI)
+}
+
+// Base32 characters, but lowercased.
+const lowerBase32Chars = "abcdefghijklmnopqrstuvwxyz234567"
+
+// base32 encoder that uses lowered characters without padding.
+var base32Lower = base32.NewEncoding(lowerBase32Chars).WithPadding(base32.NoPadding)
+
+// GenerateClientSecret will generate the client secret and returns the plaintext and saves the hash at the database
+func (app *OAuth2Application) GenerateClientSecret(ctx context.Context) (string, error) {
+ rBytes, err := util.CryptoRandomBytes(32)
+ if err != nil {
+ return "", err
+ }
+ // Add a prefix to the base32, this is in order to make it easier
+ // for code scanners to grab sensitive tokens.
+ clientSecret := "gto_" + base32Lower.EncodeToString(rBytes)
+
+ hashedSecret, err := bcrypt.GenerateFromPassword([]byte(clientSecret), bcrypt.DefaultCost)
+ if err != nil {
+ return "", err
+ }
+ app.ClientSecret = string(hashedSecret)
+ if _, err := db.GetEngine(ctx).ID(app.ID).Cols("client_secret").Update(app); err != nil {
+ return "", err
+ }
+ return clientSecret, nil
+}
+
+// ValidateClientSecret validates the given secret by the hash saved in database
+func (app *OAuth2Application) ValidateClientSecret(secret []byte) bool {
+ return bcrypt.CompareHashAndPassword([]byte(app.ClientSecret), secret) == nil
+}
+
+// GetGrantByUserID returns a OAuth2Grant by its user and application ID
+func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64) (grant *OAuth2Grant, err error) {
+ grant = new(OAuth2Grant)
+ if has, err := db.GetEngine(ctx).Where("user_id = ? AND application_id = ?", userID, app.ID).Get(grant); err != nil {
+ return nil, err
+ } else if !has {
+ return nil, nil
+ }
+ return grant, nil
+}
+
+// CreateGrant generates a grant for an user
+func (app *OAuth2Application) CreateGrant(ctx context.Context, userID int64, scope string) (*OAuth2Grant, error) {
+ grant := &OAuth2Grant{
+ ApplicationID: app.ID,
+ UserID: userID,
+ Scope: scope,
+ }
+ err := db.Insert(ctx, grant)
+ if err != nil {
+ return nil, err
+ }
+ return grant, nil
+}
+
+// GetOAuth2ApplicationByClientID returns the oauth2 application with the given client_id. Returns an error if not found.
+func GetOAuth2ApplicationByClientID(ctx context.Context, clientID string) (app *OAuth2Application, err error) {
+ app = new(OAuth2Application)
+ has, err := db.GetEngine(ctx).Where("client_id = ?", clientID).Get(app)
+ if !has {
+ return nil, ErrOAuthClientIDInvalid{ClientID: clientID}
+ }
+ return app, err
+}
+
+// GetOAuth2ApplicationByID returns the oauth2 application with the given id. Returns an error if not found.
+func GetOAuth2ApplicationByID(ctx context.Context, id int64) (app *OAuth2Application, err error) {
+ app = new(OAuth2Application)
+ has, err := db.GetEngine(ctx).ID(id).Get(app)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, ErrOAuthApplicationNotFound{ID: id}
+ }
+ return app, nil
+}
+
+// CreateOAuth2ApplicationOptions holds options to create an oauth2 application
+type CreateOAuth2ApplicationOptions struct {
+ Name string
+ UserID int64
+ ConfidentialClient bool
+ RedirectURIs []string
+}
+
+// CreateOAuth2Application inserts a new oauth2 application
+func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOptions) (*OAuth2Application, error) {
+ clientID := uuid.New().String()
+ app := &OAuth2Application{
+ UID: opts.UserID,
+ Name: opts.Name,
+ ClientID: clientID,
+ RedirectURIs: opts.RedirectURIs,
+ ConfidentialClient: opts.ConfidentialClient,
+ }
+ if err := db.Insert(ctx, app); err != nil {
+ return nil, err
+ }
+ return app, nil
+}
+
+// UpdateOAuth2ApplicationOptions holds options to update an oauth2 application
+type UpdateOAuth2ApplicationOptions struct {
+ ID int64
+ Name string
+ UserID int64
+ ConfidentialClient bool
+ RedirectURIs []string
+}
+
+// UpdateOAuth2Application updates an oauth2 application
+func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOptions) (*OAuth2Application, error) {
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer committer.Close()
+
+ app, err := GetOAuth2ApplicationByID(ctx, opts.ID)
+ if err != nil {
+ return nil, err
+ }
+ if app.UID != opts.UserID {
+ return nil, errors.New("UID mismatch")
+ }
+ builtinApps := BuiltinApplications()
+ if _, builtin := builtinApps[app.ClientID]; builtin {
+ return nil, fmt.Errorf("failed to edit OAuth2 application: application is locked: %s", app.ClientID)
+ }
+
+ app.Name = opts.Name
+ app.RedirectURIs = opts.RedirectURIs
+ app.ConfidentialClient = opts.ConfidentialClient
+
+ if err = updateOAuth2Application(ctx, app); err != nil {
+ return nil, err
+ }
+ app.ClientSecret = ""
+
+ return app, committer.Commit()
+}
+
+func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error {
+ if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client").Update(app); err != nil {
+ return err
+ }
+ return nil
+}
+
+func deleteOAuth2Application(ctx context.Context, id, userid int64) error {
+ sess := db.GetEngine(ctx)
+ // the userid could be 0 if the app is instance-wide
+ if deleted, err := sess.Where(builder.Eq{"id": id, "uid": userid}).Delete(&OAuth2Application{}); err != nil {
+ return err
+ } else if deleted == 0 {
+ return ErrOAuthApplicationNotFound{ID: id}
+ }
+ codes := make([]*OAuth2AuthorizationCode, 0)
+ // delete correlating auth codes
+ if err := sess.Join("INNER", "oauth2_grant",
+ "oauth2_authorization_code.grant_id = oauth2_grant.id AND oauth2_grant.application_id = ?", id).Find(&codes); err != nil {
+ return err
+ }
+ codeIDs := make([]int64, 0, len(codes))
+ for _, grant := range codes {
+ codeIDs = append(codeIDs, grant.ID)
+ }
+
+ if _, err := sess.In("id", codeIDs).Delete(new(OAuth2AuthorizationCode)); err != nil {
+ return err
+ }
+
+ if _, err := sess.Where("application_id = ?", id).Delete(new(OAuth2Grant)); err != nil {
+ return err
+ }
+ return nil
+}
+
+// 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.
+func DeleteOAuth2Application(ctx context.Context, id, userid int64) error {
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+ app, err := GetOAuth2ApplicationByID(ctx, id)
+ if err != nil {
+ return err
+ }
+ builtinApps := BuiltinApplications()
+ if _, builtin := builtinApps[app.ClientID]; builtin {
+ return fmt.Errorf("failed to delete OAuth2 application: application is locked: %s", app.ClientID)
+ }
+ if err := deleteOAuth2Application(ctx, id, userid); err != nil {
+ return err
+ }
+ return committer.Commit()
+}
+
+//////////////////////////////////////////////////////
+
+// OAuth2AuthorizationCode is a code to obtain an access token in combination with the client secret once. It has a limited lifetime.
+type OAuth2AuthorizationCode struct {
+ ID int64 `xorm:"pk autoincr"`
+ Grant *OAuth2Grant `xorm:"-"`
+ GrantID int64
+ Code string `xorm:"INDEX unique"`
+ CodeChallenge string
+ CodeChallengeMethod string
+ RedirectURI string
+ ValidUntil timeutil.TimeStamp `xorm:"index"`
+}
+
+// TableName sets the table name to `oauth2_authorization_code`
+func (code *OAuth2AuthorizationCode) TableName() string {
+ return "oauth2_authorization_code"
+}
+
+// GenerateRedirectURI generates a redirect URI for a successful authorization request. State will be used if not empty.
+func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL, error) {
+ redirect, err := url.Parse(code.RedirectURI)
+ if err != nil {
+ return nil, err
+ }
+ q := redirect.Query()
+ if state != "" {
+ q.Set("state", state)
+ }
+ q.Set("code", code.Code)
+ redirect.RawQuery = q.Encode()
+ return redirect, err
+}
+
+// Invalidate deletes the auth code from the database to invalidate this code
+func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error {
+ _, err := db.GetEngine(ctx).ID(code.ID).NoAutoCondition().Delete(code)
+ return err
+}
+
+// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation.
+func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool {
+ switch code.CodeChallengeMethod {
+ case "S256":
+ // base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6
+ h := sha256.Sum256([]byte(verifier))
+ hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:])
+ return hashedVerifier == code.CodeChallenge
+ case "plain":
+ return verifier == code.CodeChallenge
+ case "":
+ return true
+ default:
+ // unsupported method -> return false
+ return false
+ }
+}
+
+// GetOAuth2AuthorizationByCode returns an authorization by its code
+func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth2AuthorizationCode, err error) {
+ auth = new(OAuth2AuthorizationCode)
+ if has, err := db.GetEngine(ctx).Where("code = ?", code).Get(auth); err != nil {
+ return nil, err
+ } else if !has {
+ return nil, nil
+ }
+ auth.Grant = new(OAuth2Grant)
+ if has, err := db.GetEngine(ctx).ID(auth.GrantID).Get(auth.Grant); err != nil {
+ return nil, err
+ } else if !has {
+ return nil, nil
+ }
+ return auth, nil
+}
+
+//////////////////////////////////////////////////////
+
+// OAuth2Grant represents the permission of an user for a specific application to access resources
+type OAuth2Grant struct {
+ ID int64 `xorm:"pk autoincr"`
+ UserID int64 `xorm:"INDEX unique(user_application)"`
+ Application *OAuth2Application `xorm:"-"`
+ ApplicationID int64 `xorm:"INDEX unique(user_application)"`
+ Counter int64 `xorm:"NOT NULL DEFAULT 1"`
+ Scope string `xorm:"TEXT"`
+ Nonce string `xorm:"TEXT"`
+ CreatedUnix timeutil.TimeStamp `xorm:"created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
+}
+
+// TableName sets the table name to `oauth2_grant`
+func (grant *OAuth2Grant) TableName() string {
+ return "oauth2_grant"
+}
+
+// GenerateNewAuthorizationCode generates a new authorization code for a grant and saves it to the database
+func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redirectURI, codeChallenge, codeChallengeMethod string) (code *OAuth2AuthorizationCode, err error) {
+ rBytes, err := util.CryptoRandomBytes(32)
+ if err != nil {
+ return &OAuth2AuthorizationCode{}, err
+ }
+ // Add a prefix to the base32, this is in order to make it easier
+ // for code scanners to grab sensitive tokens.
+ codeSecret := "gta_" + base32Lower.EncodeToString(rBytes)
+
+ code = &OAuth2AuthorizationCode{
+ Grant: grant,
+ GrantID: grant.ID,
+ RedirectURI: redirectURI,
+ Code: codeSecret,
+ CodeChallenge: codeChallenge,
+ CodeChallengeMethod: codeChallengeMethod,
+ }
+ if err := db.Insert(ctx, code); err != nil {
+ return nil, err
+ }
+ return code, nil
+}
+
+// IncreaseCounter increases the counter and updates the grant
+func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error {
+ _, err := db.GetEngine(ctx).ID(grant.ID).Incr("counter").Update(new(OAuth2Grant))
+ if err != nil {
+ return err
+ }
+ updatedGrant, err := GetOAuth2GrantByID(ctx, grant.ID)
+ if err != nil {
+ return err
+ }
+ grant.Counter = updatedGrant.Counter
+ return nil
+}
+
+// ScopeContains returns true if the grant scope contains the specified scope
+func (grant *OAuth2Grant) ScopeContains(scope string) bool {
+ for _, currentScope := range strings.Split(grant.Scope, " ") {
+ if scope == currentScope {
+ return true
+ }
+ }
+ return false
+}
+
+// SetNonce updates the current nonce value of a grant
+func (grant *OAuth2Grant) SetNonce(ctx context.Context, nonce string) error {
+ grant.Nonce = nonce
+ _, err := db.GetEngine(ctx).ID(grant.ID).Cols("nonce").Update(grant)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// GetOAuth2GrantByID returns the grant with the given ID
+func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
+ grant = new(OAuth2Grant)
+ if has, err := db.GetEngine(ctx).ID(id).Get(grant); err != nil {
+ return nil, err
+ } else if !has {
+ return nil, nil
+ }
+ return grant, err
+}
+
+// GetOAuth2GrantsByUserID lists all grants of a certain user
+func GetOAuth2GrantsByUserID(ctx context.Context, uid int64) ([]*OAuth2Grant, error) {
+ type joinedOAuth2Grant struct {
+ Grant *OAuth2Grant `xorm:"extends"`
+ Application *OAuth2Application `xorm:"extends"`
+ }
+ var results *xorm.Rows
+ var err error
+ if results, err = db.GetEngine(ctx).
+ Table("oauth2_grant").
+ Where("user_id = ?", uid).
+ Join("INNER", "oauth2_application", "application_id = oauth2_application.id").
+ Rows(new(joinedOAuth2Grant)); err != nil {
+ return nil, err
+ }
+ defer results.Close()
+ grants := make([]*OAuth2Grant, 0)
+ for results.Next() {
+ joinedGrant := new(joinedOAuth2Grant)
+ if err := results.Scan(joinedGrant); err != nil {
+ return nil, err
+ }
+ joinedGrant.Grant.Application = joinedGrant.Application
+ grants = append(grants, joinedGrant.Grant)
+ }
+ return grants, nil
+}
+
+// RevokeOAuth2Grant deletes the grant with grantID and userID
+func RevokeOAuth2Grant(ctx context.Context, grantID, userID int64) error {
+ _, err := db.GetEngine(ctx).Where(builder.Eq{"id": grantID, "user_id": userID}).Delete(&OAuth2Grant{})
+ return err
+}
+
+// ErrOAuthClientIDInvalid will be thrown if client id cannot be found
+type ErrOAuthClientIDInvalid struct {
+ ClientID string
+}
+
+// IsErrOauthClientIDInvalid checks if an error is a ErrOAuthClientIDInvalid.
+func IsErrOauthClientIDInvalid(err error) bool {
+ _, ok := err.(ErrOAuthClientIDInvalid)
+ return ok
+}
+
+// Error returns the error message
+func (err ErrOAuthClientIDInvalid) Error() string {
+ return fmt.Sprintf("Client ID invalid [Client ID: %s]", err.ClientID)
+}
+
+// Unwrap unwraps this as a ErrNotExist err
+func (err ErrOAuthClientIDInvalid) Unwrap() error {
+ return util.ErrNotExist
+}
+
+// ErrOAuthApplicationNotFound will be thrown if id cannot be found
+type ErrOAuthApplicationNotFound struct {
+ ID int64
+}
+
+// IsErrOAuthApplicationNotFound checks if an error is a ErrReviewNotExist.
+func IsErrOAuthApplicationNotFound(err error) bool {
+ _, ok := err.(ErrOAuthApplicationNotFound)
+ return ok
+}
+
+// Error returns the error message
+func (err ErrOAuthApplicationNotFound) Error() string {
+ return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID)
+}
+
+// Unwrap unwraps this as a ErrNotExist err
+func (err ErrOAuthApplicationNotFound) Unwrap() error {
+ return util.ErrNotExist
+}
+
+// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name
+func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) {
+ authSource := new(Source)
+ has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
+ if err != nil {
+ return nil, err
+ }
+
+ if !has {
+ return nil, fmt.Errorf("oauth2 source not found, name: %q", name)
+ }
+
+ return authSource, nil
+}
+
+func DeleteOAuth2RelictsByUserID(ctx context.Context, userID int64) error {
+ deleteCond := builder.Select("id").From("oauth2_grant").Where(builder.Eq{"oauth2_grant.user_id": userID})
+
+ if _, err := db.GetEngine(ctx).In("grant_id", deleteCond).
+ Delete(&OAuth2AuthorizationCode{}); err != nil {
+ return err
+ }
+
+ if err := db.DeleteBeans(ctx,
+ &OAuth2Application{UID: userID},
+ &OAuth2Grant{UserID: userID},
+ ); err != nil {
+ return fmt.Errorf("DeleteBeans: %w", err)
+ }
+
+ return nil
+}
+
+// CountOrphanedOAuth2Applications returns the amount of orphaned OAuth2 applications.
+func CountOrphanedOAuth2Applications(ctx context.Context) (int64, error) {
+ return db.GetEngine(ctx).
+ Table("`oauth2_application`").
+ Join("LEFT", "`user`", "`oauth2_application`.`uid` = `user`.`id`").
+ Where(builder.IsNull{"`user`.id"}).
+ Where(builder.NotIn("`oauth2_application`.`client_id`", BuiltinApplicationsClientIDs())).
+ Select("COUNT(`oauth2_application`.`id`)").
+ Count()
+}
+
+// DeleteOrphanedOAuth2Applications deletes orphaned OAuth2 applications.
+func DeleteOrphanedOAuth2Applications(ctx context.Context) (int64, error) {
+ subQuery := builder.Select("`oauth2_application`.id").
+ From("`oauth2_application`").
+ Join("LEFT", "`user`", "`oauth2_application`.`uid` = `user`.`id`").
+ Where(builder.IsNull{"`user`.id"}).
+ Where(builder.NotIn("`oauth2_application`.`client_id`", BuiltinApplicationsClientIDs()))
+
+ b := builder.Delete(builder.In("id", subQuery)).From("`oauth2_application`")
+ _, err := db.GetEngine(ctx).Exec(b)
+ return -1, err
+}