github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/auth/github.go (about)

     1  package auth
     2  
     3  import (
     4  	"crypto/md5"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"time"
     9  
    10  	"github.com/evergreen-ci/evergreen"
    11  	"github.com/evergreen-ci/evergreen/thirdparty"
    12  	"github.com/evergreen-ci/evergreen/util"
    13  	"github.com/mongodb/grip"
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  // GithubAuthManager implements the UserManager with GitHub authentication using Oauth authentication.
    18  // The process starts off with a redirect GET request to GitHub sent with the application's ClientId,
    19  // the CallbackURI of the application where Github should redirect the user to after authenticating with username/password,
    20  // the scope (the email and organization information of the user the application uses to authorize the user,
    21  // and an unguessable State string. The state string is concatenation of a timestamp and a hash of the timestamp
    22  // and the Salt field in GithubUserManager.
    23  // After authenticating the User, GitHub redirects the user back to the CallbackURI given with a code parameter
    24  // and the unguessable State string. The application checks that the State strings are the same by reproducing
    25  // the state string using the Salt and the timestamp that is in plain-text before the hash and checking to make sure
    26  // that they are the same.
    27  // The application sends the code back in a POST with the ClientId and ClientSecret and receives a response that has the
    28  // accessToken used to get the user's information. The application stores the accessToken in a session cookie.
    29  // Whenever GetUserByToken is called, the application sends the token to GitHub, gets the user's login username and organization
    30  // and ensures that the user is either in an Authorized organization or an Authorized user.
    31  
    32  type GithubUserManager struct {
    33  	ClientId               string
    34  	ClientSecret           string
    35  	AuthorizedUsers        []string
    36  	AuthorizedOrganization string
    37  	Salt                   string
    38  }
    39  
    40  // NewGithubUserManager initializes a GithubUserManager with a Salt as randomly generated string used in Github
    41  // authentication
    42  func NewGithubUserManager(g *evergreen.GithubAuthConfig) (*GithubUserManager, error) {
    43  	if g.ClientId == "" {
    44  		return nil, errors.New("no client id for config")
    45  	}
    46  	if g.ClientSecret == "" {
    47  		return nil, errors.New("no client secret for config given")
    48  	}
    49  	return &GithubUserManager{g.ClientId, g.ClientSecret, g.Users, g.Organization, util.RandomString()}, nil
    50  }
    51  
    52  // GetUserByToken sends the token to Github and gets back a user and optionally an organization.
    53  // If there are Authorized Users, it checks the authorized usernames against the GitHub user's login
    54  // If there is no match and there is an organization it checks the user's organizations against
    55  // the UserManager's Authorized organization string.
    56  func (gum *GithubUserManager) GetUserByToken(token string) (User, error) {
    57  	user, organizations, err := thirdparty.GetGithubUser(token)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	if user != nil {
    62  		if gum.AuthorizedUsers != nil {
    63  			for _, u := range gum.AuthorizedUsers {
    64  				if u == user.Username() {
    65  					return user, nil
    66  				}
    67  			}
    68  		}
    69  		if gum.AuthorizedOrganization != "" {
    70  			for _, organization := range organizations {
    71  				if organization.Login == gum.AuthorizedOrganization {
    72  					return user, nil
    73  				}
    74  			}
    75  		}
    76  	}
    77  
    78  	return nil, errors.New("No authorized user or organization given")
    79  }
    80  
    81  // CreateUserToken is not implemented in GithubUserManager
    82  func (*GithubUserManager) CreateUserToken(string, string) (string, error) {
    83  	return "", errors.New("GithubUserManager does not create tokens via username/password")
    84  }
    85  
    86  // GetLoginHandler returns the function that starts oauth by redirecting the user to authenticate with Github
    87  func (gum *GithubUserManager) GetLoginHandler(callbackUri string) func(w http.ResponseWriter, r *http.Request) {
    88  	return func(w http.ResponseWriter, r *http.Request) {
    89  		githubScope := "user:email, read:org"
    90  		githubUrl := "https://github.com/login/oauth/authorize"
    91  		timestamp := time.Now().String()
    92  		// create a combination of the current time and the config's salt to hash as the unguessable string
    93  		githubState := fmt.Sprintf("%v%x", timestamp, md5.Sum([]byte(timestamp+gum.Salt)))
    94  		parameters := url.Values{}
    95  		parameters.Set("client_id", gum.ClientId)
    96  		parameters.Set("redirect_uri", fmt.Sprintf("%v/login/redirect/callback?%v", callbackUri, r.URL.RawQuery))
    97  		parameters.Set("scope", githubScope)
    98  		parameters.Set("state", githubState)
    99  		http.Redirect(w, r, fmt.Sprintf("%v?%v", githubUrl, parameters.Encode()), http.StatusFound)
   100  	}
   101  }
   102  
   103  // GetLoginCallbackHandler returns the function that is called when GitHub redirects the user back to Evergreen.
   104  func (gum *GithubUserManager) GetLoginCallbackHandler() func(w http.ResponseWriter, r *http.Request) {
   105  	return func(w http.ResponseWriter, r *http.Request) {
   106  		code := r.FormValue("code")
   107  		if code == "" {
   108  			grip.Error("Error getting code from github for authentication")
   109  			return
   110  		}
   111  		githubState := r.FormValue("state")
   112  		if githubState == "" {
   113  			grip.Error("Error getting state from github for authentication")
   114  			return
   115  		}
   116  		// if there is an internal redirect page, redirect the user back to that page
   117  		// otherwise redirect the user back to the home page
   118  		redirect := r.FormValue("redirect")
   119  		if redirect == "" {
   120  			redirect = "/"
   121  		}
   122  		// create the state from the timestamp and Salt and check against the one GitHub sent back
   123  		timestamp := githubState[:len(time.Now().String())]
   124  		state := fmt.Sprintf("%v%x", timestamp, md5.Sum([]byte(timestamp+gum.Salt)))
   125  
   126  		// if the state doesn't match, log the error and redirect back to the login page
   127  		if githubState != state {
   128  			grip.Errorf("Error unmatching states when authenticating with GitHub: ours: %v, theirs %v",
   129  				state, githubState)
   130  			http.Redirect(w, r, "/login", http.StatusFound)
   131  			return
   132  		}
   133  		githubResponse, err := thirdparty.GithubAuthenticate(code, gum.ClientId, gum.ClientSecret)
   134  		if err != nil {
   135  			grip.Errorf("Error sending code and authentication info to Github: %+v", err)
   136  			return
   137  		}
   138  		setLoginToken(githubResponse.AccessToken, w)
   139  		http.Redirect(w, r, redirect, http.StatusFound)
   140  	}
   141  }
   142  
   143  func (*GithubUserManager) IsRedirect() bool {
   144  	return true
   145  }