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 }