Explorar o código

feature: OAuth2 early support for CertBundles (#474) and Usergroups (#477) (#485)

James Read hai 1 ano
pai
achega
f0b1cefb72
Modificáronse 2 ficheiros con 100 adicións e 22 borrados
  1. 14 10
      internal/config/config.go
  2. 86 12
      internal/httpservers/restapi_auth_oauth2.go

+ 14 - 10
internal/config/config.go

@@ -152,16 +152,20 @@ type LocalUser struct {
 }
 }
 
 
 type OAuth2Provider struct {
 type OAuth2Provider struct {
-	Name          string
-	Title         string
-	ClientID      string
-	ClientSecret  string
-	Icon          string
-	Scopes        []string
-	AuthUrl       string
-	TokenUrl      string
-	WhoamiUrl     string
-	UsernameField string
+	Name               string
+	Title              string
+	ClientID           string
+	ClientSecret       string
+	Icon               string
+	Scopes             []string
+	AuthUrl            string
+	TokenUrl           string
+	WhoamiUrl          string
+	UsernameField      string
+	UserGroupField     string
+	InsecureSkipVerify bool
+	CallbackTimeout    int
+	CertBundlePath     string
 }
 }
 
 
 type NavigationLink struct {
 type NavigationLink struct {

+ 86 - 12
internal/httpservers/restapi_auth_oauth2.go

@@ -3,6 +3,8 @@ package httpservers
 import (
 import (
 	"context"
 	"context"
 	"crypto/rand"
 	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
@@ -12,6 +14,7 @@ import (
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 	"time"
 	"time"
+	"os"
 )
 )
 
 
 var (
 var (
@@ -93,7 +96,7 @@ func randString(nByte int) (string, error) {
 	return base64.URLEncoding.EncodeToString(b), nil
 	return base64.URLEncoding.EncodeToString(b), nil
 }
 }
 
 
-func setOauthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
+func setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
 	cookie := &http.Cookie{
 	cookie := &http.Cookie{
 		Name:     name,
 		Name:     name,
 		Value:    value,
 		Value:    value,
@@ -129,7 +132,7 @@ func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
 		Username:       "",
 		Username:       "",
 	}
 	}
 
 
-	setOauthCallbackCookie(w, r, "olivetin-sid-oauth", state)
+	setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
 
 
 	log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
 	log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
 
 
@@ -165,6 +168,44 @@ func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2St
 	return registeredState, state, true
 	return registeredState, state, true
 }
 }
 
 
+type HttpClientSettings struct {
+	Transport 	 *http.Transport
+	Timeout         time.Duration
+}
+
+func getOAuth2HttpClient(providerConfig *config.OAuth2Provider) *HttpClientSettings {
+	config := &HttpClientSettings{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: providerConfig.InsecureSkipVerify},
+		},
+		Timeout:         time.Duration(min(3, providerConfig.CallbackTimeout)) * time.Second,
+	}
+
+	if providerConfig.CertBundlePath != "" {
+		config.Transport.TLSClientConfig.RootCAs = getOAuthCertBundle(providerConfig)
+	}
+
+	return config
+}
+
+func getOAuthCertBundle(providerConfig *config.OAuth2Provider) *x509.CertPool {
+	caCert, err := os.ReadFile(providerConfig.CertBundlePath)
+
+	if err != nil {
+		log.Errorf("OAuth2 Cert Bundle - failed to read file: %v", err)
+
+		return nil
+	}
+
+	caCertPool := x509.NewCertPool()
+
+	if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
+		log.Errorf("OAuth2 Cert Bundle - failed to append certificates: %v", err)
+	}
+
+	return caCertPool
+}
+
 func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
 func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
 	log.Infof("OAuth2 Callback received")
 	log.Infof("OAuth2 Callback received")
 
 
@@ -181,9 +222,17 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
 		"token-code": code,
 		"token-code": code,
 	}).Debug("OAuth2 Token Code")
 	}).Debug("OAuth2 Token Code")
 
 
-	httpClient := &http.Client{Timeout: 2 * time.Second}
+	providerConfig := cfg.AuthOAuth2Providers[registeredState.providerName]
+
+	clientSettings := getOAuth2HttpClient(providerConfig)
+
+	exchangeClient := &http.Client{
+		Transport: clientSettings.Transport,
+		Timeout: clientSettings.Timeout,
+	}
+
 	ctx := context.Background()
 	ctx := context.Background()
-	ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
+	ctx = context.WithValue(ctx, oauth2.HTTPClient, exchangeClient)
 
 
 	tok, err := registeredState.providerConfig.Exchange(ctx, code)
 	tok, err := registeredState.providerConfig.Exchange(ctx, code)
 
 
@@ -193,11 +242,18 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	client := registeredState.providerConfig.Client(ctx, tok)
+	userInfoClient := &http.Client{
+		Transport: &oauth2.Transport{
+			Source: registeredState.providerConfig.TokenSource(ctx, tok),
+			Base: clientSettings.Transport,
+		},
+		Timeout: clientSettings.Timeout,
+	}
 
 
-	username := getUsername(client, cfg.AuthOAuth2Providers[registeredState.providerName])
+	userinfo := getUserInfo(userInfoClient, cfg.AuthOAuth2Providers[registeredState.providerName])
 
 
-	registeredStates[state].Username = username
+	registeredStates[state].Username = userinfo.Username
+	registeredStates[state].Usergroup = userinfo.Usergroup
 
 
 	for k, v := range registeredStates {
 	for k, v := range registeredStates {
 		log.Debugf("states: %+v %+v", k, v)
 		log.Debugf("states: %+v %+v", k, v)
@@ -213,12 +269,19 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
 	w.Write([]byte(loginMessage))
 	w.Write([]byte(loginMessage))
 }
 }
 
 
-func getUsername(client *http.Client, provider *config.OAuth2Provider) string {
+type UserInfo struct {
+	Username string
+	Usergroup string
+}
+
+func getUserInfo(client *http.Client, provider *config.OAuth2Provider) *UserInfo {
+	ret := &UserInfo{}
+
 	res, err := client.Get(provider.WhoamiUrl)
 	res, err := client.Get(provider.WhoamiUrl)
 
 
 	if res.StatusCode != http.StatusOK {
 	if res.StatusCode != http.StatusOK {
 		log.Errorf("Failed to get user data: %v", res.StatusCode)
 		log.Errorf("Failed to get user data: %v", res.StatusCode)
-		return ""
+		return ret
 	}
 	}
 
 
 	defer res.Body.Close()
 	defer res.Body.Close()
@@ -232,18 +295,29 @@ func getUsername(client *http.Client, provider *config.OAuth2Provider) string {
 	if err != nil {
 	if err != nil {
 		log.Errorf("Failed to unmarshal user data: %v", err)
 		log.Errorf("Failed to unmarshal user data: %v", err)
 
 
+		return ret
+	}
+
+	ret.Username = getDataField(userData, provider.UsernameField)
+	ret.Usergroup = getDataField(userData, provider.UserGroupField)
+
+	return ret
+}
+
+func getDataField(data map[string]interface{}, field string) string {
+	if field == "" {
 		return ""
 		return ""
 	}
 	}
 
 
-	username, ok := userData[provider.UsernameField]
+	val, ok := data[field]
 
 
 	if !ok {
 	if !ok {
-		log.Errorf("Failed to get username from user data: %v", userData)
+		log.Errorf("Failed to get field from user data: %v / %v", data, field)
 
 
 		return ""
 		return ""
 	}
 	}
 
 
-	return username.(string)
+	return val.(string)
 }
 }
 
 
 func parseOAuth2Cookie(r *http.Request) (string, string, string) {
 func parseOAuth2Cookie(r *http.Request) (string, string, string) {