Skip to content

Commit

Permalink
Add logout endpoint (thomseddon#107)
Browse files Browse the repository at this point in the history
Add logout endpoint that clears the auth cookie + optional "logout-redirect" config option, to which, when set, the user will be redirected.
  • Loading branch information
thomseddon authored Jun 3, 2020
1 parent 655edde commit 8b3a950
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 1 deletion.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
format:
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go

.PHONY: format
test:
go test -v ./...

.PHONY: format test
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ A minimal forward authentication service that provides OAuth/SSO login and authe
- [Operation Modes](#operation-modes)
- [Overlay Mode](#overlay-mode)
- [Auth Host Mode](#auth-host-mode)
- [Logging Out](#logging-out)
- [Copyright](#copyright)
- [License](#license)

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

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.

### Logging Out

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`.

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).

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.

## Copyright

2018 Thom Seddon
Expand Down
13 changes: 13 additions & 0 deletions internal/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ func MakeCookie(r *http.Request, email string) *http.Cookie {
}
}

// ClearCookie clears the auth cookie
func ClearCookie(r *http.Request) *http.Cookie {
return &http.Cookie{
Name: config.CookieName,
Value: "",
Path: "/",
Domain: cookieDomain(r),
HttpOnly: true,
Secure: !config.InsecureCookie,
Expires: time.Now().Local().Add(time.Hour * -1),
}
}

// MakeCSRFCookie makes a csrf cookie (used during login only)
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
return &http.Cookie{
Expand Down
1 change: 1 addition & 0 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Config struct {
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" description:"Default provider"`
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" env-delim:"," description:"Only allow given email domains, can be set multiple times"`
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
LogoutRedirect string `long:"logout-redirect" env:"LOGOUT_REDIRECT" description:"URL to redirect to following logout"`
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
Expand Down
1 change: 1 addition & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestConfigDefaults(t *testing.T) {
assert.Equal("google", c.DefaultProvider)
assert.Len(c.Domains, 0)
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
assert.Equal("", c.LogoutRedirect)
assert.Equal("/_oauth", c.Path)
assert.Len(c.Whitelist, 0)

Expand Down
20 changes: 20 additions & 0 deletions internal/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ func (s *Server) buildRoutes() {
// Add callback handler
s.router.Handle(config.Path, s.AuthCallbackHandler())

// Add logout handler
s.router.Handle(config.Path+"/logout", s.LogoutHandler())

// Add a default handler
if config.DefaultAction == "allow" {
s.router.NewRoute().Handler(s.AllowHandler("default"))
Expand Down Expand Up @@ -180,6 +183,23 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
}
}

// LogoutHandler logs a user out
func (s *Server) LogoutHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Clear cookie
http.SetCookie(w, ClearCookie(r))

logger := s.logger(r, "Logout", "default", "Handling logout")
logger.Info("Logged out user")

if config.LogoutRedirect != "" {
http.Redirect(w, r, config.LogoutRedirect, http.StatusTemporaryRedirect)
} else {
http.Error(w, "You have been logged out", 401)
}
}
}

func (s *Server) authRedirect(logger *logrus.Entry, w http.ResponseWriter, r *http.Request, p provider.Provider) {
// Error indicates no cookie, generate nonce
err, nonce := Nonce()
Expand Down
43 changes: 43 additions & 0 deletions internal/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,49 @@ func TestServerAuthCallback(t *testing.T) {
assert.Equal("", fwd.Path, "valid request should be redirected to return url")
}

func TestServerLogout(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
config = newDefaultConfig()

req := newDefaultHttpRequest("/_oauth/logout")
res, _ := doHttpRequest(req, nil)
require.Equal(401, res.StatusCode, "should return a 401")

// Check for cookie
var cookie *http.Cookie
for _, c := range res.Cookies() {
if c.Name == config.CookieName {
cookie = c
}
}
require.NotNil(cookie)
require.Less(cookie.Expires.Local().Unix(), time.Now().Local().Unix()-50, "cookie should have expired")

// Test with redirect
config.LogoutRedirect = "http://redirect/path"
req = newDefaultHttpRequest("/_oauth/logout")
res, _ = doHttpRequest(req, nil)
require.Equal(307, res.StatusCode, "should return a 307")

// Check for cookie
cookie = nil
for _, c := range res.Cookies() {
if c.Name == config.CookieName {
cookie = c
}
}
require.NotNil(cookie)
require.Less(cookie.Expires.Local().Unix(), time.Now().Local().Unix()-50, "cookie should have expired")

fwd, _ := res.Location()
require.NotNil(fwd)
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url")
assert.Equal("redirect", fwd.Host, "valid request should be redirected to return url")
assert.Equal("/path", fwd.Path, "valid request should be redirected to return url")

}

func TestServerDefaultAction(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
Expand Down

0 comments on commit 8b3a950

Please sign in to comment.