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:
parent
655eddeaf9
commit
8b3a950162
5
Makefile
5
Makefile
@ -2,4 +2,7 @@
|
|||||||
format:
|
format:
|
||||||
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go
|
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go
|
||||||
|
|
||||||
.PHONY: format
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
.PHONY: format test
|
||||||
|
14
README.md
14
README.md
@ -35,6 +35,7 @@ A minimal forward authentication service that provides OAuth/SSO login and authe
|
|||||||
- [Operation Modes](#operation-modes)
|
- [Operation Modes](#operation-modes)
|
||||||
- [Overlay Mode](#overlay-mode)
|
- [Overlay Mode](#overlay-mode)
|
||||||
- [Auth Host Mode](#auth-host-mode)
|
- [Auth Host Mode](#auth-host-mode)
|
||||||
|
- [Logging Out](#logging-out)
|
||||||
- [Copyright](#copyright)
|
- [Copyright](#copyright)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
@ -136,6 +137,7 @@ Application Options:
|
|||||||
--default-provider=[google|oidc] Default provider (default: google) [$DEFAULT_PROVIDER]
|
--default-provider=[google|oidc] Default provider (default: google) [$DEFAULT_PROVIDER]
|
||||||
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
|
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
|
||||||
--lifetime= Lifetime in seconds (default: 43200) [$LIFETIME]
|
--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]
|
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
|
||||||
--secret= Secret used for signing (required) [$SECRET]
|
--secret= Secret used for signing (required) [$SECRET]
|
||||||
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
|
--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)
|
Default: `43200` (12 hours)
|
||||||
|
|
||||||
|
- `logout-redirect`
|
||||||
|
|
||||||
|
When set, users will be redirected to this URL following logout.
|
||||||
|
|
||||||
- `url-path`
|
- `url-path`
|
||||||
|
|
||||||
Customise the path that this service uses to handle the callback following authentication.
|
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.
|
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
|
## Copyright
|
||||||
|
|
||||||
2018 Thom Seddon
|
2018 Thom Seddon
|
||||||
|
@ -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)
|
// MakeCSRFCookie makes a csrf cookie (used during login only)
|
||||||
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
|
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
|
@ -34,6 +34,7 @@ type Config struct {
|
|||||||
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" description:"Default provider"`
|
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"`
|
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"`
|
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"`
|
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:"-"`
|
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"`
|
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
|
||||||
|
@ -33,6 +33,7 @@ func TestConfigDefaults(t *testing.T) {
|
|||||||
assert.Equal("google", c.DefaultProvider)
|
assert.Equal("google", c.DefaultProvider)
|
||||||
assert.Len(c.Domains, 0)
|
assert.Len(c.Domains, 0)
|
||||||
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
|
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
|
||||||
|
assert.Equal("", c.LogoutRedirect)
|
||||||
assert.Equal("/_oauth", c.Path)
|
assert.Equal("/_oauth", c.Path)
|
||||||
assert.Len(c.Whitelist, 0)
|
assert.Len(c.Whitelist, 0)
|
||||||
|
|
||||||
|
@ -41,6 +41,9 @@ func (s *Server) buildRoutes() {
|
|||||||
// Add callback handler
|
// Add callback handler
|
||||||
s.router.Handle(config.Path, s.AuthCallbackHandler())
|
s.router.Handle(config.Path, s.AuthCallbackHandler())
|
||||||
|
|
||||||
|
// Add logout handler
|
||||||
|
s.router.Handle(config.Path+"/logout", s.LogoutHandler())
|
||||||
|
|
||||||
// Add a default handler
|
// Add a default handler
|
||||||
if config.DefaultAction == "allow" {
|
if config.DefaultAction == "allow" {
|
||||||
s.router.NewRoute().Handler(s.AllowHandler("default"))
|
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) {
|
func (s *Server) authRedirect(logger *logrus.Entry, w http.ResponseWriter, r *http.Request, p provider.Provider) {
|
||||||
// Error indicates no cookie, generate nonce
|
// Error indicates no cookie, generate nonce
|
||||||
err, nonce := Nonce()
|
err, nonce := Nonce()
|
||||||
|
@ -170,6 +170,49 @@ func TestServerAuthCallback(t *testing.T) {
|
|||||||
assert.Equal("", fwd.Path, "valid request should be redirected to return url")
|
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) {
|
func TestServerDefaultAction(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
config = newDefaultConfig()
|
config = newDefaultConfig()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user