Skip to content
Snippets Groups Projects
oauth2.go 4.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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=")
    		}
    
    
    		// Check if user has the correct role
    		err = checkUserHasRole(TokenData{User: user}, []string{AdminRole})
    
    		if err != nil {
    			// Log the connexion attempt
    			log.Printf("| %v (%v %v) | Login failed (Unauthorized user) | %v", user.Login, user.Name, user.Surname, req.RemoteAddr)
    			http.Redirect(w, r, "/", http.StatusFound)
    			return
    		}
    
    
    		// 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
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
    		http.Redirect(w, r, "/", http.StatusFound)
    
    	}
    	return http.HandlerFunc(oauth2Handler)
    }