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
2 changes: 1 addition & 1 deletion models/auth/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func RegisterTypeConfig(typ Type, exemplar Config) {
type Source struct {
ID int64 `xorm:"pk autoincr"`
Type Type
Name string `xorm:"UNIQUE"`
Name string `xorm:"UNIQUE"` // it can be the OIDC's provider name, see services/auth/source/oauth2/source_register.go: RegisterSource
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"`
Expand Down
15 changes: 14 additions & 1 deletion routers/web/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,21 @@ func SignOut(ctx *context.Context) {
Data: ctx.Session.ID(),
})
}

// prepare the sign-out URL before destroying the session
redirectTo := buildSignOutRedirectURL(ctx)
HandleSignOut(ctx)
ctx.JSONRedirect(setting.AppSubURL + "/")
ctx.Redirect(redirectTo)
}

func buildSignOutRedirectURL(ctx *context.Context) string {
// TODO: can also support REVERSE_PROXY_AUTHENTICATION logout URL in the future
if ctx.Doer != nil && ctx.Doer.LoginType == auth.OAuth2 {
if s := buildOIDCEndSessionURL(ctx, ctx.Doer); s != "" {
return s
}
}
return setting.AppSubURL + "/"
}

// SignUp render the register page
Expand Down
48 changes: 45 additions & 3 deletions routers/web/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ package auth

import (
"net/http"
"net/http/httptest"
"net/url"
"testing"

auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
Expand All @@ -19,6 +21,7 @@ import (
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
Expand All @@ -29,10 +32,10 @@ func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
IsActive: true,
Cfg: &cfg,
})
assert.NoError(t, err)
require.NoError(t, err)
}

func TestUserLogin(t *testing.T) {
func TestWebAuthUserLogin(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/user/login")
SignIn(ctx)
assert.Equal(t, http.StatusOK, resp.Code)
Expand Down Expand Up @@ -60,7 +63,7 @@ func TestUserLogin(t *testing.T) {
assert.Equal(t, "/", test.RedirectURL(resp))
}

func TestSignUpOAuth2Login(t *testing.T) {
func TestWebAuthOAuth2(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()

_ = oauth2.Init(t.Context())
Expand Down Expand Up @@ -92,4 +95,43 @@ func TestSignUpOAuth2Login(t *testing.T) {
assert.Equal(t, "/user/login", test.RedirectURL(resp))
assert.Contains(t, ctx.Flash.ErrorMsg, "auth.oauth.signin.error.general")
})

t.Run("OIDCLogout", func(t *testing.T) {
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/.well-known/openid-configuration":
_, _ = w.Write([]byte(`{
"issuer": "` + mockServer.URL + `",
"authorization_endpoint": "` + mockServer.URL + `/authorize",
"token_endpoint": "` + mockServer.URL + `/token",
"userinfo_endpoint": "` + mockServer.URL + `/userinfo",
"end_session_endpoint": "https://example.com/oidc-logout?oidc-key=oidc-val"
}`))
default:
http.NotFound(w, r)
}
}))
defer mockServer.Close()

addOAuth2Source(t, "oidc-auth-source", oauth2.Source{
Provider: "openidConnect",
ClientID: "mock-client-id",
OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
})
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "oidc-auth-source")
require.NoError(t, err)

mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
ctx, resp := contexttest.MockContext(t, "/user/logout", mockOpt)
ctx.Doer = &user_model.User{ID: 1, LoginType: auth_model.OAuth2, LoginSource: authSource.ID}
SignOut(ctx)
assert.Equal(t, http.StatusSeeOther, resp.Code)
u, err := url.Parse(test.RedirectURL(resp))
require.NoError(t, err)
expectedValues := url.Values{"oidc-key": []string{"oidc-val"}, "post_logout_redirect_uri": []string{setting.AppURL}, "client_id": []string{"mock-client-id"}}
assert.Equal(t, expectedValues, u.Query())
u.RawQuery = ""
assert.Equal(t, "https://example.com/oidc-logout", u.String())
})
}
37 changes: 37 additions & 0 deletions routers/web/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import (
"html"
"io"
"net/http"
"net/url"
"sort"
"strings"

"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
Expand Down Expand Up @@ -506,3 +508,38 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
// no user found to login
return nil, gothUser, nil
}

// buildOIDCEndSessionURL constructs an OIDC RP-Initiated Logout URL for the
// given user. Returns "" if the user's auth source is not OIDC or doesn't
// advertise an end_session_endpoint.
func buildOIDCEndSessionURL(ctx *context.Context, doer *user_model.User) string {
authSource, err := auth.GetSourceByID(ctx, doer.LoginSource)
if err != nil {
log.Error("Failed to get auth source for OIDC logout (source=%d): %v", doer.LoginSource, err)
return ""
}

oauth2Cfg, ok := authSource.Cfg.(*oauth2.Source)
if !ok {
return ""
}

endSessionEndpoint := oauth2.GetOIDCEndSessionEndpoint(authSource.Name)
if endSessionEndpoint == "" {
return ""
}

endSessionURL, err := url.Parse(endSessionEndpoint)
if err != nil {
log.Error("Failed to parse end_session_endpoint %q: %v", endSessionEndpoint, err)
return ""
}

// RP-Initiated Logout 1.0: use client_id to identify the client to the IdP.
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
params := endSessionURL.Query()
params.Set("client_id", oauth2Cfg.ClientID)
params.Set("post_logout_redirect_uri", httplib.GuessCurrentAppURL(ctx))
endSessionURL.RawQuery = params.Encode()
return endSessionURL.String()
}
2 changes: 1 addition & 1 deletion routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ func registerWebRoutes(m *web.Router) {
m.Post("/recover_account", auth.ResetPasswdPost)
m.Get("/forgot_password", auth.ForgotPasswd)
m.Post("/forgot_password", auth.ForgotPasswdPost)
m.Post("/logout", auth.SignOut)
m.Get("/logout", auth.SignOut)
m.Get("/stopwatches", reqSignIn, user.GetStopwatches)
m.Get("/search_candidates", optExploreSignIn, user.SearchCandidates)
m.Group("/oauth2", func() {
Expand Down
21 changes: 21 additions & 0 deletions services/auth/source/oauth2/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting"

"github.com/markbates/goth"
"github.com/markbates/goth/providers/openidConnect"
)

// Provider is an interface for describing a single OAuth2 provider
Expand Down Expand Up @@ -197,6 +198,26 @@ func ClearProviders() {
goth.ClearProviders()
}

// GetOIDCEndSessionEndpoint returns the OIDC end_session_endpoint for the
// given provider name. Returns "" if the provider is not OIDC or doesn't
// advertise an end_session_endpoint in its discovery document.
func GetOIDCEndSessionEndpoint(providerName string) string {
gothRWMutex.RLock()
defer gothRWMutex.RUnlock()

provider, ok := goth.GetProviders()[providerName]
if !ok {
return ""
}

oidcProvider, ok := provider.(*openidConnect.Provider)
if !ok || oidcProvider.OpenIDConfig == nil {
return ""
}

return oidcProvider.OpenIDConfig.EndSessionEndpoint
}

var ErrAuthSourceNotActivated = errors.New("auth source is not activated")

// used to create different types of goth providers
Expand Down
4 changes: 2 additions & 2 deletions templates/base/head_navbar.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
</div>

<div class="divider"></div>
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
<a class="item" href="{{AppSubUrl}}/user/logout">
{{svg "octicon-sign-out"}}
{{ctx.Locale.Tr "sign_out"}}
</a>
Expand Down Expand Up @@ -128,7 +128,7 @@
</a>
{{end}}
<div class="divider"></div>
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
<a class="item" href="{{AppSubUrl}}/user/logout">
{{svg "octicon-sign-out"}}
{{ctx.Locale.Tr "sign_out"}}
</a>
Expand Down
8 changes: 6 additions & 2 deletions tests/integration/signout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import (
"net/http"
"testing"

"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"

"github.com/stretchr/testify/assert"
)

func TestSignOut(t *testing.T) {
defer tests.PrepareTestEnv(t)()

session := loginUser(t, "user2")

req := NewRequest(t, "POST", "/user/logout")
session.MakeRequest(t, req, http.StatusOK)
req := NewRequest(t, "GET", "/user/logout")
resp := session.MakeRequest(t, req, http.StatusSeeOther)
assert.Equal(t, "/", test.RedirectURL(resp))

// try to view a private repo, should fail
req = NewRequest(t, "GET", "/user2/repo2")
Expand Down