|
|
@@ -0,0 +1,249 @@
|
|
|
+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: "http://localhost:1337/oauth/callback",
|
|
|
+ }
|
|
|
+
|
|
|
+ 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
|
|
|
+}
|