Add tests for OIDC

This commit is contained in:
TwiN 2021-12-31 20:07:19 -05:00
parent 9f8f7bb45e
commit dd5e3ee7ee
10 changed files with 207 additions and 107 deletions

23
security/basic_test.go Normal file
View 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")
}
}

View File

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

View File

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

View File

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

View File

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