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  }