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(®isteredApps); 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 }