sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/githuboauth/githuboauth_test.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  	"encoding/gob"
    21  	"encoding/hex"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"net/url"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/gorilla/securecookie"
    29  	"github.com/gorilla/sessions"
    30  	"github.com/sirupsen/logrus"
    31  	"golang.org/x/net/context"
    32  	"golang.org/x/net/xsrftoken"
    33  	"golang.org/x/oauth2"
    34  )
    35  
    36  const mockAccessToken = "justSomeRandomSecretToken"
    37  
    38  type mockOAuthClient struct {
    39  	config *oauth2.Config
    40  }
    41  
    42  func (c mockOAuthClient) WithFinalRedirectURL(path string) (OAuthClient, error) {
    43  	parsedURL, err := url.Parse("www.something.com")
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	q := parsedURL.Query()
    48  	q.Set("dest", path)
    49  	parsedURL.RawQuery = q.Encode()
    50  	return mockOAuthClient{&oauth2.Config{RedirectURL: parsedURL.String()}}, nil
    51  }
    52  
    53  func (c mockOAuthClient) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
    54  	return &oauth2.Token{
    55  		AccessToken: mockAccessToken,
    56  	}, nil
    57  }
    58  
    59  func (c mockOAuthClient) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
    60  	return c.config.AuthCodeURL(state, opts...)
    61  }
    62  
    63  func getMockConfig(cookie *sessions.CookieStore) *Config {
    64  	clientID := "mock-client-id"
    65  	clientSecret := "mock-client-secret"
    66  	redirectURL := "uni-test/redirect-url"
    67  	scopes := []string{}
    68  
    69  	return &Config{
    70  		ClientID:     clientID,
    71  		ClientSecret: clientSecret,
    72  		RedirectURL:  redirectURL,
    73  		Scopes:       scopes,
    74  
    75  		CookieStore: cookie,
    76  	}
    77  }
    78  
    79  func createMockStateToken(config *Config) string {
    80  	stateToken := xsrftoken.Generate(config.ClientSecret, "", "")
    81  	state := hex.EncodeToString([]byte(stateToken))
    82  
    83  	return state
    84  }
    85  
    86  func isEqual(token1 *oauth2.Token, token2 *oauth2.Token) bool {
    87  	return token1.AccessToken == token2.AccessToken &&
    88  		token1.Expiry == token2.Expiry &&
    89  		token1.RefreshToken == token2.RefreshToken &&
    90  		token1.TokenType == token2.TokenType
    91  }
    92  
    93  func TestHandleLogin(t *testing.T) {
    94  	dest := "wherever"
    95  	rerunStatus := "working"
    96  	cookie := sessions.NewCookieStore([]byte("secret-key"))
    97  	mockConfig := getMockConfig(cookie)
    98  	mockLogger := logrus.WithField("uni-test", "githuboauth")
    99  	mockAgent := NewAgent(mockConfig, mockLogger)
   100  	mockOAuthClient := mockOAuthClient{}
   101  
   102  	mockRequest := httptest.NewRequest(http.MethodGet, "/mock-login?dest="+dest+"?rerun="+rerunStatus, nil)
   103  	mockResponse := httptest.NewRecorder()
   104  
   105  	handleLoginFn := mockAgent.HandleLogin(mockOAuthClient, false)
   106  	handleLoginFn.ServeHTTP(mockResponse, mockRequest)
   107  	result := mockResponse.Result()
   108  	if result.StatusCode != http.StatusFound {
   109  		t.Errorf("Unexpected status code. Got %v, expected %v", result.StatusCode, http.StatusFound)
   110  	}
   111  	resultCookies := result.Cookies()
   112  	var oauthCookie *http.Cookie
   113  	for _, v := range resultCookies {
   114  		if v.Name == oauthSessionCookie {
   115  			oauthCookie = v
   116  			break
   117  		}
   118  	}
   119  	if oauthCookie == nil {
   120  		t.Fatal("Cookie for oauth session not found")
   121  	}
   122  	decodedCookie := make(map[interface{}]interface{})
   123  	if err := securecookie.DecodeMulti(oauthCookie.Name, oauthCookie.Value, &decodedCookie, cookie.Codecs...); err != nil {
   124  		t.Fatalf("Cannot decoded cookie: %v", err)
   125  	}
   126  	state, ok := decodedCookie[stateKey].(string)
   127  	if !ok {
   128  		t.Fatal("Error with getting state parameter")
   129  	}
   130  	stateTokenRaw, err := hex.DecodeString(state)
   131  	if err != nil {
   132  		t.Fatal("Cannot decoding state token")
   133  	}
   134  	stateToken := string(stateTokenRaw)
   135  	if !xsrftoken.Valid(stateToken, mockConfig.ClientSecret, "", "") {
   136  		t.Error("Expect the state token is valid, found state token invalid.")
   137  	}
   138  	if state == "" {
   139  		t.Error("Expect state parameter is not empty, found empty")
   140  	}
   141  	destCount := 0
   142  	path := mockResponse.Header().Get("Location")
   143  	for _, q := range strings.Split(path, "&") {
   144  		if q == "redirect_uri=www.something.com%3Fdest%3Dwherever%253Frerun%253Dworking" {
   145  			destCount++
   146  		}
   147  	}
   148  	if destCount != 1 {
   149  		t.Errorf("Redirect URI in path does not include correct destination. path: %s, destination: %s", path, "?dest="+dest+"?rerun=working")
   150  	}
   151  }
   152  
   153  func TestHandleLogout(t *testing.T) {
   154  	cookie := sessions.NewCookieStore([]byte("secret-key"))
   155  	mockConfig := getMockConfig(cookie)
   156  	mockLogger := logrus.WithField("uni-test", "githuboauth")
   157  	mockAgent := NewAgent(mockConfig, mockLogger)
   158  	mockOAuthClient := mockOAuthClient{}
   159  
   160  	mockRequest := httptest.NewRequest(http.MethodGet, "/mock-logout", nil)
   161  	_, err := cookie.New(mockRequest, tokenSession)
   162  	if err != nil {
   163  		t.Fatalf("Failed to create a mock token session with error: %v", err)
   164  	}
   165  	mockResponse := httptest.NewRecorder()
   166  
   167  	handleLoginFn := mockAgent.HandleLogout(mockOAuthClient)
   168  	handleLoginFn.ServeHTTP(mockResponse, mockRequest)
   169  	result := mockResponse.Result()
   170  	if result.StatusCode != http.StatusFound {
   171  		t.Errorf("Unexpected status code. Got %v, expected %v", result.StatusCode, http.StatusFound)
   172  	}
   173  	resultCookies := result.Cookies()
   174  	var tokenCookie *http.Cookie
   175  	cookieCounts := 0
   176  	for _, v := range resultCookies {
   177  		if v.Name == tokenSession {
   178  			tokenCookie = v
   179  			cookieCounts++
   180  		}
   181  	}
   182  	if cookieCounts != 1 {
   183  		t.Errorf("Wrong number of %s cookie. There should be only one cookie with name %s", tokenSession, tokenSession)
   184  	}
   185  	if tokenCookie == nil {
   186  		t.Fatal("Cookie for oauth session not found")
   187  	}
   188  	if tokenCookie.MaxAge != -1 {
   189  		t.Errorf("Expect cookie MaxAge equals -1, %d", tokenCookie.MaxAge)
   190  	}
   191  }
   192  
   193  func TestHandleLogoutWithLoginSession(t *testing.T) {
   194  	cookie := sessions.NewCookieStore([]byte("secret-key"))
   195  	mockConfig := getMockConfig(cookie)
   196  	mockLogger := logrus.WithField("uni-test", "githuboauth")
   197  	mockAgent := NewAgent(mockConfig, mockLogger)
   198  	mockOAuthClient := mockOAuthClient{}
   199  
   200  	mockRequest := httptest.NewRequest(http.MethodGet, "/mock-logout", nil)
   201  	_, err := cookie.New(mockRequest, tokenSession)
   202  	mocKLoginSession := &http.Cookie{
   203  		Name: loginSession,
   204  		Path: "/",
   205  	}
   206  	mockRequest.AddCookie(mocKLoginSession)
   207  	if err != nil {
   208  		t.Fatalf("Failed to create a mock token session with error: %v", err)
   209  	}
   210  	mockResponse := httptest.NewRecorder()
   211  
   212  	handleLoginFn := mockAgent.HandleLogout(mockOAuthClient)
   213  	handleLoginFn.ServeHTTP(mockResponse, mockRequest)
   214  	result := mockResponse.Result()
   215  	if result.StatusCode != http.StatusFound {
   216  		t.Errorf("Unexpected status code. Got %v, expected %v", result.StatusCode, http.StatusFound)
   217  	}
   218  	resultCookies := result.Cookies()
   219  	var loginCookie *http.Cookie
   220  	for _, v := range resultCookies {
   221  		if v.Name == loginSession {
   222  			loginCookie = v
   223  			break
   224  		}
   225  	}
   226  	if loginCookie == nil {
   227  		t.Fatal("Cookie for oauth session not found")
   228  	}
   229  	if loginCookie.MaxAge != -1 {
   230  		t.Errorf("Expect cookie MaxAge equals -1, %d", loginCookie.MaxAge)
   231  	}
   232  }
   233  
   234  type fakeAuthenticatedUserIdentifier struct {
   235  	login string
   236  }
   237  
   238  func (a *fakeAuthenticatedUserIdentifier) LoginForRequester(requester, token string) (string, error) {
   239  	return a.login, nil
   240  }
   241  
   242  func TestGetLogin(t *testing.T) {
   243  	cookie := sessions.NewCookieStore([]byte("secret-key"))
   244  	mockConfig := getMockConfig(cookie)
   245  	mockLogger := logrus.WithField("uni-test", "githuboauth")
   246  	mockAgent := NewAgent(mockConfig, mockLogger)
   247  	mockToken := &oauth2.Token{AccessToken: "tokentokentoken"}
   248  	mockRequest := httptest.NewRequest(http.MethodGet, "/someurl", nil)
   249  	mockSession, err := sessions.GetRegistry(mockRequest).Get(cookie, "access-token-session")
   250  	if err != nil {
   251  		t.Fatalf("Error with getting mock session: %v", err)
   252  	}
   253  	mockSession.Values["access-token"] = mockToken
   254  
   255  	login, err := mockAgent.GetLogin(mockRequest, &fakeAuthenticatedUserIdentifier{"correct-login"})
   256  	if err != nil {
   257  		t.Fatalf("Error getting login: %v", err)
   258  	}
   259  	if login != "correct-login" {
   260  		t.Fatalf("Incorrect login: %s", login)
   261  	}
   262  }
   263  
   264  func TestHandleRedirectWithInvalidState(t *testing.T) {
   265  	gob.Register(&oauth2.Token{})
   266  	cookie := sessions.NewCookieStore([]byte("secret-key"))
   267  	mockConfig := getMockConfig(cookie)
   268  	mockLogger := logrus.WithField("uni-test", "githuboauth")
   269  	mockAgent := NewAgent(mockConfig, mockLogger)
   270  	mockOAuthClient := mockOAuthClient{}
   271  	mockStateToken := createMockStateToken(mockConfig)
   272  
   273  	mockRequest := httptest.NewRequest(http.MethodGet, "/mock-login", nil)
   274  	mockResponse := httptest.NewRecorder()
   275  	query := mockRequest.URL.Query()
   276  	query.Add("state", "bad-state-token")
   277  	mockRequest.URL.RawQuery = query.Encode()
   278  	mockSession, err := sessions.GetRegistry(mockRequest).Get(cookie, oauthSessionCookie)
   279  	if err != nil {
   280  		t.Fatalf("Error with getting mock session: %v", err)
   281  	}
   282  	mockSession.Values[stateKey] = mockStateToken
   283  
   284  	handleLoginFn := mockAgent.HandleRedirect(mockOAuthClient, &fakeAuthenticatedUserIdentifier{""}, false)
   285  	handleLoginFn.ServeHTTP(mockResponse, mockRequest)
   286  	result := mockResponse.Result()
   287  
   288  	if result.StatusCode != http.StatusInternalServerError {
   289  		t.Errorf("Invalid status code. Got %v, expected %v", result.StatusCode, http.StatusInternalServerError)
   290  	}
   291  }
   292  
   293  func TestHandleRedirectWithValidState(t *testing.T) {
   294  	gob.Register(&oauth2.Token{})
   295  	cookie := sessions.NewCookieStore([]byte("secret-key"))
   296  	mockConfig := getMockConfig(cookie)
   297  	mockLogger := logrus.WithField("uni-test", "githuboauth")
   298  	mockAgent := NewAgent(mockConfig, mockLogger)
   299  	mockLogin := "foo_name"
   300  	mockOAuthClient := mockOAuthClient{}
   301  	mockStateToken := createMockStateToken(mockConfig)
   302  
   303  	dest := "somewhere"
   304  	rerunStatus := "working"
   305  	mockRequest := httptest.NewRequest(http.MethodGet, "/mock-login?dest="+dest+"?rerun="+rerunStatus, nil)
   306  	mockResponse := httptest.NewRecorder()
   307  	query := mockRequest.URL.Query()
   308  	query.Add("state", mockStateToken)
   309  	query.Add("rerun", "working")
   310  	mockRequest.URL.RawQuery = query.Encode()
   311  
   312  	mockSession, err := sessions.GetRegistry(mockRequest).Get(cookie, oauthSessionCookie)
   313  	if err != nil {
   314  		t.Fatalf("Error with getting mock session: %v", err)
   315  	}
   316  	mockSession.Values[stateKey] = mockStateToken
   317  
   318  	handleLoginFn := mockAgent.HandleRedirect(mockOAuthClient, &fakeAuthenticatedUserIdentifier{mockLogin}, false)
   319  	handleLoginFn.ServeHTTP(mockResponse, mockRequest)
   320  	result := mockResponse.Result()
   321  	if result.StatusCode != http.StatusFound {
   322  		t.Errorf("Invalid status code. Got %v, expected %v", result.StatusCode, http.StatusFound)
   323  	}
   324  	resultCookies := result.Cookies()
   325  	var oauthCookie *http.Cookie
   326  	for _, v := range resultCookies {
   327  		if v.Name == tokenSession {
   328  			oauthCookie = v
   329  			break
   330  		}
   331  	}
   332  	if oauthCookie == nil {
   333  		t.Fatalf("Cookie for oauth session not found")
   334  	}
   335  	decodedCookie := make(map[interface{}]interface{})
   336  	if err := securecookie.DecodeMulti(oauthCookie.Name, oauthCookie.Value, &decodedCookie, cookie.Codecs...); err != nil {
   337  		t.Fatalf("Cannot decoded cookie: %v", err)
   338  	}
   339  	accessTokenFromCookie, ok := decodedCookie[tokenKey].(*oauth2.Token)
   340  	if !ok {
   341  		t.Fatalf("Error with getting access token: %v", decodedCookie)
   342  	}
   343  	token := &oauth2.Token{
   344  		AccessToken: mockAccessToken,
   345  	}
   346  	if !isEqual(accessTokenFromCookie, token) {
   347  		t.Errorf("Invalid access token. Got %v, expected %v", accessTokenFromCookie, token)
   348  	}
   349  	var loginCookie *http.Cookie
   350  	for _, v := range resultCookies {
   351  		if v.Name == loginSession {
   352  			loginCookie = v
   353  			break
   354  		}
   355  	}
   356  	if loginCookie == nil {
   357  		t.Fatalf("Cookie for github login not found")
   358  	}
   359  	if loginCookie.Value != mockLogin {
   360  		t.Errorf("Mismatch github login. Got %v, expected %v", loginCookie.Value, mockLogin)
   361  	}
   362  	path := mockResponse.Header().Get("Location")
   363  	if path != "http://example.com/"+dest+"?rerun="+rerunStatus {
   364  		t.Errorf("Incorrect final redirect URL. Actual path: %s, Expected path: /%s", path, dest+"?rerun="+rerunStatus)
   365  	}
   366  }