| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- 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
- }
|