Skip to content

Commit 55eb174

Browse files
mgjmkvaster
andauthored
OAuth2 auto-register (#5123)
* Refactored handleOAuth2SignIn in routers/user/auth.go The function handleOAuth2SignIn was called twice but some code path could only be reached by one of the invocations. Moved the unnecessary code path out of handleOAuth2SignIn. * Refactored user creation There was common code to create a user and display the correct error message. And after the creation the only user should be an admin and if enabled a confirmation email should be sent. This common code is now abstracted into two functions and a helper function to call both. * Added auto-register for OAuth2 users If enabled new OAuth2 users will be registered with their OAuth2 details. The UserID, Name and Email fields from the gothUser are used. Therefore the OpenID Connect provider needs additional scopes to return the coresponding claims. * Added error for missing fields in OAuth2 response * Linking and auto linking on oauth2 registration * Set default username source to nickname * Add automatic oauth2 scopes for github and google * Add hint to change the openid connect scopes if fields are missing * Extend info about auto linking security risk Co-authored-by: Viktor Kuzmin <kvaster@gmail.com> Signed-off-by: Martin Michaelis <code@mgjm.de>
1 parent ca2e1d8 commit 55eb174

File tree

9 files changed

+351
-135
lines changed

9 files changed

+351
-135
lines changed

custom/conf/app.example.ini

+24
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,30 @@ WHITELISTED_URIS =
617617
; Example value: loadaverage.org/badguy stackexchange.com/.*spammer
618618
BLACKLISTED_URIS =
619619

620+
[oauth2_client]
621+
; Whether a new auto registered oauth2 user needs to confirm their email.
622+
; Do not include to use the REGISTER_EMAIL_CONFIRM setting from the `[service]` section.
623+
REGISTER_EMAIL_CONFIRM =
624+
; Scopes for the openid connect oauth2 provider (seperated by space, the openid scope is implicitly added).
625+
; Typical values are profile and email.
626+
; For more information about the possible values see https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
627+
OPENID_CONNECT_SCOPES =
628+
; Automatically create user accounts for new oauth2 users.
629+
ENABLE_AUTO_REGISTRATION = false
630+
; The source of the username for new oauth2 accounts:
631+
; userid = use the userid / sub attribute
632+
; nickname = use the nickname attribute
633+
; email = use the username part of the email attribute
634+
USERNAME = nickname
635+
; Update avatar if available from oauth2 provider.
636+
; Update will be performed on each login.
637+
UPDATE_AVATAR = false
638+
; How to handle if an account / email already exists:
639+
; disabled = show an error
640+
; login = show an account linking login
641+
; auto = link directly with the account
642+
ACCOUNT_LINKING = disabled
643+
620644
[service]
621645
; Time limit to confirm account/email registration
622646
ACTIVE_CODE_LIVE_MINUTES = 180

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+15
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,21 @@ relation to port exhaustion.
429429
- `BLACKLISTED_URIS`: **\<empty\>**: If non-empty, list of POSIX regex patterns matching
430430
OpenID URI's to block.
431431

432+
## OAuth2 Client (`oauth2_client`)
433+
434+
- `REGISTER_EMAIL_CONFIRM`: *[service]* **REGISTER\_EMAIL\_CONFIRM**: Set this to enable or disable email confirmation of OAuth2 auto-registration. (Overwrites the REGISTER\_EMAIL\_CONFIRM setting of the `[service]` section)
435+
- `OPENID_CONNECT_SCOPES`: **\<empty\>**: List of additional openid connect scopes. (`openid` is implicitly added)
436+
- `ENABLE_AUTO_REGISTRATION`: **false**: Automatically create user accounts for new oauth2 users.
437+
- `USERNAME`: **nickname**: The source of the username for new oauth2 accounts:
438+
- userid - use the userid / sub attribute
439+
- nickname - use the nickname attribute
440+
- email - use the username part of the email attribute
441+
- `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login.
442+
- `ACCOUNT_LINKING`: **disabled**: How to handle if an account / email already exists:
443+
- disabled - show an error
444+
- login - show an account linking login
445+
- auto - automatically link with the account (Please be aware that this will grant access to an existing account just because the same username or email is provided. You must make sure that this does not cause issues with your authentication providers.)
446+
432447
## Service (`service`)
433448

434449
- `ACTIVE_CODE_LIVE_MINUTES`: **180**: Time limit (min) to confirm account/email registration.

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ var migrations = []Migration{
307307

308308
// v178 -> v179
309309
NewMigration("Add LFS columns to Mirror", addLFSMirrorColumns),
310+
// v179 -> v180
311+
NewMigration("Convert avatar url to text", convertAvatarURLToText),
310312
}
311313

312314
// GetCurrentDBVersion returns the current db version

models/migrations/v179.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"xorm.io/xorm"
9+
"xorm.io/xorm/schemas"
10+
)
11+
12+
func convertAvatarURLToText(x *xorm.Engine) error {
13+
dbType := x.Dialect().URI().DBType
14+
if dbType == schemas.SQLITE { // For SQLITE, varchar or char will always be represented as TEXT
15+
return nil
16+
}
17+
18+
// Some oauth2 providers may give very long avatar urls (i.e. Google)
19+
return modifyColumn(x, "external_login_user", &schemas.Column{
20+
Name: "avatar_url",
21+
SQLType: schemas.SQLType{
22+
Name: schemas.Text,
23+
},
24+
Nullable: true,
25+
})
26+
}

modules/auth/oauth2/oauth2.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo
157157
emailURL = customURLMapping.EmailURL
158158
}
159159
}
160-
provider = github.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, emailURL)
160+
scopes := []string{}
161+
if setting.OAuth2Client.EnableAutoRegistration {
162+
scopes = append(scopes, "user:email")
163+
}
164+
provider = github.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, emailURL, scopes...)
161165
case "gitlab":
162166
authURL := gitlab.AuthURL
163167
tokenURL := gitlab.TokenURL
@@ -175,9 +179,13 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo
175179
}
176180
provider = gitlab.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, "read_user")
177181
case "gplus": // named gplus due to legacy gplus -> google migration (Google killed Google+). This ensures old connections still work
178-
provider = google.New(clientID, clientSecret, callbackURL)
182+
scopes := []string{"email"}
183+
if setting.OAuth2Client.UpdateAvatar || setting.OAuth2Client.EnableAutoRegistration {
184+
scopes = append(scopes, "profile")
185+
}
186+
provider = google.New(clientID, clientSecret, callbackURL, scopes...)
179187
case "openidConnect":
180-
if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL); err != nil {
188+
if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL, setting.OAuth2Client.OpenIDConnectScopes...); err != nil {
181189
log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, openIDConnectAutoDiscoveryURL, err)
182190
}
183191
case "twitter":

modules/setting/oauth2_client.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package setting
6+
7+
import (
8+
"code.gitea.io/gitea/modules/log"
9+
10+
"gopkg.in/ini.v1"
11+
)
12+
13+
// OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data
14+
type OAuth2UsernameType string
15+
16+
const (
17+
// OAuth2UsernameUserid oauth2 userid field will be used as gitea name
18+
OAuth2UsernameUserid OAuth2UsernameType = "userid"
19+
// OAuth2UsernameNickname oauth2 nickname field will be used as gitea name
20+
OAuth2UsernameNickname OAuth2UsernameType = "nickname"
21+
// OAuth2UsernameEmail username of oauth2 email filed will be used as gitea name
22+
OAuth2UsernameEmail OAuth2UsernameType = "email"
23+
)
24+
25+
func (username OAuth2UsernameType) isValid() bool {
26+
switch username {
27+
case OAuth2UsernameUserid, OAuth2UsernameNickname, OAuth2UsernameEmail:
28+
return true
29+
}
30+
return false
31+
}
32+
33+
// OAuth2AccountLinkingType is enum describing behaviour of linking with existing account
34+
type OAuth2AccountLinkingType string
35+
36+
const (
37+
// OAuth2AccountLinkingDisabled error will be displayed if account exist
38+
OAuth2AccountLinkingDisabled OAuth2AccountLinkingType = "disabled"
39+
// OAuth2AccountLinkingLogin account linking login will be displayed if account exist
40+
OAuth2AccountLinkingLogin OAuth2AccountLinkingType = "login"
41+
// OAuth2AccountLinkingAuto account will be automatically linked if account exist
42+
OAuth2AccountLinkingAuto OAuth2AccountLinkingType = "auto"
43+
)
44+
45+
func (accountLinking OAuth2AccountLinkingType) isValid() bool {
46+
switch accountLinking {
47+
case OAuth2AccountLinkingDisabled, OAuth2AccountLinkingLogin, OAuth2AccountLinkingAuto:
48+
return true
49+
}
50+
return false
51+
}
52+
53+
// OAuth2Client settings
54+
var OAuth2Client struct {
55+
RegisterEmailConfirm bool
56+
OpenIDConnectScopes []string
57+
EnableAutoRegistration bool
58+
Username OAuth2UsernameType
59+
UpdateAvatar bool
60+
AccountLinking OAuth2AccountLinkingType
61+
}
62+
63+
func newOAuth2Client() {
64+
sec := Cfg.Section("oauth2_client")
65+
OAuth2Client.RegisterEmailConfirm = sec.Key("REGISTER_EMAIL_CONFIRM").MustBool(Service.RegisterEmailConfirm)
66+
OAuth2Client.OpenIDConnectScopes = parseScopes(sec, "OPENID_CONNECT_SCOPES")
67+
OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool()
68+
OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname)))
69+
if !OAuth2Client.Username.isValid() {
70+
log.Warn("Username setting is not valid: '%s', will fallback to '%s'", OAuth2Client.Username, OAuth2UsernameNickname)
71+
OAuth2Client.Username = OAuth2UsernameNickname
72+
}
73+
OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool()
74+
OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingDisabled)))
75+
if !OAuth2Client.AccountLinking.isValid() {
76+
log.Warn("Account linking setting is not valid: '%s', will fallback to '%s'", OAuth2Client.AccountLinking, OAuth2AccountLinkingDisabled)
77+
OAuth2Client.AccountLinking = OAuth2AccountLinkingDisabled
78+
}
79+
}
80+
81+
func parseScopes(sec *ini.Section, name string) []string {
82+
parts := sec.Key(name).Strings(" ")
83+
scopes := make([]string, 0, len(parts))
84+
for _, scope := range parts {
85+
if scope != "" {
86+
scopes = append(scopes, scope)
87+
}
88+
}
89+
return scopes
90+
}

modules/setting/setting.go

+1
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,7 @@ func MakeManifestData(appName string, appURL string, absoluteAssetURL string) []
11631163
func NewServices() {
11641164
InitDBConfig()
11651165
newService()
1166+
newOAuth2Client()
11661167
NewLogServices(false)
11671168
newCacheService()
11681169
newSessionService()

0 commit comments

Comments
 (0)