Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 40 additions & 4 deletions routers/web/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,18 +218,50 @@ func performAutoLogin(ctx *context.Context) bool {
return false
}

func prepareSignInPageData(ctx *context.Context) {
func performAutoLoginOAuth2(ctx *context.Context, data *preparedSignInData) bool {
// If only 1 OAuth provider is present and other login methods are disabled, redirect to the OAuth provider.
onlySingleOAuth2 := len(data.oauth2Providers) == 1 &&
!setting.Service.EnablePasswordSignInForm &&
!setting.Service.EnableOpenIDSignIn &&
!setting.Service.EnablePasskeyAuth &&
!data.enableSSPI

if !onlySingleOAuth2 {
return false
}

skipToOAuthURL := setting.AppSubURL + "/user/oauth2/" + url.QueryEscape(data.oauth2Providers[0].DisplayName())
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
skipToOAuthURL += "?redirect_to=" + url.QueryEscape(redirectTo)
}
ctx.Redirect(skipToOAuthURL)
return true
}

type preparedSignInData struct {
oauth2Providers []oauth2.Provider
enableSSPI bool
}

func prepareSignInPageData(ctx *context.Context) (ret preparedSignInData) {
var err error
ret.enableSSPI = auth.IsSSPIEnabled(ctx)
ret.oauth2Providers, err = oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
log.Error("Failed to get OAuth2 providers: %v", err)
}
ctx.Data["Title"] = ctx.Tr("sign_in")
ctx.Data["OAuth2Providers"], _ = oauth2.GetOAuth2Providers(ctx, optional.Some(true))
ctx.Data["OAuth2Providers"] = ret.oauth2Providers
ctx.Data["Title"] = ctx.Tr("sign_in")
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
ctx.Data["EnableSSPI"] = ret.enableSSPI

prepareCommonAuthPageData(ctx, CommonAuthOptions{
EnableCaptcha: setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin,
})
return ret
}

// SignIn render sign in page
Expand All @@ -241,7 +273,10 @@ func SignIn(ctx *context.Context) {
redirectAfterAuth(ctx)
return
}
prepareSignInPageData(ctx)
data := prepareSignInPageData(ctx)
if performAutoLoginOAuth2(ctx, &data) {
return
}
ctx.HTML(http.StatusOK, tplSignIn)
}

Expand Down Expand Up @@ -471,6 +506,7 @@ func prepareSignUpPageData(ctx *context.Context) bool {
ctx.Data["Title"] = ctx.Tr("sign_up")
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
ctx.Data["PageIsSignUp"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)

hasUsers, err := user_model.HasUsers(ctx)
if err != nil {
Expand Down
31 changes: 31 additions & 0 deletions routers/web/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,37 @@ func TestWebAuthOAuth2(t *testing.T) {
assert.Contains(t, ctx.Flash.ErrorMsg, "auth.oauth.signin.error.general")
})

t.Run("RedirectSingleProvider", func(t *testing.T) {
enablePassword := &setting.Service.EnablePasswordSignInForm
enableOpenID := &setting.Service.EnableOpenIDSignIn
enablePasskey := &setting.Service.EnablePasskeyAuth
defer test.MockVariableValue(enablePassword, false)()
defer test.MockVariableValue(enableOpenID, false)()
defer test.MockVariableValue(enablePasskey, false)()

testSignIn := func(t *testing.T, link string, expectedCode int, expectedRedirect string) {
ctx, resp := contexttest.MockContext(t, link)
SignIn(ctx)
assert.Equal(t, expectedCode, resp.Code)
if expectedCode == http.StatusSeeOther {
assert.Equal(t, expectedRedirect, test.RedirectURL(resp))
}
}
testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy-auth-source")
testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy-auth-source?redirect_to=%2F")

*enablePassword, *enableOpenID, *enablePasskey = true, false, false
testSignIn(t, "/user/login", http.StatusOK, "")
*enablePassword, *enableOpenID, *enablePasskey = false, true, false
testSignIn(t, "/user/login", http.StatusOK, "")
*enablePassword, *enableOpenID, *enablePasskey = false, false, true
testSignIn(t, "/user/login", http.StatusOK, "")

*enablePassword, *enableOpenID, *enablePasskey = false, false, false
addOAuth2Source(t, "dummy-auth-source-2", oauth2.Source{})
testSignIn(t, "/user/login", http.StatusOK, "")
})

t.Run("OIDCLogout", func(t *testing.T) {
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
2 changes: 1 addition & 1 deletion services/auth/source/oauth2/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (p *AuthSourceProvider) DisplayName() string {

func (p *AuthSourceProvider) IconHTML(size int) template.HTML {
if p.iconURL != "" {
img := fmt.Sprintf(`<img class="tw-object-contain tw-mr-2" width="%d" height="%d" src="%s" alt="%s">`,
img := fmt.Sprintf(`<img class="tw-object-contain" width="%d" height="%d" src="%s" alt="%s">`,
size,
size,
html.EscapeString(p.iconURL), html.EscapeString(p.DisplayName()),
Expand Down
4 changes: 2 additions & 2 deletions services/auth/source/oauth2/providers_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ func (b *BaseProvider) IconHTML(size int) template.HTML {
case "github":
svgName = "octicon-mark-github"
}
svgHTML := svg.RenderHTML(svgName, size, "tw-mr-2")
svgHTML := svg.RenderHTML(svgName, size)
if svgHTML == "" {
log.Error("No SVG icon for oauth2 provider %q", b.name)
svgHTML = svg.RenderHTML("gitea-openid", size, "tw-mr-2")
svgHTML = svg.RenderHTML("gitea-openid", size)
}
return svgHTML
}
Expand Down
2 changes: 1 addition & 1 deletion services/auth/source/oauth2/providers_openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (o *OpenIDProvider) DisplayName() string {

// IconHTML returns icon HTML for this provider
func (o *OpenIDProvider) IconHTML(size int) template.HTML {
return svg.RenderHTML("gitea-openid", size, "tw-mr-2")
return svg.RenderHTML("gitea-openid", size)
}

// CreateGothProvider creates a GothProvider from this Provider
Expand Down
18 changes: 18 additions & 0 deletions templates/user/auth/external_auth_methods.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div id="external-login-navigator" class="tw-py-1 tw-flex tw-flex-col tw-gap-3">
{{range $provider := .OAuth2Providers}}
{{/* use QueryEscape for consistent with frontend urlQueryEscape, it is right for a path component */}}
<a class="ui button external-login-link tw-gap-3" data-require-appurl-check="true" rel="nofollow" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName | QueryEscape}}">
{{$provider.IconHTML 24}} {{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
</a>
{{end}}
{{if .EnableOpenIDSignIn}}
<a class="ui button external-login-link tw-gap-3" data-require-appurl-check="true" rel="nofollow" href="{{AppSubUrl}}/user/login/openid">
{{svg "fontawesome-openid" 24}} {{ctx.Locale.Tr "sign_in_with_provider" "OpenID"}}
</a>
{{end}}
{{if .EnableSSPI}}
<a class="ui button external-login-link tw-gap-3" rel="nofollow" href="{{AppSubUrl}}/user/login?auth_with_sspi=1">
{{svg "fontawesome-windows" 24}} Windows SSPI
</a>
{{end}}
</div>
24 changes: 0 additions & 24 deletions templates/user/auth/oauth_container.tmpl

This file was deleted.

11 changes: 5 additions & 6 deletions templates/user/auth/signin_inner.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,13 @@
</button>
</div>
</form>
{{end}}{{/*if .EnablePasswordSignInForm*/}}
{{/* "oauth_container" contains not only "oauth2" methods, but also "OIDC" and "SSPI" methods */}}
{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
{{if and $showOAuth2Methods .EnablePasswordSignInForm}}
{{end}}{{/*end if .EnablePasswordSignInForm*/}}
{{$showExternalAuthMethods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
{{if and $showExternalAuthMethods .EnablePasswordSignInForm}}
<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
{{end}}
{{if $showOAuth2Methods}}
{{template "user/auth/oauth_container" .}}
{{if $showExternalAuthMethods}}
{{template "user/auth/external_auth_methods" .}}
{{end}}
</div>
</div>
Expand Down
8 changes: 3 additions & 5 deletions templates/user/auth/signup_inner.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,10 @@
</button>
</div>
{{end}}
{{/* "oauth_container" contains not only "oauth2" methods, but also "OIDC" and "SSPI" methods */}}
{{/* TODO: it seems that "EnableSSPI" is only set in "sign-in" handlers, but it should use the same logic to control its display */}}
{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
{{if $showOAuth2Methods}}
{{$showExternalAuthMethods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
{{if $showExternalAuthMethods}}
<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
{{template "user/auth/oauth_container" .}}
{{template "user/auth/external_auth_methods" .}}
{{end}}
</form>
</div>
Expand Down
26 changes: 13 additions & 13 deletions web_src/js/features/user-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ export function initUserCheckAppUrl() {
checkAppUrlScheme();
}

export function initUserAuthOauth2() {
const outer = document.querySelector('#oauth2-login-navigator');
if (!outer) return;
const inner = document.querySelector('#oauth2-login-navigator-inner')!;
export function initUserExternalLogins() {
const container = document.querySelector('#external-login-navigator');
if (!container) return;

checkAppUrl();

for (const link of outer.querySelectorAll('.oauth-login-link')) {
// whether the auth method requires app url check (need consistent ROOT_URL with visited URL)
let needCheckAppUrl = false;
for (const link of container.querySelectorAll('.external-login-link')) {
needCheckAppUrl = needCheckAppUrl || link.getAttribute('data-require-appurl-check') === 'true';
link.addEventListener('click', () => {
inner.classList.add('tw-invisible');
outer.classList.add('is-loading');
container.classList.add('is-loading');
setTimeout(() => {
// recover previous content to let user try again
// usually redirection will be performed before this action
outer.classList.remove('is-loading');
inner.classList.remove('tw-invisible');
// recover previous content to let user try again, usually redirection will be performed before this action
container.classList.remove('is-loading');
}, 5000);
});
}
if (needCheckAppUrl) {
checkAppUrl();
}
}
4 changes: 2 additions & 2 deletions web_src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {initStopwatch} from './features/stopwatch.ts';
import {initRepoFileSearch} from './features/repo-findfile.ts';
import {initMarkupContent} from './markup/content.ts';
import {initRepoFileView} from './features/file-view.ts';
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
import {initUserExternalLogins, initUserCheckAppUrl} from './features/user-auth.ts';
import {initRepoPullRequestReview, initRepoIssueFilterItemLabel} from './features/repo-issue.ts';
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
import {initRepoTopicBar} from './features/repo-home.ts';
Expand Down Expand Up @@ -149,7 +149,7 @@ const initPerformanceTracer = callInitFunctions([
initCaptcha,

initUserCheckAppUrl,
initUserAuthOauth2,
initUserExternalLogins,
initUserAuthWebAuthn,
initUserAuthWebAuthnRegister,
initUserSettings,
Expand Down