github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/githuboauth/githuboauth.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package githuboauth 18 19 import ( 20 "crypto/subtle" 21 "encoding/hex" 22 "fmt" 23 "net/http" 24 "time" 25 26 "github.com/google/go-github/github" 27 "github.com/sirupsen/logrus" 28 "golang.org/x/net/context" 29 "golang.org/x/net/xsrftoken" 30 "golang.org/x/oauth2" 31 32 "k8s.io/test-infra/ghclient" 33 "k8s.io/test-infra/prow/config" 34 ) 35 36 const ( 37 loginSession = "github_login" 38 tokenSession = "access-token-session" 39 tokenKey = "access-token" 40 oauthSessionCookie = "oauth-session" 41 stateKey = "state" 42 ) 43 44 // GithubClientWrapper is an interface for github clients which implements GetUser method 45 // that returns github.User. 46 type GithubClientWrapper interface { 47 GetUser(login string) (*github.User, error) 48 } 49 50 // GithubClientGetter interface is used by handleRedirect to get github client. 51 type GithubClientGetter interface { 52 GetGithubClient(accessToken string, dryRun bool) GithubClientWrapper 53 } 54 55 type OAuthClient interface { 56 // Exchanges code from github oauth redirect for user access token. 57 Exchange(ctx context.Context, code string) (*oauth2.Token, error) 58 // Returns a URL to OAuth 2.0 github's consent page. The state is a token to protect user from 59 // XSRF attack. 60 AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string 61 } 62 63 type githubClientGetterImpl struct{} 64 65 func (gci *githubClientGetterImpl) GetGithubClient(accessToken string, dryRun bool) GithubClientWrapper { 66 return ghclient.NewClient(accessToken, dryRun) 67 } 68 69 func NewGithubClientGetter() GithubClientGetter { 70 return &githubClientGetterImpl{} 71 } 72 73 // GithubOAuth Agent represents an agent that takes care Github authentication process such as handles 74 // login request from users or handles redirection from Github OAuth server. 75 type GithubOAuthAgent struct { 76 gc *config.GithubOAuthConfig 77 logger *logrus.Entry 78 } 79 80 // Returns new GithubOAUth Agent. 81 func NewGithubOAuthAgent(config *config.GithubOAuthConfig, logger *logrus.Entry) *GithubOAuthAgent { 82 return &GithubOAuthAgent{ 83 gc: config, 84 logger: logger, 85 } 86 } 87 88 // HandleLogin handles Github login request from front-end. It starts a new git oauth session and 89 // redirect user to Github OAuth end-point for authentication. 90 func (ga *GithubOAuthAgent) HandleLogin(client OAuthClient) http.HandlerFunc { 91 return func(w http.ResponseWriter, r *http.Request) { 92 stateToken := xsrftoken.Generate(ga.gc.ClientSecret, "", "") 93 state := hex.EncodeToString([]byte(stateToken)) 94 oauthSession, err := ga.gc.CookieStore.New(r, oauthSessionCookie) 95 oauthSession.Options.Secure = true 96 oauthSession.Options.HttpOnly = true 97 if err != nil { 98 ga.serverError(w, "Creating new OAuth session", err) 99 return 100 } 101 oauthSession.Options.MaxAge = 10 * 60 102 oauthSession.Values[stateKey] = state 103 104 if err := oauthSession.Save(r, w); err != nil { 105 ga.serverError(w, "Save oauth session", err) 106 return 107 } 108 109 redirectURL := client.AuthCodeURL(state, oauth2.ApprovalForce, oauth2.AccessTypeOnline) 110 http.Redirect(w, r, redirectURL, http.StatusFound) 111 } 112 } 113 114 // HandleLogout handles Github logout request from front-end. It invalidates cookie sessions and 115 // redirect back to the front page. 116 func (ga *GithubOAuthAgent) HandleLogout(client OAuthClient) http.HandlerFunc { 117 return func(w http.ResponseWriter, r *http.Request) { 118 accessTokenSession, err := ga.gc.CookieStore.Get(r, tokenSession) 119 if err != nil { 120 ga.serverError(w, "get cookie", err) 121 return 122 } 123 // Clear session 124 accessTokenSession.Options.MaxAge = -1 125 if err := accessTokenSession.Save(r, w); err != nil { 126 ga.serverError(w, "Save invalidated session on log out", err) 127 return 128 } 129 loginCookie, err := r.Cookie(loginSession) 130 if err == nil { 131 loginCookie.MaxAge = -1 132 loginCookie.Expires = time.Now().Add(-time.Hour * 24) 133 http.SetCookie(w, loginCookie) 134 } 135 http.Redirect(w, r, ga.gc.FinalRedirectURL, http.StatusFound) 136 } 137 } 138 139 // HandleRedirect handles the redirection from Github. It exchanges the code from redirect URL for 140 // user access token. The access token is then saved to the cookie and the page is redirected to 141 // the final destination in the config, which should be the front-end. 142 func (ga *GithubOAuthAgent) HandleRedirect(client OAuthClient, getter GithubClientGetter) http.HandlerFunc { 143 return func(w http.ResponseWriter, r *http.Request) { 144 state := r.FormValue("state") 145 stateTokenRaw, err := hex.DecodeString(state) 146 if err != nil { 147 ga.serverError(w, "Decode state", fmt.Errorf("error with decoding state")) 148 } 149 stateToken := string(stateTokenRaw) 150 // Check if the state token is still valid or not. 151 if !xsrftoken.Valid(stateToken, ga.gc.ClientSecret, "", "") { 152 ga.serverError(w, "Validate state", fmt.Errorf("state token has expired")) 153 return 154 } 155 156 oauthSession, err := ga.gc.CookieStore.Get(r, oauthSessionCookie) 157 if err != nil { 158 ga.serverError(w, "Get cookie", err) 159 return 160 } 161 secretState, ok := oauthSession.Values[stateKey].(string) 162 if !ok { 163 ga.serverError(w, "Get secret state", fmt.Errorf("empty string or cannot convert to string")) 164 return 165 } 166 // Validate the state parameter to prevent cross-site attack. 167 if state == "" || subtle.ConstantTimeCompare([]byte(state), []byte(secretState)) != 1 { 168 ga.serverError(w, "Validate state", fmt.Errorf("invalid state")) 169 return 170 } 171 172 // Exchanges the code for user access token. 173 code := r.FormValue("code") 174 token, err := client.Exchange(context.Background(), code) 175 if err != nil { 176 ga.serverError(w, "Exchange code for token", err) 177 return 178 } 179 180 // New session that stores the token. 181 session, err := ga.gc.CookieStore.New(r, tokenSession) 182 session.Options.Secure = true 183 session.Options.HttpOnly = true 184 if err != nil { 185 ga.serverError(w, "Create new session", err) 186 return 187 } 188 189 session.Values[tokenKey] = token 190 if err := session.Save(r, w); err != nil { 191 ga.serverError(w, "Save session", err) 192 return 193 } 194 ghc := getter.GetGithubClient(token.AccessToken, false) 195 user, err := ghc.GetUser("") 196 if err != nil { 197 ga.serverError(w, "Get user login", err) 198 return 199 } 200 http.SetCookie(w, &http.Cookie{ 201 Name: loginSession, 202 Value: *user.Login, 203 Path: "/", 204 Expires: time.Now().Add(time.Hour * 24 * 30), 205 Secure: true, 206 }) 207 http.Redirect(w, r, ga.gc.FinalRedirectURL, http.StatusFound) 208 } 209 } 210 211 // Handles server errors. 212 func (ga *GithubOAuthAgent) serverError(w http.ResponseWriter, action string, err error) { 213 ga.logger.WithError(err).Errorf("Error %s.", action) 214 msg := fmt.Sprintf("500 Internal server error %s: %v", action, err) 215 http.Error(w, msg, http.StatusInternalServerError) 216 }