diff --git a/pkg/forwardauth/cookies.go b/pkg/forwardauth/cookies.go index f38647b..25ce666 100644 --- a/pkg/forwardauth/cookies.go +++ b/pkg/forwardauth/cookies.go @@ -24,10 +24,10 @@ func getBaseCookie(options *options.Options) *http.Cookie { } } -func (fw *ForwardAuth) MakeCSRFCookie(w http.ResponseWriter, r *http.Request, options *options.Options, state string) *http.Cookie { +func (fw *ForwardAuth) MakeCSRFCookie(w http.ResponseWriter, r *http.Request, options *options.Options, redirect string, state string) *http.Cookie { cookie := getBaseCookie(options) cookie.Name = "__auth_csrf" - cookie.Value = fmt.Sprintf("%s|%s", fw.GetReturnUri(r), state) + cookie.Value = fmt.Sprintf("%s|%s", redirect, state) cookie.Expires = time.Now().Local().Add(time.Hour) return cookie diff --git a/pkg/forwardauth/forwardauth.go b/pkg/forwardauth/forwardauth.go index b00d531..271b300 100644 --- a/pkg/forwardauth/forwardauth.go +++ b/pkg/forwardauth/forwardauth.go @@ -19,6 +19,12 @@ type ForwardAuth struct { OidcProvider *oidc.Provider OAuth2Config oauth2.Config OidcVefifier *oidc.IDTokenVerifier + + OidcProviderClaims *OidcProviderClaims +} + +type OidcProviderClaims struct { + EndSessionURL string `json:"end_session_endpoint"` } // Claims represents the claims struct which we get from the identity provider @@ -49,8 +55,16 @@ func Create(ctx context.Context, options *options.Options) (*ForwardAuth, error) ClientID: options.ClientID, }) + providerClaims := OidcProviderClaims{} + if err = provider.Claims(&providerClaims); err != nil { + return nil, err + } + return &ForwardAuth{ - OidcProvider: provider, + OidcProvider: provider, + OidcVefifier: verifier, + OidcProviderClaims: &providerClaims, + OAuth2Config: oauth2.Config{ ClientID: options.ClientID, ClientSecret: options.ClientSecret, @@ -62,6 +76,5 @@ func Create(ctx context.Context, options *options.Options) (*ForwardAuth, error) // "openid" is a required scope for OpenID Connect flows. Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, }, - OidcVefifier: verifier, }, nil } diff --git a/pkg/forwardauth/utils.go b/pkg/forwardauth/utils.go index d94fb6a..b882696 100644 --- a/pkg/forwardauth/utils.go +++ b/pkg/forwardauth/utils.go @@ -7,6 +7,7 @@ package forwardauth import ( "fmt" "net/http" + "net/url" ) func (fw *ForwardAuth) GetReturnUri(r *http.Request) string { @@ -16,3 +17,19 @@ func (fw *ForwardAuth) GetReturnUri(r *http.Request) string { return fmt.Sprintf("%s://%s%s", proto, host, path) } + +func (fw *ForwardAuth) GetLogoutUri(redirectURL string, state string) string { + logoutURL, err := url.Parse(fw.OidcProviderClaims.EndSessionURL) + if err != nil { + return "" + } + query := logoutURL.Query() + if redirectURL != "" { + query.Set("post_logout_redirect_uri", redirectURL) + } + if state != "" { + query.Set("state", state) + } + logoutURL.RawQuery = query.Encode() + return logoutURL.String() +} diff --git a/pkg/httphandler/callback.go b/pkg/httphandler/auth_callback.go similarity index 90% rename from pkg/httphandler/callback.go rename to pkg/httphandler/auth_callback.go index 9585ac9..f1ae63f 100644 --- a/pkg/httphandler/callback.go +++ b/pkg/httphandler/auth_callback.go @@ -12,10 +12,10 @@ import ( ) // CallbackHandler returns a handler function which handles the callback from oidc provider -func (root *HttpHandler) callbackHandler(w http.ResponseWriter, r *http.Request, forwardedURI *url.URL) { +func (root *HttpHandler) authCallbackHandler(w http.ResponseWriter, r *http.Request, forwardedURI *url.URL) { logger := logrus.WithFields(logrus.Fields{ "SourceIP": r.Header.Get("X-Forwarded-For"), - "Path": forwardedURI.Path, + "Path": "/auth/resp", }) // check for the csrf cookie diff --git a/pkg/httphandler/root.go b/pkg/httphandler/auth_root.go similarity index 74% rename from pkg/httphandler/root.go rename to pkg/httphandler/auth_root.go index 1c87f82..c42ed74 100644 --- a/pkg/httphandler/root.go +++ b/pkg/httphandler/auth_root.go @@ -5,6 +5,7 @@ This code is licensed under MIT license (see LICENSE for details) package httphandler import ( + "fmt" "net/http" "net/url" @@ -13,11 +14,12 @@ import ( ) // RootHandler returns a handler function which handles all requests to the root -func (root *HttpHandler) rootHandler(w http.ResponseWriter, r *http.Request, forwardedURI *url.URL) { +func (root *HttpHandler) authRootHandler(w http.ResponseWriter, r *http.Request, forwardedURI *url.URL) { + redirect := fmt.Sprintf("%s://%s%s", r.Header.Get("X-Forwarded-Proto"), r.Header.Get("X-Forwarded-Host"), r.Header.Get("X-Forwarded-Uri")) logger := logrus.WithFields(logrus.Fields{ "SourceIP": r.Header.Get("X-Forwarded-For"), - "RequestTarget": root.forwardAuth.GetReturnUri(r), - "Path": forwardedURI.Path, + "RequestTarget": redirect, + "Path": "/", }) claims, err := root.forwardAuth.IsAuthenticated(r.Context(), logger, w, r, root.options) @@ -29,11 +31,11 @@ func (root *HttpHandler) rootHandler(w http.ResponseWriter, r *http.Request, for //http.SetCookie(w, root.forwardAuth.ClearRefreshAuthCookie(root.options)) state := uuid.New().String() - http.SetCookie(w, root.forwardAuth.MakeCSRFCookie(w, r, root.options, state)) + http.SetCookie(w, root.forwardAuth.MakeCSRFCookie(w, r, root.options, redirect, state)) http.Redirect(w, r, root.forwardAuth.OAuth2Config.AuthCodeURL(state), http.StatusTemporaryRedirect) return } w.Header().Set("X-Forwarded-User", claims.Email) - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) } diff --git a/pkg/httphandler/handler.go b/pkg/httphandler/handler.go index 06a70df..1315d2e 100644 --- a/pkg/httphandler/handler.go +++ b/pkg/httphandler/handler.go @@ -5,6 +5,7 @@ This code is licensed under MIT license (see LICENSE for details) package httphandler import ( + "fmt" "net/http" "net/url" @@ -24,7 +25,7 @@ func Create(fw *forwardauth.ForwardAuth, options *options.Options) *HttpHandler } } -func (h *HttpHandler) Entrypoint() func(http.ResponseWriter, *http.Request) { +func (root *HttpHandler) Entrypoint() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { uri, err := url.Parse(r.Header.Get("X-Forwarded-Uri")) switch { @@ -32,12 +33,19 @@ func (h *HttpHandler) Entrypoint() func(http.ResponseWriter, *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return - case uri.Path == h.options.RedirectURL: - h.callbackHandler(w, r, uri) + case uri.Path == root.options.RedirectURL: + root.authCallbackHandler(w, r, uri) + return + + case uri.Path == root.options.LogoutUrl: + root.logoutHandler(w, r, uri) + return + + case uri.Path == fmt.Sprintf("%s/resp", root.options.LogoutUrl): return default: - h.rootHandler(w, r, uri) + root.authRootHandler(w, r, uri) return } } diff --git a/pkg/httphandler/logout_root.go b/pkg/httphandler/logout_root.go new file mode 100644 index 0000000..a8c8b73 --- /dev/null +++ b/pkg/httphandler/logout_root.go @@ -0,0 +1,49 @@ +/* +Copyright (c) 2020 Stefan Kürzeder +This code is licensed under MIT license (see LICENSE for details) +*/ +package httphandler + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +// RootHandler returns a handler function which handles all requests to the root +func (root *HttpHandler) logoutHandler(w http.ResponseWriter, r *http.Request, forwardedURI *url.URL) { + logger := logrus.WithFields(logrus.Fields{ + "SourceIP": r.Header.Get("X-Forwarded-For"), + "RequestTarget": root.forwardAuth.GetReturnUri(r), + "Path": root.options.LogoutUrl, + }) + + // check for the csrf cookie + state, redirect, err := root.forwardAuth.ValidateCSRFCookie(r) + if err != nil { + state := uuid.New().String() + redirect := fmt.Sprintf("%s://%s", r.Header.Get("X-Forwarded-Proto"), r.Header.Get("X-Forwarded-Host")) + + http.SetCookie(w, root.forwardAuth.MakeCSRFCookie(w, r, root.options, redirect, state)) + + responseURL := fmt.Sprintf("https://%s%s/resp", root.options.AuthDomain, root.options.LogoutUrl) + http.Redirect(w, r, root.forwardAuth.GetLogoutUri(responseURL, state), http.StatusTemporaryRedirect) + return + } + + // verify the state + if forwardedURI.Query().Get("state") != state { + http.Error(w, "state did not match", http.StatusBadRequest) + return + } + + // Clear the auth information + logger.Info("Destroying auth cookie.") + http.SetCookie(w, root.forwardAuth.ClearAuthCookie(root.options)) + + // Redirect to the base + http.Redirect(w, r, redirect, http.StatusFound) +} diff --git a/pkg/options/options.go b/pkg/options/options.go index 12a677a..3653e68 100644 --- a/pkg/options/options.go +++ b/pkg/options/options.go @@ -18,6 +18,7 @@ type Options struct { CookieDomain string `env:"COOKIE_DOMAIN"` Port int `env:"PORT" envDefault:"4181"` RedirectURL string `env:"REDIRECT_URL" envDefault:"/auth/resp"` + LogoutUrl string `env:"LOGOUT_URL" envDefault:"/auth/logout"` } // LoadOptions parses the environment vars and the options