Skip to content
Draft
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
18 changes: 18 additions & 0 deletions e2e/tests/keycloak/tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,24 @@ test("logout", async ({ page }) => {
expect(logoutResponse?.url()).toMatch(/http:\/\/localhost:8000\/realms\/master\/protocol\/openid-connect\/auth.*/);
});

test("frontchannel logout", async ({ page }) => {
await expectGotoOkay(page, "http://localhost:9080");

const response = await login(page, "admin", "admin", "http://localhost:9080");

expect(response.status()).toBe(200);

const logoutResponse = await page.goto("http://localhost:9080/frontchannel-logout");

expect(logoutResponse?.status()).toBe(200);
});

test("frontchannel logout doesn't fail if no session", async ({ page }) => {
const logoutResponse = await page.goto("http://localhost:9080/frontchannel-logout");

expect(logoutResponse?.status()).toBe(200);
});

test("test two services is seamless", async ({ page }) => {
await configureTraefik(`
http:
Expand Down
2 changes: 2 additions & 0 deletions src/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Config struct {
LogoutUri string `json:"logout_uri"`
PostLogoutRedirectUri string `json:"post_logout_redirect_uri"`
ValidPostLogoutRedirectUris []string `json:"valid_post_logout_redirect_uris"`
FrontChannelLogoutUri string `json:"front_channel_logout_uri"`

CookieNamePrefix string `json:"cookie_name_prefix"`
SessionCookie *SessionCookieConfig `json:"session_cookie"`
Expand Down Expand Up @@ -152,6 +153,7 @@ func CreateConfig() *Config {
//Scopes: []string{"openid", "profile", "email"},
CallbackUri: "/oidc/callback",
LogoutUri: "/logout",
FrontChannelLogoutUri: "/frontchannel-logout",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we support environment variable expansion?

PostLogoutRedirectUri: "/",
CookieNamePrefix: "TraefikOidcAuth",
SessionCookie: &SessionCookieConfig{
Expand Down
53 changes: 53 additions & 0 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ func (toa *TraefikOidcAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request)
toa.handleLogout(rw, req, session)
return
}
if toa.Config.FrontChannelLogoutUri != "" && strings.HasPrefix(req.RequestURI, toa.Config.FrontChannelLogoutUri) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would prefer to move this before the if err == nil && session != nil { and then pass in the "optional" session and claims.
Then we can handle both cases (with and without session) within the handleFrontChannelLogout function and we have everything in one place.

Eg:

session, updateSession, claims, err := toa.getSessionForRequest(req)

// Handle front-channel logout
if err == nil && strings.HasPrefix(req.RequestURI, toa.Config.FrontChannelLogoutUri) {
	toa.handleFrontChannelLogout(rw, req, session, claims)
	return
}

What do you think?

toa.handleFrontchannelLogout(rw, req, claims)
return
}

// If this request is using external authentication by using a header or custom cookie,
// we need to validate the authorization on every request.
Expand Down Expand Up @@ -180,6 +184,11 @@ func (toa *TraefikOidcAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request)
// Clear the session cookie
clearChunkedCookie(toa.Config, rw, req, getSessionCookieName(toa.Config))

// Don't display unauthenticated error for frontchannel-logout URI
if strings.HasPrefix(req.RequestURI, toa.Config.FrontChannelLogoutUri) {
toa.writeSuccessfulLogout(rw, req)
return
}
toa.handleUnauthenticated(rw, req)
}

Expand Down Expand Up @@ -426,6 +435,50 @@ func (toa *TraefikOidcAuth) handleLogout(rw http.ResponseWriter, req *http.Reque
http.Redirect(rw, req, endSessionURL.String(), http.StatusFound)
}

func (toa *TraefikOidcAuth) handleFrontchannelLogout(rw http.ResponseWriter, req *http.Request, claims map[string]interface{}) {
toa.logger.Log(logging.LevelInfo, "Handling frontchannel logout...")
// https://openid.net/specs/openid-connect-frontchannel-1_0.html

// if exactly one of iss or sid is missing, we ignore the request
iss := req.URL.Query().Get("iss")
sid := req.URL.Query().Get("sid")
if (iss == "" && sid != "") || (iss != "" && sid == "") {
toa.logger.Log(logging.LevelWarn, "Ignoring frontchannel logout request: iss or sid is missing")
http.Error(rw, "iss or sid is missing", http.StatusBadRequest)
return
}

if (iss == "" && sid == "") || (claims["iss"] == iss && claims["sid"] == sid) {
// If both are missing or the issuer is valid, we proceed
toa.logger.Log(logging.LevelInfo, "Proceeding with frontchannel logout")

clearChunkedCookie(toa.Config, rw, req, getSessionCookieName(toa.Config))
toa.writeSuccessfulLogout(rw, req)
return
} else {
toa.logger.Log(logging.LevelWarn, "Ignoring frontchannel logout request: sid or iss does not match")
http.Error(rw, "sid or iss does not match", http.StatusBadRequest)
return
}
}

func (toa *TraefikOidcAuth) writeSuccessfulLogout(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")

data := make(map[string]interface{})
data["statusCode"] = http.StatusOK
data["statusName"] = "Logged out"
data["description"] = "You have been logged out successfully."

if toa.Config.LoginUri != "" {
data["primaryButtonText"] = "Log back in"
data["primaryButtonUrl"] = utils.EnsureAbsoluteUrl(req, toa.Config.LoginUri)
}

errorPages.WriteError(toa.logger, &errorPages.ErrorPageConfig{}, rw, req, data)
}


func (toa *TraefikOidcAuth) handleUnauthenticated(rw http.ResponseWriter, req *http.Request) {
switch toa.Config.UnauthorizedBehavior {
case "Challenge":
Expand Down