package auth import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/tokens" "golang.org/x/oauth2" ) const ( oAuth2StateKey string = "oauth2_state" ) // Manager exposes the handlers for OAuth2 endpoints type Manager struct { Config *oauth2.Config Hostname string UserInfoURL string } // NewManager returns a new Manager according to environment variables func NewManager() Manager { return Manager{Config: &oauth2.Config{ RedirectURL: os.Getenv("REDIRECT_URL"), ClientID: os.Getenv("CLIENT_ID"), ClientSecret: os.Getenv("CLIENT_SECRET"), Scopes: []string{"login", "memberOf", "displayName", "email"}, Endpoint: oauth2.Endpoint{ AuthURL: os.Getenv("AUTH_URL"), TokenURL: os.Getenv("TOKEN_URL"), }, }, Hostname: common.StringValueFromEnv("HOSTNAME", "ecolyobackoffice.127.0.0.1.nip.io"), UserInfoURL: os.Getenv("USERINFO_URL"), } } // HandleOAuth2Login handles the OAuth2 login func (m Manager) HandleOAuth2Login(w http.ResponseWriter, r *http.Request) { // Generate state and store it in cookie oauthStateString, err := common.GenerateRandomString(16) if err != nil { log.Fatalf("Error generating OAuth2 strate string :%v\n", err) } tokens.CreateCookie(oauthStateString, m.Hostname, oAuth2StateKey, 60*time.Second, w) url := m.Config.AuthCodeURL(oauthStateString) http.Redirect(w, r, url, http.StatusTemporaryRedirect) } // HandleOAuth2Callback handles the OAuth2 Callback and get user info func (m Manager) HandleOAuth2Callback() http.Handler { oauth2Handler := func(w http.ResponseWriter, r *http.Request) { // Recover state from tokens var oauthState string _, err := tokens.ExtractAndValidateToken(r, oAuth2StateKey, &oauthState, false) if err != nil { fmt.Println("Code exchange failed with ", err) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } // Check states match state := r.FormValue("state") if state != oauthState { fmt.Printf("invalid oauth state, expected '%s', got '%s'\n", oauthState, state) http.Error(w, "invalid oauth state", http.StatusInternalServerError) return } // Delete the state cookie c := http.Cookie{ Name: oAuth2StateKey, Domain: m.Hostname, MaxAge: -1, } http.SetCookie(w, &c) // Perform code exchange code := r.FormValue("code") token, err := m.Config.Exchange(context.Background(), code) if err != nil { fmt.Printf("Code exchange failed with '%s'\n", err) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } // Get user infos client := &http.Client{} req, _ := http.NewRequest("GET", m.UserInfoURL+"?access_token="+token.AccessToken, nil) req.Header.Set("Authorization", "Bearer "+token.AccessToken) response, err := client.Do(req) if err != nil || response.StatusCode == http.StatusBadRequest { fmt.Printf("User info failed with '%s'\n", err) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } // Get user var user User if response.Body == nil { http.Error(w, "no response body", http.StatusBadRequest) return } //////////////////////////////////////////////// // UNCOMMENT THIS TO DEBUG USERINFO RESPONSE // // readBody, err := ioutil.ReadAll(response.Body) // if err != nil { // panic(err) // } // newBody := ioutil.NopCloser(bytes.NewBuffer(readBody)) // response.Body = newBody // if string(readBody) != "" { // fmt.Printf("BODY : %q \n", readBody) // } //////////////////////////////////////////////// err = json.NewDecoder(response.Body).Decode(&user) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Trim the user roles in case they come from LDAP for key, role := range user.Roles { user.Roles[key] = strings.TrimPrefix(strings.Split(role, ",")[0], "CN=") } // Store the user in cookie // Generate xsrfToken, err := common.GenerateRandomString(16) if err != nil { http.Error(w, "error generating XSRF Token", http.StatusInternalServerError) return } tokenData := TokenData{User: user, XSRFToken: xsrfToken} tokens.CreateCookie(tokenData, m.Hostname, authTokenKey, 24*time.Hour, w) // Log the connexion log.Printf("| %v (%v %v) | Login success | %v", user.Login, user.Name, user.Surname, req.RemoteAddr) // Redirect http.Redirect(w, r, "/", http.StatusFound) } return http.HandlerFunc(oauth2Handler) }