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 }