From 9ca0e7905c24f18ed246e65397589f0f41b50506 Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Fri, 10 Sep 2021 17:37:57 +0100
Subject: [PATCH] Add setting to OAuth handlers to skip local 2FA
 authentication (#16594)

This PR adds a setting to OAuth and OpenID login sources to allow the source to
skip local 2FA authentication.

Fix #13939

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 cmd/admin.go                           |  5 +++++
 options/locale/locale_en-US.ini        |  2 ++
 routers/web/admin/auths.go             |  1 +
 routers/web/user/auth.go               | 20 ++++++++++++--------
 services/auth/source/oauth2/source.go  |  1 +
 services/forms/auth_form.go            |  1 +
 templates/admin/auth/edit.tmpl         |  7 +++++++
 templates/admin/auth/source/oauth.tmpl |  7 +++++++
 8 files changed, 36 insertions(+), 8 deletions(-)

diff --git a/cmd/admin.go b/cmd/admin.go
index 94e78186c9..cfc297c474 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -288,6 +288,10 @@ var (
 			Value: "",
 			Usage: "Custom icon URL for OAuth2 login source",
 		},
+		cli.BoolFlag{
+			Name:  "skip-local-2fa",
+			Usage: "Set to true to skip local 2fa for users authenticated by this source",
+		},
 	}
 
 	microcmdAuthUpdateOauth = cli.Command{
@@ -616,6 +620,7 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
 		OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
 		CustomURLMapping:              customURLMapping,
 		IconURL:                       c.String("icon-url"),
+		SkipLocalTwoFA:                c.Bool("skip-local-2fa"),
 	}
 }
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 17701094d7..9ebd2e7a5f 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2456,6 +2456,8 @@ auths.oauth2_tokenURL = Token URL
 auths.oauth2_authURL = Authorize URL
 auths.oauth2_profileURL = Profile URL
 auths.oauth2_emailURL = Email URL
+auths.skip_local_two_fa = Skip local 2FA
+auths.skip_local_two_fa_helper = Leaving unset means local users with 2FA set will still have to pass 2FA to log on
 auths.oauth2_tenant = Tenant
 auths.enable_auto_register = Enable Auto Registration
 auths.sspi_auto_create_users = Automatically create users
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 342318e04e..b2879d7c4f 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -181,6 +181,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
 		OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
 		CustomURLMapping:              customURLMapping,
 		IconURL:                       form.Oauth2IconURL,
+		SkipLocalTwoFA:                form.SkipLocalTwoFA,
 	}
 }
 
diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
index 313a583004..38e0d989b8 100644
--- a/routers/web/user/auth.go
+++ b/routers/web/user/auth.go
@@ -574,7 +574,7 @@ func SignInOAuth(ctx *context.Context) {
 	user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
 	if err == nil && user != nil {
 		// we got the user without going through the whole OAuth2 authentication flow again
-		handleOAuth2SignIn(ctx, user, gothUser)
+		handleOAuth2SignIn(ctx, loginSource, user, gothUser)
 		return
 	}
 
@@ -660,7 +660,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 		}
 	}
 
-	handleOAuth2SignIn(ctx, u, gothUser)
+	handleOAuth2SignIn(ctx, loginSource, u, gothUser)
 }
 
 func getUserName(gothUser *goth.User) string {
@@ -702,18 +702,22 @@ func updateAvatarIfNeed(url string, u *models.User) {
 	}
 }
 
-func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User) {
+func handleOAuth2SignIn(ctx *context.Context, source *models.LoginSource, u *models.User, gothUser goth.User) {
 	updateAvatarIfNeed(gothUser.AvatarURL, u)
 
-	// 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)
-	if err != nil {
-		if !models.IsErrTwoFactorNotEnrolled(err) {
+	needs2FA := false
+	if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA {
+		_, err := models.GetTwoFactorByUID(u.ID)
+		if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
 			ctx.ServerError("UserSignIn", err)
 			return
 		}
+		needs2FA = err == nil
+	}
 
+	// If this user is enrolled in 2FA and this source doesn't override it,
+	// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
+	if !needs2FA {
 		if err := ctx.Session.Set("uid", u.ID); err != nil {
 			log.Error("Error setting uid in session: %v", err)
 		}
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 40d8973b4b..7b22383d7e 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -24,6 +24,7 @@ type Source struct {
 	OpenIDConnectAutoDiscoveryURL string
 	CustomURLMapping              *CustomURLMapping
 	IconURL                       string
+	SkipLocalTwoFA                bool
 
 	// reference to the loginSource
 	loginSource *models.LoginSource
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index b45ea6ea12..229728cf7d 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -66,6 +66,7 @@ type AuthenticationForm struct {
 	Oauth2EmailURL                string
 	Oauth2IconURL                 string
 	Oauth2Tenant                  string
+	SkipLocalTwoFA                bool
 	SSPIAutoCreateUsers           bool
 	SSPIAutoActivateUsers         bool
 	SSPIStripDomainNames          bool
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 109186a178..3e21710353 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -255,6 +255,13 @@
 						<label for="open_id_connect_auto_discovery_url">{{.i18n.Tr "admin.auths.openIdConnectAutoDiscoveryURL"}}</label>
 						<input id="open_id_connect_auto_discovery_url" name="open_id_connect_auto_discovery_url" value="{{$cfg.OpenIDConnectAutoDiscoveryURL}}">
 					</div>
+					<div class="optional field">
+						<div class="ui checkbox">
+							<label for="skip_local_two_fa"><strong>{{.i18n.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
+							<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
+							<p class="help">{{.i18n.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
+						</div>
+					</div>
 
 					<div class="oauth2_use_custom_url inline field">
 						<div class="ui checkbox">
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl
index b19fe3d428..6e91da14e2 100644
--- a/templates/admin/auth/source/oauth.tmpl
+++ b/templates/admin/auth/source/oauth.tmpl
@@ -28,6 +28,13 @@
 		<label for="open_id_connect_auto_discovery_url">{{.i18n.Tr "admin.auths.openIdConnectAutoDiscoveryURL"}}</label>
 		<input id="open_id_connect_auto_discovery_url" name="open_id_connect_auto_discovery_url" value="{{.open_id_connect_auto_discovery_url}}">
 	</div>
+	<div class="optional field">
+		<div class="ui checkbox">
+			<label for="skip_local_two_fa"><strong>{{.i18n.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
+			<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if .skip_local_two_fa}}checked{{end}}>
+			<p class="help">{{.i18n.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
+		</div>
+	</div>
 
 	<div class="oauth2_use_custom_url inline field">
 		<div class="ui checkbox">