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