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

View File

@ -3,6 +3,8 @@ package httpservers
import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
@ -12,6 +14,7 @@ import (
"io"
"net/http"
"time"
"os"
)
var (
@ -93,7 +96,7 @@ func randString(nByte int) (string, error) {
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{
Name: name,
Value: value,
@ -129,7 +132,7 @@ func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
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)
@ -165,6 +168,44 @@ func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2St
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) {
log.Infof("OAuth2 Callback received")
@ -181,9 +222,17 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
"token-code": 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.WithValue(ctx, oauth2.HTTPClient, httpClient)
ctx = context.WithValue(ctx, oauth2.HTTPClient, exchangeClient)
tok, err := registeredState.providerConfig.Exchange(ctx, code)
@ -193,11 +242,18 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
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 {
log.Debugf("states: %+v %+v", k, v)
@ -213,12 +269,19 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
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)
if res.StatusCode != http.StatusOK {
log.Errorf("Failed to get user data: %v", res.StatusCode)
return ""
return ret
}
defer res.Body.Close()
@ -232,18 +295,29 @@ func getUsername(client *http.Client, provider *config.OAuth2Provider) string {
if err != nil {
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 ""
}
username, ok := userData[provider.UsernameField]
val, ok := data[field]
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 username.(string)
return val.(string)
}
func parseOAuth2Cookie(r *http.Request) (string, string, string) {