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

This commit is contained in:
James Read 2024-11-24 05:00:25 -05:00 committed by GitHub
parent b4a555e3da
commit f0b1cefb72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 100 additions and 22 deletions

View File

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

View File

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