Skip to content
Snippets Groups Projects
oauth2.go 5.71 KiB
Newer Older
  • Learn to ignore specific revisions
  • Alexis Poyen's avatar
    Alexis Poyen committed
    package auth
    
    import (
    	"encoding/json"
    	"errors"
    	"fmt"
    	"net/http"
    	"os"
    	"strings"
    	"time"
    
    	"forge.grandlyon.com/apoyen/sdk-go/pkg/common"
    	"forge.grandlyon.com/apoyen/sdk-go/pkg/log"
    	"forge.grandlyon.com/apoyen/sdk-go/pkg/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
    }
    
    // UserOAuth2 is the user get from OAuth2 authentification
    type UserOAuth2 struct {
    	ID          string   `json:"id,omitempty"`
    	Login       string   `json:"login"`
    	DisplayName string   `json:"displayName,omitempty"`
    	Groups      []string `json:"memberOf"`
    	IsAdmin     bool     `json:"isAdmin,omitempty"`
    	Name        string   `json:"name,omitempty"`
    	Surname     string   `json:"surname,omitempty"`
    }
    
    // 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:    os.Getenv("HOSTNAME"),
    		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.Logger.Fatalf("Error generating OAuth2 strate string :%v\n", err)
    	}
    	tokens.Manager.StoreData(oauthStateString, m.Hostname, oAuth2StateKey, 30*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.Manager.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(oauth2.NoContext, 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 userOauth2 UserOAuth2
    		if response.Body == nil {
    			http.Error(w, "no response body", 400)
    			return
    		}
    		err = json.NewDecoder(response.Body).Decode(&userOauth2)
    		if err != nil {
    			http.Error(w, err.Error(), 400)
    			return
    		}
    		// Trim the user roles in case they come from LDAP
    		for key, role := range userOauth2.Groups {
    			userOauth2.Groups[key] = strings.TrimPrefix(strings.Split(role, ",")[0], "CN=")
    		}
    		// Store the user in cookie
    		dh := NewDataHandler()
    		user, err := dh.getUserInMemory(userOauth2)
    		if err != nil {
    			http.Error(w, err.Error(), 400)
    			return
    		}
    		// Generate
    		xsrfToken, err := common.GenerateRandomString(16)
    		if err != nil {
    			http.Error(w, "error generating XSRF Token", 500)
    			return
    		}
    		tokenData := TokenData{User: user, XSRFToken: xsrfToken}
    		tokens.Manager.StoreData(tokenData, m.Hostname, authTokenKey, 24*time.Hour, w)
    		// Log the connexion
    		log.Logger.Printf("| %v (%v %v) | Login success | %v | %v", user.Login, user.Name, user.Surname, req.RemoteAddr, log.GetCityAndCountryFromRequest(req))
    		// Redirect
    		http.Redirect(w, r, "/", http.StatusFound)
    	}
    	return http.HandlerFunc(oauth2Handler)
    }
    
    func (d *DataHandler) getUserInMemory(userOAuth2 UserOAuth2) (User, error) {
    	var users = d.getUsers()
    	for _, val := range users {
    		if userOAuth2.ID == val.IDOAuth {
    			return val, nil
    		}
    	}
    	return d.addUserInMemory(userOAuth2)
    }
    
    func (d *DataHandler) addUserInMemory(userOauth2 UserOAuth2) (User, error) {
    	var user User
    	// Define user role or refuse if not in a correct group
    	for _, userRole := range userOauth2.Groups {
    		if userRole != "" && (userRole == os.Getenv("ADMIN_GROUP")) {
    			user.Role = "ADMIN"
    
    		} else if userRole != "" && (userRole == os.Getenv("VISUALIZER_GROUP")) {
    			user.Role = "VISUALIZER"
    
    Alexis Poyen's avatar
    Alexis Poyen committed
    		} else {
    			return user, errors.New("user not in an app group")
    		}
    	}
    	user.IDOAuth = userOauth2.ID
    	user.Login = userOauth2.Login
    	user.DisplayName = userOauth2.DisplayName
    	user.Name = userOauth2.Name
    	user.Surname = userOauth2.Surname
    
    	var users []User
    	err := common.Load(UsersFile, &users)
    	if err != nil {
    		return user, errors.New("Error on loading user")
    	}
    	// Select the new id for the user
    	user.ID = 1
    	for _, val := range users {
    		if user.ID <= val.ID {
    			user.ID = val.ID + 1
    		}
    	}
    	// Sauvegarder l'utilisateur dans InMemory
    	d.createUser(user)
    	return user, nil
    }