From 77ead9b8a16aa46cc51cd00c0c4ec1a73313f52e Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Thu, 29 Aug 2024 13:13:57 +0100 Subject: [PATCH] Add option to exclude URLs from the authentication. --- .traefik.yml | 9 +++++--- README.md | 9 +++++--- main.go | 24 +++++++++++++++++++- main_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ settings.go | 1 + 5 files changed, 98 insertions(+), 7 deletions(-) diff --git a/.traefik.yml b/.traefik.yml index 9da7166..0ce8122 100644 --- a/.traefik.yml +++ b/.traefik.yml @@ -12,11 +12,14 @@ testData: clientSecret: secret callbackURL: /oauth2/callback logoutURL: /oauth2/logout - scopes: + scopes: # If not provided, default scopes will be used (openid, email, profile) - openid - email - profile sessionEncryptionKey: potato-secret forceHTTPS: false - logLevel: debug - rateLimit: 100 + logLevel: debug # debug, info, warn, error + rateLimit: 100 # Simple rate limiter to prevent brute force attacks + excludedURLs: # Determines the list of URLs which are NOT a subject to authentication + - /login + - /my-public-data diff --git a/README.md b/README.md index ffb9e36..c05b93b 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,15 @@ http: clientSecret: secret callbackURL: /oauth2/callback logoutURL: /oauth2/logout - scopes: + scopes: # If not provided, default scopes will be used (openid, email, profile) - openid - email - profile sessionEncryptionKey: potato-secret forceHTTPS: false - logLevel: info - rateLimit: 100 # 100 requests per minute + logLevel: debug # debug, info, warn, error + rateLimit: 100 # Simple rate limiter to prevent brute force attacks + excludedURLs: # Determines the list of URLs which are NOT a subject to authentication + - /login # covers /login, /login/me, /login/reminder etc. + - /my-public-data ``` diff --git a/main.go b/main.go index 6de6dfa..4250990 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,7 @@ type TraefikOidc struct { redirectURL string tokenVerifier TokenVerifier jwtVerifier JWTVerifier + excludedURLs map[string]struct{} } type ProviderMetadata struct { @@ -178,7 +179,14 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h tokenCache: NewTokenCache(), httpClient: httpClient, logger: NewLogger(config.LogLevel), - redirectURL: "", + excludedURLs: func() map[string]struct{} { + m := make(map[string]struct{}) + for _, url := range config.ExcludedURLs { + m[url] = struct{}{} + } + return m + }(), + redirectURL: "", } t.tokenVerifier = t @@ -211,6 +219,11 @@ func discoverProviderMetadata(providerURL string, httpClient http.Client) (*Prov } func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if t.determineExcludedURL(req.URL.Path) { + t.next.ServeHTTP(rw, req) + return + } + t.scheme = t.determineScheme(req) host := t.determineHost(req) @@ -266,6 +279,15 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) { t.initiateAuthentication(rw, req, session, t.redirectURL) } +func (t *TraefikOidc) determineExcludedURL(currentRequest string) bool { + for excludedURL := range t.excludedURLs { + if strings.HasPrefix(currentRequest, excludedURL) { + return true + } + } + return false +} + func (t *TraefikOidc) determineScheme(req *http.Request) string { if t.forceHTTPS { return "https" diff --git a/main_test.go b/main_test.go index 27b732b..df5104b 100644 --- a/main_test.go +++ b/main_test.go @@ -749,3 +749,65 @@ func (suite *TraefikOidcTestSuite) TestDiscoverProviderMetadata_InvalidURL() { suite.Error(err) suite.Contains(err.Error(), "failed to fetch provider metadata") } + +func (suite *TraefikOidcTestSuite) TestServeHTTP_ExcludedURLs() { + suite.oidc.excludedURLs = map[string]struct{}{ + "/public": {}, + "/api/health": {}, + } + + testCases := []struct { + name string + url string + expectedStatus int + expectedBody string + }{ + { + name: "Excluded URL - public", + url: "http://example.com/public", + expectedStatus: http.StatusOK, + expectedBody: "Public content", + }, + { + name: "Excluded URL - api health", + url: "http://example.com/api/health", + expectedStatus: http.StatusOK, + expectedBody: "API is healthy", + }, + { + name: "Non-excluded URL", + url: "http://example.com/private", + expectedStatus: http.StatusFound, // Expect a redirect to auth + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + req := httptest.NewRequest("GET", tc.url, nil) + rw := httptest.NewRecorder() + + if !strings.HasPrefix(req.URL.Path, "/public") && !strings.HasPrefix(req.URL.Path, "/api/health") { + // For non-excluded URLs, set up session mock + session := sessions.NewSession(suite.mockStore, cookieName) + suite.mockStore.On("Get", req, cookieName).Return(session, nil).Once() + suite.mockStore.On("Save", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + } + + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(tc.expectedBody)) + }) + + suite.oidc.next = nextHandler + + suite.oidc.ServeHTTP(rw, req) + + suite.Equal(tc.expectedStatus, rw.Code) + if tc.expectedStatus == http.StatusOK { + suite.Equal(tc.expectedBody, rw.Body.String()) + } + + suite.mockStore.AssertExpectations(suite.T()) + }) + } +} diff --git a/settings.go b/settings.go index b59d359..3687d17 100644 --- a/settings.go +++ b/settings.go @@ -23,6 +23,7 @@ type Config struct { SessionEncryptionKey string `json:"sessionEncryptionKey"` ForceHTTPS bool `json:"forceHTTPS"` RateLimit int `json:"rateLimit"` + ExcludedURLs []string `json:"excludedURLs"` } func CreateConfig() *Config {