mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-21 23:43:27 +01:00
Add tests for OIDC
This commit is contained in:
parent
9f8f7bb45e
commit
dd5e3ee7ee
23
security/basic_test.go
Normal file
23
security/basic_test.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBasicConfig_IsValid(t *testing.T) {
|
||||||
|
basicConfig := &BasicConfig{
|
||||||
|
Username: "admin",
|
||||||
|
PasswordSha512Hash: Sha512("test"),
|
||||||
|
}
|
||||||
|
if !basicConfig.isValid() {
|
||||||
|
t.Error("basicConfig should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicConfig_IsValidWhenPasswordIsInvalid(t *testing.T) {
|
||||||
|
basicConfig := &BasicConfig{
|
||||||
|
Username: "admin",
|
||||||
|
PasswordSha512Hash: "",
|
||||||
|
}
|
||||||
|
if basicConfig.isValid() {
|
||||||
|
t.Error("basicConfig shouldn't have been valid")
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,116 @@
|
|||||||
package security
|
package security
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
func TestBasicConfig_IsValid(t *testing.T) {
|
"github.com/gorilla/mux"
|
||||||
basicConfig := &BasicConfig{
|
"golang.org/x/oauth2"
|
||||||
Username: "admin",
|
)
|
||||||
PasswordSha512Hash: Sha512("test"),
|
|
||||||
|
func TestConfig_IsValid(t *testing.T) {
|
||||||
|
c := &Config{
|
||||||
|
Basic: nil,
|
||||||
|
OIDC: nil,
|
||||||
}
|
}
|
||||||
if !basicConfig.isValid() {
|
if c.IsValid() {
|
||||||
t.Error("basicConfig should've been valid")
|
t.Error("expected empty config to be valid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicConfig_IsValidWhenPasswordIsInvalid(t *testing.T) {
|
func TestConfig_ApplySecurityMiddleware(t *testing.T) {
|
||||||
basicConfig := &BasicConfig{
|
///////////
|
||||||
Username: "admin",
|
// BASIC //
|
||||||
PasswordSha512Hash: "",
|
///////////
|
||||||
|
c := &Config{Basic: &BasicConfig{
|
||||||
|
Username: "john.doe",
|
||||||
|
PasswordSha512Hash: "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22",
|
||||||
|
}}
|
||||||
|
api := mux.NewRouter()
|
||||||
|
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
c.ApplySecurityMiddleware(api)
|
||||||
|
// Try to access the route without basic auth
|
||||||
|
request, _ := http.NewRequest("GET", "/test", nil)
|
||||||
|
responseRecorder := httptest.NewRecorder()
|
||||||
|
api.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusUnauthorized {
|
||||||
|
t.Error("expected code to be 401, but was", responseRecorder.Code)
|
||||||
}
|
}
|
||||||
if basicConfig.isValid() {
|
// Try again, but with basic auth
|
||||||
t.Error("basicConfig shouldn't have been valid")
|
request, _ = http.NewRequest("GET", "/test", nil)
|
||||||
|
responseRecorder = httptest.NewRecorder()
|
||||||
|
request.SetBasicAuth("john.doe", "hunter2")
|
||||||
|
api.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusOK {
|
||||||
|
t.Error("expected code to be 200, but was", responseRecorder.Code)
|
||||||
|
}
|
||||||
|
//////////
|
||||||
|
// OIDC //
|
||||||
|
//////////
|
||||||
|
api = mux.NewRouter()
|
||||||
|
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
c.OIDC = &OIDCConfig{
|
||||||
|
IssuerURL: "https://sso.gatus.io/",
|
||||||
|
RedirectURL: "http://localhost:80/authorization-code/callback",
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
AllowedSubjects: []string{"user1@example.com"},
|
||||||
|
oauth2Config: oauth2.Config{},
|
||||||
|
verifier: nil,
|
||||||
|
}
|
||||||
|
c.Basic = nil
|
||||||
|
c.ApplySecurityMiddleware(api)
|
||||||
|
// Try without any session cookie
|
||||||
|
request, _ = http.NewRequest("GET", "/test", nil)
|
||||||
|
responseRecorder = httptest.NewRecorder()
|
||||||
|
api.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusUnauthorized {
|
||||||
|
t.Error("expected code to be 401, but was", responseRecorder.Code)
|
||||||
|
}
|
||||||
|
// Try with a session cookie
|
||||||
|
request, _ = http.NewRequest("GET", "/test", nil)
|
||||||
|
request.AddCookie(&http.Cookie{Name: "session", Value: "123"})
|
||||||
|
responseRecorder = httptest.NewRecorder()
|
||||||
|
api.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusUnauthorized {
|
||||||
|
t.Error("expected code to be 401, but was", responseRecorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_RegisterHandlers(t *testing.T) {
|
||||||
|
c := &Config{}
|
||||||
|
router := mux.NewRouter()
|
||||||
|
c.RegisterHandlers(router)
|
||||||
|
// Try to access the OIDC handler. This should fail, because the security config doesn't have OIDC
|
||||||
|
request, _ := http.NewRequest("GET", "/oidc/login", nil)
|
||||||
|
responseRecorder := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusNotFound {
|
||||||
|
t.Error("expected code to be 404, but was", responseRecorder.Code)
|
||||||
|
}
|
||||||
|
// Set an empty OIDC config. This should fail, because the IssuerURL is required.
|
||||||
|
c.OIDC = &OIDCConfig{}
|
||||||
|
if err := c.RegisterHandlers(router); err == nil {
|
||||||
|
t.Fatal("expected an error, but got none")
|
||||||
|
}
|
||||||
|
// Set the OIDC config and try again
|
||||||
|
c.OIDC = &OIDCConfig{
|
||||||
|
IssuerURL: "https://sso.gatus.io/",
|
||||||
|
RedirectURL: "http://localhost:80/authorization-code/callback",
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
AllowedSubjects: []string{"user1@example.com"},
|
||||||
|
}
|
||||||
|
if err := c.RegisterHandlers(router); err != nil {
|
||||||
|
t.Fatal("expected no error, but got", err)
|
||||||
|
}
|
||||||
|
request, _ = http.NewRequest("GET", "/oidc/login", nil)
|
||||||
|
responseRecorder = httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusFound {
|
||||||
|
t.Error("expected code to be 302, but was", responseRecorder.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
package security
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler takes care of security for a given handler with the given security configuration
|
|
||||||
func Handler(handler http.HandlerFunc, security *Config) http.HandlerFunc {
|
|
||||||
if security == nil {
|
|
||||||
return handler
|
|
||||||
} else if security.Basic != nil {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
usernameEntered, passwordEntered, ok := r.BasicAuth()
|
|
||||||
if !ok || usernameEntered != security.Basic.Username || Sha512(passwordEntered) != strings.ToLower(security.Basic.PasswordSha512Hash) {
|
|
||||||
w.Header().Set("WWW-Authenticate", "Basic")
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
_, _ = w.Write([]byte("Unauthorized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handler(w, r)
|
|
||||||
}
|
|
||||||
} else if security.OIDC != nil {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// TODO: Check if the user is authenticated, and redirect to /login if they're not?
|
|
||||||
handler(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return handler
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package security
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mockHandler(writer http.ResponseWriter, _ *http.Request) {
|
|
||||||
writer.WriteHeader(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerWhenNotAuthenticated(t *testing.T) {
|
|
||||||
handler := Handler(mockHandler, &Config{Basic: &BasicConfig{
|
|
||||||
Username: "john.doe",
|
|
||||||
PasswordSha512Hash: "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22",
|
|
||||||
}})
|
|
||||||
request, _ := http.NewRequest("GET", "/api/v1/results", nil)
|
|
||||||
responseRecorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler.ServeHTTP(responseRecorder, request)
|
|
||||||
|
|
||||||
if responseRecorder.Code != http.StatusUnauthorized {
|
|
||||||
t.Error("Expected code to be 401, but was", responseRecorder.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerWhenAuthenticated(t *testing.T) {
|
|
||||||
handler := Handler(mockHandler, &Config{Basic: &BasicConfig{
|
|
||||||
Username: "john.doe",
|
|
||||||
PasswordSha512Hash: "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22",
|
|
||||||
}})
|
|
||||||
request, _ := http.NewRequest("GET", "/api/v1/results", nil)
|
|
||||||
request.SetBasicAuth("john.doe", "hunter2")
|
|
||||||
responseRecorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler.ServeHTTP(responseRecorder, request)
|
|
||||||
|
|
||||||
if responseRecorder.Code != http.StatusOK {
|
|
||||||
t.Error("Expected code to be 200, but was", responseRecorder.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlerWhenAuthenticatedWithBadCredentials(t *testing.T) {
|
|
||||||
handler := Handler(mockHandler, &Config{Basic: &BasicConfig{
|
|
||||||
Username: "john.doe",
|
|
||||||
PasswordSha512Hash: "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22",
|
|
||||||
}})
|
|
||||||
request, _ := http.NewRequest("GET", "/api/v1/results", nil)
|
|
||||||
request.SetBasicAuth("john.doe", "bad-password")
|
|
||||||
responseRecorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler.ServeHTTP(responseRecorder, request)
|
|
||||||
|
|
||||||
if responseRecorder.Code != http.StatusUnauthorized {
|
|
||||||
t.Error("Expected code to be 401, but was", responseRecorder.Code)
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gocache"
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@ -140,5 +139,3 @@ func (c *OIDCConfig) setSessionCookie(w http.ResponseWriter, idToken *oidc.IDTok
|
|||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessions = gocache.NewCache()
|
|
||||||
|
70
security/oidc_test.go
Normal file
70
security/oidc_test.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOIDCConfig_isValid(t *testing.T) {
|
||||||
|
c := &OIDCConfig{
|
||||||
|
IssuerURL: "https://sso.gatus.io/",
|
||||||
|
RedirectURL: "http://localhost:80/authorization-code/callback",
|
||||||
|
ClientID: "client-id",
|
||||||
|
ClientSecret: "client-secret",
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
AllowedSubjects: []string{"user1@example.com"},
|
||||||
|
}
|
||||||
|
if !c.isValid() {
|
||||||
|
t.Error("OIDCConfig should be valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOIDCConfig_callbackHandler(t *testing.T) {
|
||||||
|
c := &OIDCConfig{
|
||||||
|
IssuerURL: "https://sso.gatus.io/",
|
||||||
|
RedirectURL: "http://localhost:80/authorization-code/callback",
|
||||||
|
ClientID: "client-id",
|
||||||
|
ClientSecret: "client-secret",
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
AllowedSubjects: []string{"user1@example.com"},
|
||||||
|
}
|
||||||
|
if err := c.initialize(); err != nil {
|
||||||
|
t.Fatal("expected no error, but got", err)
|
||||||
|
}
|
||||||
|
// Try with no state cookie
|
||||||
|
request, _ := http.NewRequest("GET", "/authorization-code/callback", nil)
|
||||||
|
responseRecorder := httptest.NewRecorder()
|
||||||
|
c.callbackHandler(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusBadRequest {
|
||||||
|
t.Error("expected code to be 400, but was", responseRecorder.Code)
|
||||||
|
}
|
||||||
|
// Try with state cookie
|
||||||
|
request, _ = http.NewRequest("GET", "/authorization-code/callback", nil)
|
||||||
|
request.AddCookie(&http.Cookie{Name: cookieNameState, Value: "fake-state"})
|
||||||
|
responseRecorder = httptest.NewRecorder()
|
||||||
|
c.callbackHandler(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusBadRequest {
|
||||||
|
t.Error("expected code to be 400, but was", responseRecorder.Code)
|
||||||
|
}
|
||||||
|
// Try with state cookie and state query parameter
|
||||||
|
request, _ = http.NewRequest("GET", "/authorization-code/callback?state=fake-state", nil)
|
||||||
|
request.AddCookie(&http.Cookie{Name: cookieNameState, Value: "fake-state"})
|
||||||
|
responseRecorder = httptest.NewRecorder()
|
||||||
|
c.callbackHandler(responseRecorder, request)
|
||||||
|
// Exchange should fail, so 500.
|
||||||
|
if responseRecorder.Code != http.StatusInternalServerError {
|
||||||
|
t.Error("expected code to be 500, but was", responseRecorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOIDCConfig_setSessionCookie(t *testing.T) {
|
||||||
|
c := &OIDCConfig{}
|
||||||
|
responseRecorder := httptest.NewRecorder()
|
||||||
|
c.setSessionCookie(responseRecorder, &oidc.IDToken{Subject: "test@example.com"})
|
||||||
|
if len(responseRecorder.Result().Cookies()) == 0 {
|
||||||
|
t.Error("expected cookie to be set")
|
||||||
|
}
|
||||||
|
}
|
5
security/sessions.go
Normal file
5
security/sessions.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import "github.com/TwiN/gocache/v2"
|
||||||
|
|
||||||
|
var sessions = gocache.NewCache() // TODO: Move this to storage
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user