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.
This commit is contained in:
Thom Seddon 2020-06-03 14:00:47 +01:00 committed by GitHub
parent 655eddeaf9
commit 8b3a950162
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 1 deletions

View File

@ -2,4 +2,7 @@
format:
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go
.PHONY: format
test:
go test -v ./...
.PHONY: format test

View File

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

View File

@ -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{

View File

@ -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"`

View File

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

View File

@ -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"))
@ -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()

View File

@ -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()