Skip to content

Commit 8b3a950

Browse files
authored
Add logout endpoint (#107)
Add logout endpoint that clears the auth cookie + optional "logout-redirect" config option, to which, when set, the user will be redirected.
1 parent 655edde commit 8b3a950

File tree

7 files changed

+96
-1
lines changed

7 files changed

+96
-1
lines changed

Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22
format:
33
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go
44

5-
.PHONY: format
5+
test:
6+
go test -v ./...
7+
8+
.PHONY: format test

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ A minimal forward authentication service that provides OAuth/SSO login and authe
3535
- [Operation Modes](#operation-modes)
3636
- [Overlay Mode](#overlay-mode)
3737
- [Auth Host Mode](#auth-host-mode)
38+
- [Logging Out](#logging-out)
3839
- [Copyright](#copyright)
3940
- [License](#license)
4041

@@ -136,6 +137,7 @@ Application Options:
136137
--default-provider=[google|oidc] Default provider (default: google) [$DEFAULT_PROVIDER]
137138
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
138139
--lifetime= Lifetime in seconds (default: 43200) [$LIFETIME]
140+
--logout-redirect= URL to redirect to following logout [$LOGOUT_REDIRECT]
139141
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
140142
--secret= Secret used for signing (required) [$SECRET]
141143
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
@@ -243,6 +245,10 @@ All options can be supplied in any of the following ways, in the following prece
243245
244246
Default: `43200` (12 hours)
245247
248+
- `logout-redirect`
249+
250+
When set, users will be redirected to this URL following logout.
251+
246252
- `url-path`
247253
248254
Customise the path that this service uses to handle the callback following authentication.
@@ -443,6 +449,14 @@ Two criteria must be met for an `auth-host` to be used:
443449

444450
Please note: For Auth Host mode to work, you must ensure that requests to your auth-host are routed to the traefik-forward-auth container, as demonstrated with the service labels in the [docker-compose-auth.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/swarm/docker-compose-auth-host.yml) example and the [ingressroute resource](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/kubernetes/advanced-separate-pod/traefik-forward-auth/ingress.yaml) in a kubernetes example.
445451

452+
### Logging Out
453+
454+
The service provides an endpoint to clear a users session and "log them out". The path is created by appending `/logout` to your configured `path` and so with the default settings it will be: `/_oauth/logout`.
455+
456+
You can use the `logout-redirect` config option to redirect users to another URL following logout (note: the user will not have a valid auth cookie after being logged out).
457+
458+
Note: This only clears the auth cookie from the users browser and as this service is stateless, it does not invalidate the cookie against future use. So if the cookie was recorded, for example, it could continue to be used for the duration of the cookie lifetime.
459+
446460
## Copyright
447461

448462
2018 Thom Seddon

internal/auth.go

+13
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ func MakeCookie(r *http.Request, email string) *http.Cookie {
144144
}
145145
}
146146

147+
// ClearCookie clears the auth cookie
148+
func ClearCookie(r *http.Request) *http.Cookie {
149+
return &http.Cookie{
150+
Name: config.CookieName,
151+
Value: "",
152+
Path: "/",
153+
Domain: cookieDomain(r),
154+
HttpOnly: true,
155+
Secure: !config.InsecureCookie,
156+
Expires: time.Now().Local().Add(time.Hour * -1),
157+
}
158+
}
159+
147160
// MakeCSRFCookie makes a csrf cookie (used during login only)
148161
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
149162
return &http.Cookie{

internal/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Config struct {
3434
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" description:"Default provider"`
3535
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" env-delim:"," description:"Only allow given email domains, can be set multiple times"`
3636
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
37+
LogoutRedirect string `long:"logout-redirect" env:"LOGOUT_REDIRECT" description:"URL to redirect to following logout"`
3738
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
3839
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
3940
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`

internal/config_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func TestConfigDefaults(t *testing.T) {
3333
assert.Equal("google", c.DefaultProvider)
3434
assert.Len(c.Domains, 0)
3535
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
36+
assert.Equal("", c.LogoutRedirect)
3637
assert.Equal("/_oauth", c.Path)
3738
assert.Len(c.Whitelist, 0)
3839

internal/server.go

+20
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ func (s *Server) buildRoutes() {
4141
// Add callback handler
4242
s.router.Handle(config.Path, s.AuthCallbackHandler())
4343

44+
// Add logout handler
45+
s.router.Handle(config.Path+"/logout", s.LogoutHandler())
46+
4447
// Add a default handler
4548
if config.DefaultAction == "allow" {
4649
s.router.NewRoute().Handler(s.AllowHandler("default"))
@@ -180,6 +183,23 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
180183
}
181184
}
182185

186+
// LogoutHandler logs a user out
187+
func (s *Server) LogoutHandler() http.HandlerFunc {
188+
return func(w http.ResponseWriter, r *http.Request) {
189+
// Clear cookie
190+
http.SetCookie(w, ClearCookie(r))
191+
192+
logger := s.logger(r, "Logout", "default", "Handling logout")
193+
logger.Info("Logged out user")
194+
195+
if config.LogoutRedirect != "" {
196+
http.Redirect(w, r, config.LogoutRedirect, http.StatusTemporaryRedirect)
197+
} else {
198+
http.Error(w, "You have been logged out", 401)
199+
}
200+
}
201+
}
202+
183203
func (s *Server) authRedirect(logger *logrus.Entry, w http.ResponseWriter, r *http.Request, p provider.Provider) {
184204
// Error indicates no cookie, generate nonce
185205
err, nonce := Nonce()

internal/server_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,49 @@ func TestServerAuthCallback(t *testing.T) {
170170
assert.Equal("", fwd.Path, "valid request should be redirected to return url")
171171
}
172172

173+
func TestServerLogout(t *testing.T) {
174+
require := require.New(t)
175+
assert := assert.New(t)
176+
config = newDefaultConfig()
177+
178+
req := newDefaultHttpRequest("/_oauth/logout")
179+
res, _ := doHttpRequest(req, nil)
180+
require.Equal(401, res.StatusCode, "should return a 401")
181+
182+
// Check for cookie
183+
var cookie *http.Cookie
184+
for _, c := range res.Cookies() {
185+
if c.Name == config.CookieName {
186+
cookie = c
187+
}
188+
}
189+
require.NotNil(cookie)
190+
require.Less(cookie.Expires.Local().Unix(), time.Now().Local().Unix()-50, "cookie should have expired")
191+
192+
// Test with redirect
193+
config.LogoutRedirect = "http://redirect/path"
194+
req = newDefaultHttpRequest("/_oauth/logout")
195+
res, _ = doHttpRequest(req, nil)
196+
require.Equal(307, res.StatusCode, "should return a 307")
197+
198+
// Check for cookie
199+
cookie = nil
200+
for _, c := range res.Cookies() {
201+
if c.Name == config.CookieName {
202+
cookie = c
203+
}
204+
}
205+
require.NotNil(cookie)
206+
require.Less(cookie.Expires.Local().Unix(), time.Now().Local().Unix()-50, "cookie should have expired")
207+
208+
fwd, _ := res.Location()
209+
require.NotNil(fwd)
210+
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url")
211+
assert.Equal("redirect", fwd.Host, "valid request should be redirected to return url")
212+
assert.Equal("/path", fwd.Path, "valid request should be redirected to return url")
213+
214+
}
215+
173216
func TestServerDefaultAction(t *testing.T) {
174217
assert := assert.New(t)
175218
config = newDefaultConfig()

0 commit comments

Comments
 (0)