#205: Start working on adding support for IODC

This commit is contained in:
TwiN
2021-12-14 23:20:43 -05:00
parent 1777d69495
commit 45a47940ad
7 changed files with 171 additions and 15 deletions

View File

@ -1,13 +1,30 @@
package security
import (
"github.com/gorilla/mux"
)
// Config is the security configuration for Gatus
type Config struct {
Basic *BasicConfig `yaml:"basic"`
Basic *BasicConfig `yaml:"basic,omitempty"`
OIDC *OIDCConfig `yaml:"oidc,omitempty"`
}
// IsValid returns whether the security configuration is valid or not
func (c *Config) IsValid() bool {
return c.Basic != nil && c.Basic.IsValid()
return (c.Basic != nil && c.Basic.isValid()) || (c.OIDC != nil && c.OIDC.isValid())
}
// RegisterHandlers registers all handlers required based on the security configuration
func (c *Config) RegisterHandlers(router *mux.Router) error {
if c.OIDC != nil {
if err := c.OIDC.initialize(); err != nil {
return err
}
router.HandleFunc("/login", c.OIDC.loginHandler)
router.HandleFunc("/authorization-code/callback", c.OIDC.callbackHandler)
}
return nil
}
// BasicConfig is the configuration for Basic authentication
@ -19,7 +36,7 @@ type BasicConfig struct {
PasswordSha512Hash string `yaml:"password-sha512"`
}
// IsValid returns whether the basic security configuration is valid or not
func (c *BasicConfig) IsValid() bool {
// 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

@ -7,7 +7,7 @@ func TestBasicConfig_IsValid(t *testing.T) {
Username: "admin",
PasswordSha512Hash: Sha512("test"),
}
if !basicConfig.IsValid() {
if !basicConfig.isValid() {
t.Error("basicConfig should've been valid")
}
}
@ -17,7 +17,7 @@ func TestBasicConfig_IsValidWhenPasswordIsInvalid(t *testing.T) {
Username: "admin",
PasswordSha512Hash: "",
}
if basicConfig.IsValid() {
if basicConfig.isValid() {
t.Error("basicConfig shouldn't have been valid")
}
}

View File

@ -7,14 +7,24 @@ import (
// Handler takes care of security for a given handler with the given security configuration
func Handler(handler http.HandlerFunc, security *Config) http.HandlerFunc {
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
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)
}
handler(w, r)
}
return handler
}

104
security/oidc.go Normal file
View File

@ -0,0 +1,104 @@
package security
import (
"context"
"net/http"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/google/uuid"
"golang.org/x/oauth2"
)
// 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]
oauth2Config oauth2.Config
verifier *oidc.IDTokenVerifier
}
// isValid returns whether the basic security configuration is valid or not
func (c *OIDCConfig) isValid() bool {
return len(c.IssuerURL) > 0 && len(c.RedirectURL) > 0 && len(c.ClientID) > 0 && len(c.ClientSecret) > 0 && len(c.Scopes) > 0
}
func (c *OIDCConfig) initialize() error {
provider, err := oidc.NewProvider(context.Background(), c.IssuerURL)
if err != nil {
return err
}
c.verifier = provider.Verifier(&oidc.Config{ClientID: c.ClientID})
// Configure an OpenID Connect aware OAuth2 client.
c.oauth2Config = oauth2.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Scopes: c.Scopes,
RedirectURL: c.RedirectURL,
Endpoint: provider.Endpoint(),
}
return nil
}
func (c *OIDCConfig) loginHandler(w http.ResponseWriter, r *http.Request) {
state, nonce := uuid.NewString(), uuid.NewString()
http.SetCookie(w, &http.Cookie{
Name: "state",
Value: state,
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
HttpOnly: true,
})
http.SetCookie(w, &http.Cookie{
Name: "nonce",
Value: nonce,
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
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) {
// Ensure that the state has the expected value
state, err := r.Cookie("state")
if err != nil {
http.Error(w, "state not found", http.StatusBadRequest)
return
}
if r.URL.Query().Get("state") != state.Value {
http.Error(w, "state did not match", http.StatusBadRequest)
return
}
// Validate token
oauth2Token, err := c.oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Error exchanging token: "+err.Error(), http.StatusInternalServerError)
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "Missing 'id_token' in oauth2 token", http.StatusInternalServerError)
return
}
idToken, err := c.verifier.Verify(r.Context(), rawIDToken)
if err != nil {
http.Error(w, "Failed to verify id_token: "+err.Error(), http.StatusInternalServerError)
return
}
// Validate nonce
nonce, err := r.Cookie("nonce")
if err != nil {
http.Error(w, "nonce not found", http.StatusBadRequest)
return
}
if idToken.Nonce != nonce.Value {
http.Error(w, "nonce did not match", http.StatusBadRequest)
return
}
http.Redirect(w, r, "/", http.StatusFound)
}