olivetin/internal/httpservers/oauth2.go

250 lines
5.7 KiB
Go

package httpservers
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
config "github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"io"
"net/http"
"time"
)
var (
registeredStates = make(map[string]*oauth2State)
registeredProviders = make(map[string]*oauth2.Config)
)
type oauth2State struct {
provider *oauth2.Config
Username string
Usergroup string
}
func assignIfEmpty(target *string, value string) {
if *target == "" {
*target = value
}
}
func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
dbConfig, ok := oauth2ProviderDatabase[providerName]
if ok {
assignIfEmpty(&providerConfig.WhoamiUrl, dbConfig.WhoamiUrl)
assignIfEmpty(&providerConfig.TokenUrl, dbConfig.TokenUrl)
assignIfEmpty(&providerConfig.AuthUrl, dbConfig.AuthUrl)
assignIfEmpty(&providerConfig.Icon, dbConfig.Icon)
assignIfEmpty(&providerConfig.UsernameField, dbConfig.UsernameField)
if providerConfig.Scopes == nil {
providerConfig.Scopes = dbConfig.Scopes
}
} else {
log.Warnf("Provider not found in database: %v", providerName)
}
}
func getOAuth2Config(cfg *config.Config, providerName string) (*oauth2.Config, error) {
config, ok := registeredProviders[providerName]
if !ok {
providerConfig, ok := cfg.AuthOAuth2Providers[providerName]
if !ok {
return nil, fmt.Errorf("Provider not found in config: %v", providerName)
}
completeProviderConfig(providerName, providerConfig)
config = &oauth2.Config{
ClientID: providerConfig.ClientID,
ClientSecret: providerConfig.ClientSecret,
Scopes: providerConfig.Scopes,
Endpoint: oauth2.Endpoint{
AuthURL: providerConfig.AuthUrl,
TokenURL: providerConfig.TokenUrl,
},
RedirectURL: cfg.AuthOAuth2RedirectURL,
}
registeredProviders[providerName] = config
log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
}
return config, nil
}
func randString(nByte int) (string, error) {
b := make([]byte, nByte)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
func setOauthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
cookie := &http.Cookie{
Name: name,
Value: value,
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
HttpOnly: true,
Path: "/",
}
http.SetCookie(w, cookie)
}
func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
state, err := randString(16)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
providerName := r.URL.Query().Get("provider")
provider, err := getOAuth2Config(cfg, providerName)
registeredStates[state] = &oauth2State{
provider: provider,
}
if err != nil {
log.Errorf("Failed to get provider config: %v %v", providerName, err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
setOauthCallbackCookie(w, r, "oauth2state", state)
log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
}
func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, bool) {
state, err := r.Cookie("oauth2state")
if err != nil {
log.Errorf("Failed to get state cookie: %v", err)
http.Error(w, "State not found", http.StatusBadRequest)
return nil, false
}
if r.URL.Query().Get("state") != state.Value {
log.Errorf("State mismatch: %v != %v", r.URL.Query().Get("state"), state.Value)
http.Error(w, "State mismatch", http.StatusBadRequest)
return nil, false
}
registeredState, ok := registeredStates[state.Value]
if !ok {
log.Errorf("State not found in server: %v", state.Value)
http.Error(w, "State not found in server", http.StatusBadRequest)
}
return registeredState, true
}
func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
log.Infof("OAuth2 Callback received")
registeredState, ok := checkOAuthCallbackCookie(w, r)
if !ok {
return
}
code := r.FormValue("code")
log.Debugf("OAuth2 Token Code: %v", code)
httpClient := &http.Client{Timeout: 2 * time.Second}
ctx := context.Background()
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
tok, err := registeredState.provider.Exchange(ctx, code)
if err != nil {
log.Errorf("Failed to exchange code: %v", err)
http.Error(w, "Failed to exchange code", http.StatusBadRequest)
return
}
client := registeredState.provider.Client(ctx, tok)
registeredState.Username = getUsername(client)
loginMessage := fmt.Sprintf("Logged in as %v", registeredState.Username)
log.Infof(loginMessage)
w.Write([]byte(loginMessage))
}
func getUsername(client *http.Client) string {
provider := cfg.AuthOAuth2Providers["github"]
res, err := client.Get(provider.WhoamiUrl)
if res.StatusCode != http.StatusOK {
log.Errorf("Failed to get user data: %v", res.StatusCode)
return ""
}
defer res.Body.Close()
contents, err := io.ReadAll(res.Body)
var userData map[string]interface{}
err = json.Unmarshal([]byte(contents), &userData)
if err != nil {
log.Errorf("Failed to unmarshal user data: %v", err)
return ""
}
username, ok := userData[provider.UsernameField]
if !ok {
log.Errorf("Failed to get username from user data: %v", userData)
return ""
}
return username.(string)
}
func parseOAuth2Cookie(r *http.Request) (string, string) {
cookie, err := r.Cookie("oauth2state")
if err != nil {
log.Warnf("Failed to read OAuth2 cookie: %v", err)
return "", ""
}
serverState, found := registeredStates[cookie.Value]
if !found {
log.Warnf("Failed to find OAuth2 state: %v", cookie.Value)
return "", ""
}
return serverState.Username, serverState.Usergroup
}