Skip to content
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

OAuth2 auto-register #5123

Merged
merged 46 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a3366c4
Refactored handleOAuth2SignIn in routers/user/auth.go
mgjm Oct 19, 2018
6e2ece4
Refactored user creation
mgjm Oct 19, 2018
a473815
Added auto-register for OAuth2 users
mgjm Oct 19, 2018
a700b02
Moved oauth2 settings to new section in app.ini
mgjm Oct 23, 2018
f0124c4
Added oauth2 use nickname setting
mgjm Oct 23, 2018
9504a74
Merge branch 'master' into oauth2-auto-register
lafriks Oct 30, 2018
688afbd
Merge branch 'master' into oauth2-auto-register
mgjm Feb 24, 2020
aeb833c
Moved oauth2_client settings to service file
mgjm Feb 25, 2020
527c7e6
Updated comments on auth helpers
mgjm Feb 25, 2020
6a3a0e7
Renamed oauth2_client settings
mgjm Feb 25, 2020
7a84789
Uncommented setting in app.ini.sample
mgjm Apr 1, 2020
b57cb72
Merge branch 'master' into oauth2-auto-register
mgjm Apr 1, 2020
3f93e42
Merge branch 'master' into oauth2-auto-register
6543 Jan 26, 2021
6fe0f39
fix conflict resolve relict
6543 Jan 26, 2021
c39ed5a
Merge branch 'master' into oauth2-auto-register
6543 Jan 26, 2021
bcd3fb6
Merge branch 'master' into oauth2-auto-register
6543 Feb 4, 2021
9d1e1f2
Added error for missing fields in OAuth2 response
mgjm Feb 7, 2021
8a64dfe
Fixed error handling in createUserInContext
mgjm Feb 7, 2021
aece370
Merge branch 'master' into oauth2-auto-register
6543 Feb 7, 2021
1373b78
Merge branch 'master' into oauth2-auto-register
6543 Feb 7, 2021
8ee4097
Merge branch 'master' into oauth2-auto-register
6543 Feb 9, 2021
7b2ff29
Merge branch 'master' into oauth2-auto-register
6543 Feb 18, 2021
e166d49
Merge branch 'master' into oauth2-auto-register
6543 Feb 21, 2021
4cc2d8f
Merge branch 'master' into oauth2-auto-register
kvaster Mar 26, 2021
3edad74
Linking and auto linking on oauth2 registration
kvaster Mar 26, 2021
7a811af
Code cleanup
kvaster Mar 30, 2021
150163e
Fix lint problems
kvaster Mar 30, 2021
586dff7
Fix bugs in validating config options
kvaster Mar 30, 2021
fb464db
Convert oauth2 client types to string enums
kvaster Mar 30, 2021
d62cf15
Fix ioutil.ReadAll
mgjm Mar 30, 2021
f4924a3
Set default username source to nickname
mgjm Mar 31, 2021
28b696c
Merge branch 'master' into oauth2-auto-register
mgjm Mar 31, 2021
491610a
Add copyright and empty line
mgjm Apr 1, 2021
cd7241e
Move oauth2 client settings to new file
mgjm Apr 3, 2021
a3cf72b
Add automatic oauth2 scopes for github and google
mgjm Apr 3, 2021
3d99ee8
Add hint to change the openid connect scopes if fields are missing
mgjm Apr 3, 2021
917ed54
Merge branch 'master' into oauth2-auto-register
mgjm Apr 3, 2021
ebc0d0b
OAuth2 sign in is not handled properly after all merges
kvaster Apr 4, 2021
7795946
Merge branch 'master' into oauth2-auto-register
mgjm Apr 6, 2021
d448ed9
Merge branch 'master' into oauth2-auto-register
6543 Apr 8, 2021
1d4d7d3
Merge branch 'master' into oauth2-auto-register
6543 Apr 10, 2021
8f44610
Merge branch 'master' into oauth2-auto-register
6543 Apr 11, 2021
f320e34
More detailed description of options in cheat sheet
kvaster Apr 13, 2021
2987081
Correct info about auto linking security risk
kvaster Apr 13, 2021
7b96396
Extend info about auto linking security risk
mgjm Apr 14, 2021
4de0b71
Merge branch 'master' into oauth2-auto-register
6543 Apr 14, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ REGISTER_EMAIL_CONFIRM = false
DISABLE_REGISTRATION = false
; Allow registration only using third part services, it works only when DISABLE_REGISTRATION is false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
; Automatically create user accounts for new oauth2 users.
ENABLE_OAUTH2_AUTO_REGISTRATION = false
; Whether a new auto registered oauth2 user needs to confirm their email.
; Do not include to use the REGISTER_EMAIL_CONFIRM setting.
; OAUTH2_REGISTER_EMAIL_CONFIRM =
; User must sign in to view anything.
REQUIRE_SIGNIN_VIEW = false
; Mail notification
Expand Down
4 changes: 4 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
Requires `Mailer` to be enabled.
- `DISABLE_REGISTRATION`: **false**: Disable registration, after which only admin can create
accounts for users.
- `ENABLE_OAUTH2_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration
for oauth2 authentication.
- `OAUTH2_REGISTER_EMAIL_CONFIRM`: **REGISTER\_EMAIL\_CONFIRM**: Set this to enable or disable
mail confirmation of OAuth2 auto-registration.
- `REQUIRE_SIGNIN_VIEW`: **false**: Enable this to force users to log in to view any page.
- `ENABLE_NOTIFY_MAIL`: **false**: Enable this to send e-mail to watchers of a repository when
something happens, like creating issues. Requires `Mailer` to be enabled.
Expand Down
2 changes: 1 addition & 1 deletion modules/auth/oauth2/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo
case "gplus":
provider = gplus.New(clientID, clientSecret, callbackURL, "email")
case "openidConnect":
if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL); err != nil {
if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL, setting.Service.OAuth2OpenIDConnectScopes...); err != nil {
log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, openIDConnectAutoDiscoveryURL, err)
}
case "twitter":
Expand Down
16 changes: 16 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,9 @@ var Service struct {
DisableRegistration bool
AllowOnlyExternalRegistration bool
ShowRegistrationButton bool
EnableOAuth2AutoRegister bool
OAuth2RegisterEmailConfirm bool
OAuth2OpenIDConnectScopes []string
RequireSignInView bool
EnableNotifyMail bool
EnableReverseProxyAuth bool
Expand Down Expand Up @@ -1233,6 +1236,19 @@ func newService() {
Service.DisableRegistration = sec.Key("DISABLE_REGISTRATION").MustBool()
Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool()
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
Service.EnableOAuth2AutoRegister = sec.Key("ENABLE_OAUTH2_AUTO_REGISTRATION").MustBool()
Service.OAuth2RegisterEmailConfirm = sec.Key("OAUTH2_REGISTER_EMAIL_CONFIRM").MustBool(Service.RegisterEmailConfirm)
if !sec.HasKey("OAUTH2_OPENID_CONNECT_SCOPES") && Service.EnableOAuth2AutoRegister {
Service.OAuth2OpenIDConnectScopes = []string{"profile", "email"}
} else {
pats := sec.Key("OAUTH2_OPENID_CONNECT_SCOPES").Strings(" ")
Service.OAuth2OpenIDConnectScopes = make([]string, 0, len(pats))
for _, scope := range pats {
if scope != "" {
Service.OAuth2OpenIDConnectScopes = append(Service.OAuth2OpenIDConnectScopes, scope)
}
}
}
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
Expand Down
137 changes: 75 additions & 62 deletions routers/user/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,10 +508,10 @@ func SignInOAuth(ctx *context.Context) {
}

// try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp)
user, _, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp)
if err == nil && user != nil {
// we got the user without going through the whole OAuth2 authentication flow again
handleOAuth2SignIn(user, gothUser, ctx, err)
handleOAuth2SignIn(ctx, user)
return
}

Expand Down Expand Up @@ -540,25 +540,43 @@ func SignInOAuthCallback(ctx *context.Context) {

u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req.Request, ctx.Resp)

handleOAuth2SignIn(u, gothUser, ctx, err)
}

func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context, err error) {
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}

if u == nil {
// no existing user is found, request attach or new account
ctx.Session.Set("linkAccountGothUser", gothUser)
ctx.Redirect(setting.AppSubURL + "/user/link_account")
return
if setting.Service.EnableOAuth2AutoRegister {
// create new user with details from oauth2 provider
u = &models.User{
Name: gothUser.UserID,
FullName: gothUser.Name,
Email: gothUser.Email,
IsActive: !setting.Service.OAuth2RegisterEmailConfirm,
LoginType: models.LoginOAuth2,
LoginSource: loginSource.ID,
LoginName: gothUser.UserID,
mgjm marked this conversation as resolved.
Show resolved Hide resolved
6543 marked this conversation as resolved.
Show resolved Hide resolved
}

if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u) {
// error already handled
return
}
} else {
// no existing user is found, request attach or new account
ctx.Session.Set("linkAccountGothUser", gothUser)
ctx.Redirect(setting.AppSubURL + "/user/link_account")
return
}
}

handleOAuth2SignIn(ctx, u)
}

func handleOAuth2SignIn(ctx *context.Context, u *models.User) {
// If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page.
_, err = models.GetTwoFactorByUID(u.ID)
_, err := models.GetTwoFactorByUID(u.ID)
if err != nil {
if models.IsErrTwoFactorNotEnrolled(err) {
ctx.Session.Set("uid", u.ID)
Expand Down Expand Up @@ -810,49 +828,8 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au
LoginName: gothUser.(goth.User).UserID,
}

if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplLinkAccount, &form)
case models.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplLinkAccount, &form)
case models.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplLinkAccount, &form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplLinkAccount, &form)
default:
ctx.ServerError("CreateUser", err)
}
return
}
log.Trace("Account created: %s", u.Name)

// Auto-set admin for the only user.
if models.CountUsers() == 1 {
u.IsAdmin = true
u.IsActive = true
u.SetLastLogin()
if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
}

// Send confirmation email
if setting.Service.RegisterEmailConfirm && u.ID > 1 {
models.SendActivateAccountMail(ctx.Context, u)
ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email
ctx.Data["ActiveCodeLives"] = base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
ctx.HTML(200, TplActivate)

if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
}
if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u) {
// error already handled
return
}

Expand Down Expand Up @@ -943,27 +920,64 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo
Passwd: form.Password,
IsActive: !setting.Service.RegisterEmailConfirm,
}

if !createAndHandleCreatedUser(ctx, tplSignUp, form, u) {
// error already handled
return
}

ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
handleSignInFull(ctx, u, false, true)
}

// CreateAndHandleCreatedUser calls createUserInContext and
// then handleUserCreated.
func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User) (ok bool) {
ok = createUserInContext(ctx, tpl, form, u)
if !ok {
return
}
ok = handleUserCreated(ctx, u)
return
}

// CreateUserInContext creates a user and handles errors within a given context.
// Optionaly a template can be specified.
func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User) (ok bool) {
if err := models.CreateUser(u); err != nil {
// handle error without template
if len(tpl) == 0 {
ctx.ServerError("CreateUser", err)
return
}

// handle error with template
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSignUp, &form)
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, &form)
case models.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignUp, &form)
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, &form)
case models.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplSignUp, &form)
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, &form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSignUp, &form)
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, &form)
default:
ctx.ServerError("CreateUser", err)
}
return
}
log.Trace("Account created: %s", u.Name)
return true
}

// HandleUserCreated does additional steps after a new user is created.
mgjm marked this conversation as resolved.
Show resolved Hide resolved
// It auto-sets admin for the only user and
// sends a confirmation email if required.
func handleUserCreated(ctx *context.Context, u *models.User) (ok bool) {
// Auto-set admin for the only user.
if models.CountUsers() == 1 {
u.IsAdmin = true
Expand All @@ -975,8 +989,8 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo
}
}

// Send confirmation email, no need for social account.
if setting.Service.RegisterEmailConfirm && u.ID > 1 {
// Send confirmation email
if !u.IsActive && u.ID > 1 {
models.SendActivateAccountMail(ctx.Context, u)
ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email
Expand All @@ -989,8 +1003,7 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo
return
}

ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
handleSignInFull(ctx, u, false, true)
return true
}

// Activate render activate user page
Expand Down
45 changes: 4 additions & 41 deletions routers/user/auth_openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,33 +366,16 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si
return
}

// TODO: abstract a finalizeSignUp function ?
u := &models.User{
Name: form.UserName,
Email: form.Email,
Passwd: password,
IsActive: !setting.Service.RegisterEmailConfirm,
}
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSignUpOID, &form)
case models.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignUpOID, &form)
case models.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplSignUpOID, &form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSignUpOID, &form)
default:
ctx.ServerError("CreateUser", err)
}
if !createUserInContext(ctx, tplSignUpOID, form, u) {
// error already handled
return
}
log.Trace("Account created: %s", u.Name)

// add OpenID for the user
userOID := &models.UserOpenID{UID: u.ID, URI: oid}
Expand All @@ -405,28 +388,8 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si
return
}

// Auto-set admin for the only user.
if models.CountUsers() == 1 {
u.IsAdmin = true
u.IsActive = true
u.SetLastLogin()
if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
}

// Send confirmation email, no need for social account.
if setting.Service.RegisterEmailConfirm && u.ID > 1 {
models.SendActivateAccountMail(ctx.Context, u)
ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email
ctx.Data["ActiveCodeLives"] = base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
ctx.HTML(200, TplActivate)

if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
}
if !handleUserCreated(ctx, u) {
// error already handled
return
}

Expand Down