#205: Work on supporting OpenID Connect for auth

This commit is contained in:
TwiN
2021-12-31 00:10:54 -05:00
parent 4ab5724fc1
commit be9087bee3
123 changed files with 27693 additions and 49 deletions

15
security/basic.go Normal file
View File

@ -0,0 +1,15 @@
package security
// BasicConfig is the configuration for Basic authentication
type BasicConfig struct {
// Username is the name which will need to be used for a successful authentication
Username string `yaml:"username"`
// PasswordSha512Hash is the SHA512 hash of the password which will need to be used for a successful authentication
PasswordSha512Hash string `yaml:"password-sha512"`
}
// isValid returns whether the basic security configuration is valid or not
func (c *BasicConfig) isValid() bool {
return len(c.Username) > 0 && len(c.PasswordSha512Hash) == 128
}

View File

@ -1,9 +1,19 @@
package security
import (
"net/http"
"strings"
"github.com/TwiN/g8"
"github.com/gorilla/mux"
)
const (
cookieNameState = "gatus_state"
cookieNameNonce = "gatus_nonce"
cookieNameSession = "gatus_session"
)
// Config is the security configuration for Gatus
type Config struct {
Basic *BasicConfig `yaml:"basic,omitempty"`
@ -21,22 +31,44 @@ func (c *Config) RegisterHandlers(router *mux.Router) error {
if err := c.OIDC.initialize(); err != nil {
return err
}
router.HandleFunc("/login", c.OIDC.loginHandler)
router.HandleFunc("/oidc/login", c.OIDC.loginHandler)
router.HandleFunc("/authorization-code/callback", c.OIDC.callbackHandler)
}
return nil
}
// BasicConfig is the configuration for Basic authentication
type BasicConfig struct {
// Username is the name which will need to be used for a successful authentication
Username string `yaml:"username"`
// PasswordSha512Hash is the SHA512 hash of the password which will need to be used for a successful authentication
PasswordSha512Hash string `yaml:"password-sha512"`
}
// isValid returns whether the basic security configuration is valid or not
func (c *BasicConfig) isValid() bool {
return len(c.Username) > 0 && len(c.PasswordSha512Hash) == 128
func (c *Config) ApplySecurityMiddleware(api *mux.Router) {
if c.OIDC != nil {
// We're going to use g8 for session handling
clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
if _, exists := sessions.Get(token); exists {
return g8.NewClient(token)
}
return nil
})
customTokenExtractorFunc := func(request *http.Request) string {
sessionCookie, err := request.Cookie(cookieNameSession)
if err != nil {
return ""
}
return sessionCookie.Value
}
// TODO: g8: Add a way to update cookie after? would need the writer
authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)
gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
api.Use(gate.Protect)
} else if c.Basic != nil {
api.Use(func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
usernameEntered, passwordEntered, ok := r.BasicAuth()
if !ok || usernameEntered != c.Basic.Username || Sha512(passwordEntered) != strings.ToLower(c.Basic.PasswordSha512Hash) {
w.Header().Set("WWW-Authenticate", "Basic")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("Unauthorized"))
return
}
handler.ServeHTTP(w, r)
})
})
}
}

View File

@ -2,9 +2,12 @@ package security
import (
"context"
"log"
"net/http"
"strings"
"time"
"github.com/TwiN/gocache"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/google/uuid"
"golang.org/x/oauth2"
@ -12,11 +15,12 @@ import (
// OIDCConfig is the configuration for OIDC authentication
type OIDCConfig struct {
IssuerURL string `yaml:"issuer-url"` // e.g. https://dev-12345678.okta.com
RedirectURL string `yaml:"redirect-url"` // e.g. http://localhost:8080/authorization-code/callback
ClientID string `yaml:"client-id"`
ClientSecret string `yaml:"client-secret"`
Scopes []string `yaml:"scopes"` // e.g. [openid]
IssuerURL string `yaml:"issuer-url"` // e.g. https://dev-12345678.okta.com
RedirectURL string `yaml:"redirect-url"` // e.g. http://localhost:8080/authorization-code/callback
ClientID string `yaml:"client-id"`
ClientSecret string `yaml:"client-secret"`
Scopes []string `yaml:"scopes"` // e.g. ["openid"]
AllowedSubjects []string `yaml:"allowed-subjects"` // e.g. ["user1@example.com"]. If empty, all subjects are allowed
oauth2Config oauth2.Config
verifier *oidc.IDTokenVerifier
@ -47,25 +51,32 @@ func (c *OIDCConfig) initialize() error {
func (c *OIDCConfig) loginHandler(w http.ResponseWriter, r *http.Request) {
state, nonce := uuid.NewString(), uuid.NewString()
http.SetCookie(w, &http.Cookie{
Name: "state",
Name: cookieNameState,
Value: state,
Path: "/",
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
SameSite: http.SameSiteLaxMode,
HttpOnly: true,
})
http.SetCookie(w, &http.Cookie{
Name: "nonce",
Name: cookieNameNonce,
Value: nonce,
Path: "/",
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
SameSite: http.SameSiteLaxMode,
HttpOnly: true,
})
http.Redirect(w, r, c.oauth2Config.AuthCodeURL(state, oidc.Nonce(nonce)), http.StatusFound)
}
func (c *OIDCConfig) callbackHandler(w http.ResponseWriter, r *http.Request) {
// Check if there's an error
if len(r.URL.Query().Get("error")) > 0 {
http.Error(w, r.URL.Query().Get("error")+": "+r.URL.Query().Get("error_description"), http.StatusBadRequest)
return
}
// Ensure that the state has the expected value
state, err := r.Cookie("state")
state, err := r.Cookie(cookieNameState)
if err != nil {
http.Error(w, "state not found", http.StatusBadRequest)
return
@ -91,7 +102,7 @@ func (c *OIDCConfig) callbackHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Validate nonce
nonce, err := r.Cookie("nonce")
nonce, err := r.Cookie(cookieNameNonce)
if err != nil {
http.Error(w, "nonce not found", http.StatusBadRequest)
return
@ -100,5 +111,34 @@ func (c *OIDCConfig) callbackHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "nonce did not match", http.StatusBadRequest)
return
}
http.Redirect(w, r, "/", http.StatusFound)
if len(c.AllowedSubjects) == 0 {
// If there's no allowed subjects, all subjects are allowed.
c.setSessionCookie(w, idToken)
http.Redirect(w, r, "/", http.StatusFound)
return
}
for _, subject := range c.AllowedSubjects {
if strings.ToLower(subject) == strings.ToLower(idToken.Subject) {
c.setSessionCookie(w, idToken)
http.Redirect(w, r, "/", http.StatusFound)
return
}
}
log.Println("user is not in the list of allowed subjects")
http.Redirect(w, r, "/login?error=access_denied", http.StatusFound)
}
func (c *OIDCConfig) setSessionCookie(w http.ResponseWriter, idToken *oidc.IDToken) {
// At this point, the user has been confirmed. All that's left to do is create a session.
sessionID := uuid.NewString()
sessions.SetWithTTL(sessionID, idToken.Subject, time.Hour)
http.SetCookie(w, &http.Cookie{
Name: cookieNameSession,
Value: sessionID,
Path: "/",
MaxAge: int(time.Hour.Seconds()),
SameSite: http.SameSiteStrictMode,
})
}
var sessions = gocache.NewCache()