-
-
Notifications
You must be signed in to change notification settings - Fork 6k
allow synchronizing user status from OAuth2 login providers #31572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
b8411f4
allow synchronizing user status from OAuth2 login providers
bohde 63add1d
fix lint
bohde 0d65525
Merge branch 'main' into rb/synchronize-oauth2-users
kdumontnu 0327ff2
remove extra Limit & Start
bohde 6e91032
Merge remote-tracking branch 'upstream/main' into rb/synchronize-oaut…
bohde 2b71ec7
use passed in context
bohde 302e9c8
Merge branch 'main' into rb/synchronize-oauth2-users
kdumontnu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next
Next commit
allow synchronizing user status from OAuth2 login providers
This leverages the existing `sync_external_users` cron job to synchronize the IsActive flag on users who use an OAuth2 provider set to synchronize. This synchronization is done by checking for expired access tokens, and using the stored refresh token to request a new access token. If the response back from the OAuth2 provider is the `invalid_grant` error code, the user is marked as inactive. However, the user is able to reactivate their account by logging in the web browser through their OAuth flow. Also changed to support this is that a linked `ExternalLoginUser` is always created upon a login or signup via OAuth2.
- Loading branch information
commit b8411f448ef1427e44b0de4ed7f79b2b9c80c4fb
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Copyright 2024 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package oauth2 | ||
|
||
import ( | ||
"testing" | ||
|
||
"code.gitea.io/gitea/models/unittest" | ||
) | ||
|
||
func TestMain(m *testing.M) { | ||
unittest.MainTest(m, &unittest.TestOptions{}) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// Copyright 2024 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package oauth2 | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/markbates/goth" | ||
"golang.org/x/oauth2" | ||
) | ||
|
||
type fakeProvider struct{} | ||
|
||
func (p *fakeProvider) Name() string { | ||
return "fake" | ||
} | ||
|
||
func (p *fakeProvider) SetName(name string) {} | ||
|
||
func (p *fakeProvider) BeginAuth(state string) (goth.Session, error) { | ||
return nil, nil | ||
} | ||
|
||
func (p *fakeProvider) UnmarshalSession(string) (goth.Session, error) { | ||
return nil, nil | ||
} | ||
|
||
func (p *fakeProvider) FetchUser(goth.Session) (goth.User, error) { | ||
return goth.User{}, nil | ||
} | ||
|
||
func (p *fakeProvider) Debug(bool) { | ||
} | ||
|
||
func (p *fakeProvider) RefreshToken(refreshToken string) (*oauth2.Token, error) { | ||
switch refreshToken { | ||
case "expired": | ||
return nil, &oauth2.RetrieveError{ | ||
ErrorCode: "invalid_grant", | ||
} | ||
default: | ||
return &oauth2.Token{ | ||
AccessToken: "token", | ||
TokenType: "Bearer", | ||
RefreshToken: "refresh", | ||
Expiry: time.Now().Add(time.Hour), | ||
}, nil | ||
} | ||
} | ||
|
||
func (p *fakeProvider) RefreshTokenAvailable() bool { | ||
return true | ||
} | ||
|
||
func init() { | ||
RegisterGothProvider( | ||
NewSimpleProvider("fake", "Fake", []string{"account"}, | ||
func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { | ||
return &fakeProvider{} | ||
})) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
// Copyright 2024 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package oauth2 | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"code.gitea.io/gitea/models/auth" | ||
"code.gitea.io/gitea/models/db" | ||
user_model "code.gitea.io/gitea/models/user" | ||
"code.gitea.io/gitea/modules/log" | ||
|
||
"github.com/markbates/goth" | ||
"golang.org/x/oauth2" | ||
) | ||
|
||
// Sync causes this OAuth2 source to synchronize its users with the db. | ||
func (source *Source) Sync(ctx context.Context, updateExisting bool) error { | ||
log.Trace("Doing: SyncExternalUsers[%s] %d", source.authSource.Name, source.authSource.ID) | ||
|
||
if !updateExisting { | ||
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.authSource.Name) | ||
return nil | ||
} | ||
|
||
provider, err := createProvider(source.authSource.Name, source) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if !provider.RefreshTokenAvailable() { | ||
log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.authSource.Name) | ||
return nil | ||
} | ||
|
||
opts := user_model.FindExternalUserOptions{ | ||
HasRefreshToken: true, | ||
Expired: true, | ||
LoginSourceID: source.authSource.ID, | ||
} | ||
|
||
return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error { | ||
return source.refresh(ctx, provider, u) | ||
}) | ||
} | ||
|
||
func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *user_model.ExternalLoginUser) error { | ||
log.Trace("Syncing login_source_id=%d external_id=%s expiration=%s", u.LoginSourceID, u.ExternalID, u.ExpiresAt) | ||
|
||
shouldDisable := false | ||
|
||
token, err := provider.RefreshToken(u.RefreshToken) | ||
if err != nil { | ||
if err, ok := err.(*oauth2.RetrieveError); ok && err.ErrorCode == "invalid_grant" { | ||
// this signals that the token is not valid and the user should be disabled | ||
shouldDisable = true | ||
} else { | ||
return err | ||
} | ||
} | ||
|
||
user := &user_model.User{ | ||
LoginName: u.ExternalID, | ||
LoginType: auth.OAuth2, | ||
LoginSource: u.LoginSourceID, | ||
} | ||
|
||
hasUser, err := user_model.GetUser(ctx, user) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// If the grant is no longer valid, disable the user and | ||
// delete local tokens. If the OAuth2 provider still | ||
// recognizes them as a valid user, they will be able to login | ||
// via their provider and reactivate their account. | ||
if shouldDisable { | ||
log.Info("SyncExternalUsers[%s] disabling user %d", source.authSource.Name, user.ID) | ||
|
||
return db.WithTx(ctx, func(ctx context.Context) error { | ||
if hasUser { | ||
user.IsActive = false | ||
err := user_model.UpdateUserCols(ctx, user, "is_active") | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
// Delete stored tokens, since they are invalid. This | ||
// also provents us from checking this in subsequent runs. | ||
u.AccessToken = "" | ||
u.RefreshToken = "" | ||
u.ExpiresAt = time.Time{} | ||
|
||
return user_model.UpdateExternalUserByExternalID(ctx, u) | ||
}) | ||
} | ||
|
||
// Otherwise, update the tokens | ||
u.AccessToken = token.AccessToken | ||
u.ExpiresAt = token.Expiry | ||
|
||
// Some providers only update access tokens provide a new | ||
// refresh token, so avoid updating it if it's empty | ||
if token.RefreshToken != "" { | ||
u.RefreshToken = token.RefreshToken | ||
} | ||
|
||
err = user_model.UpdateExternalUserByExternalID(ctx, u) | ||
|
||
return err | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.