mirror of
https://github.com/nais/wonderwall.git
synced 2026-05-21 07:42:53 +00:00
feat: add local logout endpoint
This commit is contained in:
23
README.md
23
README.md
@@ -42,20 +42,21 @@ Wonderwall exposes and owns these endpoints (which means they will never be prox
|
||||
|
||||
Endpoints that are available for use by applications:
|
||||
|
||||
| Path | Description |
|
||||
|---------------------------|------------------------------------------------------------------------------------------------|
|
||||
| `/oauth2/login` | Initiates the OpenID Connect Authorization Code flow |
|
||||
| `/oauth2/logout` | Initiates local and global/single-logout |
|
||||
| `/oauth2/session` | Returns the current user's session metadata |
|
||||
| `/oauth2/session/refresh` | Refreshes the tokens for the user's session. Requires the `session.refresh` flag to be enabled |
|
||||
| Path | Description |
|
||||
|--------------------------------|------------------------------------------------------------------------------------------------|
|
||||
| `GET /oauth2/login` | Initiates the OpenID Connect Authorization Code flow |
|
||||
| `GET /oauth2/logout` | Performs local logout and redirects the user to global/single-logout |
|
||||
| `GET /oauth2/logout/local` | Performs local logout only |
|
||||
| `GET /oauth2/session` | Returns the current user's session metadata |
|
||||
| `POST /oauth2/session/refresh` | Refreshes the tokens for the user's session. Requires the `session.refresh` flag to be enabled |
|
||||
|
||||
Endpoints that should be registered at and only be triggered by identity providers:
|
||||
|
||||
| Path | Description |
|
||||
|-------------------------------|--------------------------------------------------------------------------------------------|
|
||||
| `/oauth2/callback` | Handles the callback from the identity provider |
|
||||
| `/oauth2/logout/callback` | Handles the logout callback from the identity provider |
|
||||
| `/oauth2/logout/frontchannel` | Handles global logout request (initiated by identity provider on behalf of another client) |
|
||||
| Path | Description |
|
||||
|-----------------------------------|--------------------------------------------------------------------------------------------|
|
||||
| `GET /oauth2/callback` | Handles the callback from the identity provider |
|
||||
| `GET /oauth2/logout/callback` | Handles the logout callback from the identity provider |
|
||||
| `GET /oauth2/logout/frontchannel` | Handles global logout request (initiated by identity provider on behalf of another client) |
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ type Source interface {
|
||||
GetSessions() *session.Handler
|
||||
}
|
||||
|
||||
func Handler(src Source, w http.ResponseWriter, r *http.Request) {
|
||||
type Options struct {
|
||||
GlobalLogout bool
|
||||
}
|
||||
|
||||
func Handler(src Source, w http.ResponseWriter, r *http.Request, opts Options) {
|
||||
logger := logentry.LogEntryFrom(r)
|
||||
logout, err := src.GetClient().Logout(r)
|
||||
if err != nil {
|
||||
@@ -49,6 +53,7 @@ func Handler(src Source, w http.ResponseWriter, r *http.Request) {
|
||||
"jti": sessionData.IDTokenJwtID,
|
||||
}
|
||||
logger.WithFields(fields).Info("logout: successful local logout")
|
||||
metrics.ObserveLogout(metrics.LogoutOperationLocal)
|
||||
}
|
||||
|
||||
cookie.Clear(w, cookie.Session, src.GetCookieOptsPathAware(r))
|
||||
@@ -57,7 +62,9 @@ func Handler(src Source, w http.ResponseWriter, r *http.Request) {
|
||||
src.GetLoginstatus().ClearCookie(w, src.GetCookieOptions())
|
||||
}
|
||||
|
||||
logger.Debug("logout: redirecting to identity provider")
|
||||
metrics.ObserveLogout(metrics.LogoutOperationSelfInitiated)
|
||||
http.Redirect(w, r, logout.SingleLogoutURL(idToken), http.StatusTemporaryRedirect)
|
||||
if opts.GlobalLogout {
|
||||
logger.Debug("logout: redirecting to identity provider for global/single-logout")
|
||||
metrics.ObserveLogout(metrics.LogoutOperationSelfInitiated)
|
||||
http.Redirect(w, r, logout.SingleLogoutURL(idToken), http.StatusTemporaryRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,17 @@ func (s *StandardHandler) LoginCallback(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (s *StandardHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
apilogout.Handler(s, w, r)
|
||||
opts := apilogout.Options{
|
||||
GlobalLogout: true,
|
||||
}
|
||||
apilogout.Handler(s, w, r, opts)
|
||||
}
|
||||
|
||||
func (s *StandardHandler) LogoutLocal(w http.ResponseWriter, r *http.Request) {
|
||||
opts := apilogout.Options{
|
||||
GlobalLogout: false,
|
||||
}
|
||||
apilogout.Handler(s, w, r, opts)
|
||||
}
|
||||
|
||||
func (s *StandardHandler) LogoutCallback(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestHandler_Logout(t *testing.T) {
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := localLogout(t, rpClient, idp)
|
||||
resp := selfInitiatedLogout(t, rpClient, idp)
|
||||
|
||||
// Get endsession endpoint after local logout
|
||||
endsessionURL := resp.Location
|
||||
@@ -81,7 +81,7 @@ func TestHandler_Logout(t *testing.T) {
|
||||
idpserverURL, err := url.Parse(idp.ProviderServer.URL)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := idp.GetRequest(idp.RelyingPartyServer.URL + "/oauth2/logout")
|
||||
req := idp.GetRequest(idp.RelyingPartyServer.URL + "/oauth2/logout/callback")
|
||||
expectedLogoutCallbackURL, err := urlpkg.LogoutCallbackURL(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -139,6 +139,17 @@ func TestHandler_FrontChannelLogout(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestHandler_LogoutLocal(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
localLogout(t, rpClient, idp)
|
||||
}
|
||||
|
||||
func TestHandler_SessionStateRequired(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
@@ -708,7 +719,7 @@ func login(t *testing.T, rpClient *http.Client, idp *mock.IdentityProvider) *htt
|
||||
return callback(t, rpClient, resp)
|
||||
}
|
||||
|
||||
func localLogout(t *testing.T, rpClient *http.Client, idp *mock.IdentityProvider) response {
|
||||
func selfInitiatedLogout(t *testing.T, rpClient *http.Client, idp *mock.IdentityProvider) response {
|
||||
// Request self-initiated logout
|
||||
logoutURL, err := url.Parse(idp.RelyingPartyServer.URL + "/oauth2/logout")
|
||||
assert.NoError(t, err)
|
||||
@@ -726,7 +737,7 @@ func localLogout(t *testing.T, rpClient *http.Client, idp *mock.IdentityProvider
|
||||
|
||||
func logout(t *testing.T, rpClient *http.Client, idp *mock.IdentityProvider) {
|
||||
// Get endsession endpoint after local logout
|
||||
resp := localLogout(t, rpClient, idp)
|
||||
resp := selfInitiatedLogout(t, rpClient, idp)
|
||||
|
||||
// Follow redirect to endsession endpoint at identity provider
|
||||
resp = get(t, rpClient, resp.Location.String())
|
||||
@@ -755,6 +766,21 @@ func logout(t *testing.T, rpClient *http.Client, idp *mock.IdentityProvider) {
|
||||
assert.Nil(t, sessionCookie)
|
||||
}
|
||||
|
||||
func localLogout(t *testing.T, rpClient *http.Client, idp *mock.IdentityProvider) response {
|
||||
logoutURL, err := url.Parse(idp.RelyingPartyServer.URL + "/oauth2/logout/local")
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp := get(t, rpClient, logoutURL.String())
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
cookies := rpClient.Jar.Cookies(logoutURL)
|
||||
sessionCookie := getCookieFromJar(cookie.Session, cookies)
|
||||
|
||||
assert.Nil(t, sessionCookie)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func sessionInfo(t *testing.T, idp *mock.IdentityProvider, rpClient *http.Client) response {
|
||||
sessionInfoURL, err := url.Parse(idp.RelyingPartyServer.URL + "/oauth2/session")
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -27,8 +27,9 @@ const (
|
||||
type LogoutOperation = string
|
||||
|
||||
const (
|
||||
LogoutOperationSelfInitiated = "self_initiated"
|
||||
LogoutOperationFrontChannel = "front_channel"
|
||||
LogoutOperationLocal = "local"
|
||||
LogoutOperationSelfInitiated = "self_initiated"
|
||||
)
|
||||
|
||||
type RedisOperation = string
|
||||
|
||||
@@ -7,6 +7,7 @@ const (
|
||||
Logout = "/logout"
|
||||
LogoutCallback = "/logout/callback"
|
||||
LogoutFrontChannel = "/logout/frontchannel"
|
||||
LogoutLocal = "/logout/local"
|
||||
Session = "/session"
|
||||
SessionRefresh = "/session/refresh"
|
||||
)
|
||||
|
||||
@@ -21,12 +21,14 @@ type Handlers interface {
|
||||
Login(http.ResponseWriter, *http.Request)
|
||||
// LoginCallback handles the authentication response from the identity provider.
|
||||
LoginCallback(http.ResponseWriter, *http.Request)
|
||||
// Logout triggers self-initiated logout for the current user.
|
||||
// Logout triggers self-initiated logout for the current user, as well as single-logout at the identity provider.
|
||||
Logout(http.ResponseWriter, *http.Request)
|
||||
// LogoutCallback handles the callback initiated by the self-initiated logout after single-logout at the identity provider.
|
||||
LogoutCallback(http.ResponseWriter, *http.Request)
|
||||
// LogoutFrontChannel performs a local logout initiated by a third party in the SSO circle-of-trust.
|
||||
LogoutFrontChannel(http.ResponseWriter, *http.Request)
|
||||
// LogoutLocal clears the current user's local session for logout, without triggering single-logout at the identity provider.
|
||||
LogoutLocal(http.ResponseWriter, *http.Request)
|
||||
// Session returns metadata for the current user's session.
|
||||
Session(http.ResponseWriter, *http.Request)
|
||||
// SessionRefresh refreshes current user's session and returns the associated updated metadata.
|
||||
@@ -62,8 +64,9 @@ func New(src Source) chi.Router {
|
||||
r.Get(paths.Login, src.Login)
|
||||
r.Get(paths.LoginCallback, src.LoginCallback)
|
||||
r.Get(paths.Logout, src.Logout)
|
||||
r.Get(paths.LogoutFrontChannel, src.LogoutFrontChannel)
|
||||
r.Get(paths.LogoutCallback, src.LogoutCallback)
|
||||
r.Get(paths.LogoutFrontChannel, src.LogoutFrontChannel)
|
||||
r.Get(paths.LogoutLocal, src.LogoutLocal)
|
||||
r.Get(paths.Session, src.Session)
|
||||
r.Get(paths.SessionRefresh, src.SessionRefresh) // TODO: for legacy purposes, remove after grace period
|
||||
r.Post(paths.SessionRefresh, src.SessionRefresh)
|
||||
|
||||
Reference in New Issue
Block a user