github.com/argoproj/argo-cd/v2@v2.10.9/server/logout/logout_test.go (about) 1 package logout 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/http/httptest" 9 "regexp" 10 "testing" 11 12 "github.com/argoproj/argo-cd/v2/common" 13 appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake" 14 "github.com/argoproj/argo-cd/v2/test" 15 "github.com/argoproj/argo-cd/v2/util/session" 16 "github.com/argoproj/argo-cd/v2/util/settings" 17 18 "github.com/golang-jwt/jwt/v4" 19 "github.com/stretchr/testify/assert" 20 corev1 "k8s.io/api/core/v1" 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/client-go/kubernetes/fake" 23 ) 24 25 var ( 26 validJWTPattern = regexp.MustCompile(`[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+`) 27 baseURL = "http://localhost:4000" 28 rootPath = "argocd" 29 baseHRef = "argocd" 30 baseLogoutURL = "http://localhost:4000/logout" 31 baseLogoutURLwithToken = "http://localhost:4000/logout?id_token_hint={{token}}" 32 baseLogoutURLwithRedirectURL = "http://localhost:4000/logout?post_logout_redirect_uri={{logoutRedirectURL}}" 33 baseLogoutURLwithTokenAndRedirectURL = "http://localhost:4000/logout?id_token_hint={{token}}&post_logout_redirect_uri={{logoutRedirectURL}}" 34 invalidToken = "sample-token" 35 oidcToken = "eyJraWQiOiJYQi1MM3ZFdHhYWXJLcmRSQnVEV0NwdnZsSnk3SEJVb2d5N253M1U1Z1ZZIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIwMHVqNnM1NDVyNU5peVNLcjVkNSIsIm5hbWUiOiJqZCByIiwiZW1haWwiOiJqYWlkZWVwMTdydWx6QGdtYWlsLmNvbSIsInZlciI6MSwiaXNzIjoiaHR0cHM6Ly9kZXYtNTY5NTA5OC5va3RhLmNvbSIsImF1ZCI6IjBvYWowM2FmSEtqN3laWXJwNWQ1IiwiaWF0IjoxNjA1NTcyMzU5LCJleHAiOjE2MDU1NzU5NTksImp0aSI6IklELl9ORDJxVG5iREFtc3hIZUt2U2ZHeVBqTXRicXFEQXdkdlRQTDZCTnpfR3ciLCJhbXIiOlsicHdkIl0sImlkcCI6IjAwb2lnaGZmdkpRTDYzWjhoNWQ1IiwicHJlZmVycmVkX3VzZXJuYW1lIjoiamFpZGVlcDE3cnVsekBnbWFpbC5jb20iLCJhdXRoX3RpbWUiOjE2MDU1NzIzNTcsImF0X2hhc2giOiJqZVEwRml2ak9nNGI2TUpXRDIxOWxnIn0.GHkqwXgW-lrAhJdypW7SVjW0YdNLFQiRL8iwgT6DHJxP9Nb0OtkH2NKcBYAA5N6bTPLRQUHgYwWcgm5zSXmvqa7ciIgPF3tiQI8UmJA9VFRRDR-x9ExX15nskCbXfiQ67MriLslUrQUyzSCfUrSjXKwnDxbKGQncrtmRsh5asfCzJFb9excn311W9HKbT3KA0Ot7eOMnVS6V7SGfXxnKs6szcXIEMa_FhB4zDAVLr-dnxvSG_uuWcHrAkLTUVhHbdQQXF7hXIEfyr5lkMJN-drjdz-bn40GaYulEmUvO1bjcL9toCVQ3Ismypyr0b8phj4w3uRsLDZQxTxK7jAXlyQ" 36 nonOidcToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MDU1NzQyMTIsImlzcyI6ImFyZ29jZCIsIm5iZiI6MTYwNTU3NDIxMiwic3ViIjoiYWRtaW4ifQ.zDJ4piwWnwsHON-oPusHMXWINlnrRDTQykYogT7afeE" 37 expectedNonOIDCLogoutURL = "http://localhost:4000" 38 expectedOIDCLogoutURL = "https://dev-5695098.okta.com/oauth2/v1/logout?id_token_hint=" + oidcToken + "&post_logout_redirect_uri=" + baseURL 39 expectedOIDCLogoutURLWithRootPath = "https://dev-5695098.okta.com/oauth2/v1/logout?id_token_hint=" + oidcToken + "&post_logout_redirect_uri=" + baseURL + "/" + rootPath 40 ) 41 42 func TestConstructLogoutURL(t *testing.T) { 43 tests := []struct { 44 name string 45 logoutURL string 46 token string 47 logoutRedirectURL string 48 expectedLogoutURL string 49 }{ 50 { 51 name: "Case: No additional parameters passed to logout URL", 52 logoutURL: baseLogoutURL, 53 token: oidcToken, 54 logoutRedirectURL: baseURL, 55 expectedLogoutURL: baseLogoutURL, 56 }, 57 { 58 name: "Case: ID token passed to logout URL", 59 logoutURL: baseLogoutURLwithToken, 60 token: oidcToken, 61 logoutRedirectURL: baseURL, 62 expectedLogoutURL: "http://localhost:4000/logout?id_token_hint=" + oidcToken, 63 }, 64 { 65 name: "Case: Redirect required", 66 logoutURL: baseLogoutURLwithRedirectURL, 67 token: oidcToken, 68 logoutRedirectURL: baseURL, 69 expectedLogoutURL: "http://localhost:4000/logout?post_logout_redirect_uri=" + baseURL, 70 }, 71 { 72 name: "Case: ID token and redirect URL passed to logout URL", 73 logoutURL: baseLogoutURLwithTokenAndRedirectURL, 74 token: oidcToken, 75 logoutRedirectURL: baseURL, 76 expectedLogoutURL: "http://localhost:4000/logout?id_token_hint=" + oidcToken + "&post_logout_redirect_uri=" + baseURL, 77 }, 78 } 79 for _, tt := range tests { 80 t.Run(tt.name, func(t *testing.T) { 81 constructedLogoutURL := constructLogoutURL(tt.logoutURL, tt.token, tt.logoutRedirectURL) 82 assert.Equal(t, constructedLogoutURL, tt.expectedLogoutURL) 83 }) 84 } 85 } 86 87 func TestHandlerConstructLogoutURL(t *testing.T) { 88 kubeClientWithOIDCConfig := fake.NewSimpleClientset( 89 &corev1.ConfigMap{ 90 ObjectMeta: metav1.ObjectMeta{ 91 Name: common.ArgoCDConfigMapName, 92 Namespace: "default", 93 Labels: map[string]string{ 94 "app.kubernetes.io/part-of": "argocd", 95 }, 96 }, 97 Data: map[string]string{ 98 "oidc.config": "name: Okta \n" + 99 "issuer: https://dev-5695098.okta.com \n" + 100 "requestedScopes: [\"openid\", \"profile\", \"email\", \"groups\"] \n" + 101 "requestedIDTokenClaims: {\"groups\": {\"essential\": true}} \n" + 102 "logoutURL: https://dev-5695098.okta.com/oauth2/v1/logout?id_token_hint={{token}}&post_logout_redirect_uri={{logoutRedirectURL}}", 103 "url": "http://localhost:4000", 104 }, 105 }, 106 &corev1.Secret{ 107 ObjectMeta: metav1.ObjectMeta{ 108 Name: common.ArgoCDSecretName, 109 Namespace: "default", 110 Labels: map[string]string{ 111 "app.kubernetes.io/part-of": "argocd", 112 }, 113 }, 114 Data: map[string][]byte{ 115 "admin.password": nil, 116 "server.secretkey": nil, 117 }, 118 }, 119 ) 120 kubeClientWithOIDCConfigButNoURL := fake.NewSimpleClientset( 121 &corev1.ConfigMap{ 122 ObjectMeta: metav1.ObjectMeta{ 123 Name: common.ArgoCDConfigMapName, 124 Namespace: "default", 125 Labels: map[string]string{ 126 "app.kubernetes.io/part-of": "argocd", 127 }, 128 }, 129 Data: map[string]string{ 130 "oidc.config": "name: Okta \n" + 131 "issuer: https://dev-5695098.okta.com \n" + 132 "requestedScopes: [\"openid\", \"profile\", \"email\", \"groups\"] \n" + 133 "requestedIDTokenClaims: {\"groups\": {\"essential\": true}} \n" + 134 "logoutURL: https://dev-5695098.okta.com/oauth2/v1/logout?id_token_hint={{token}}&post_logout_redirect_uri={{logoutRedirectURL}}", 135 "url": "", 136 }, 137 }, 138 &corev1.Secret{ 139 ObjectMeta: metav1.ObjectMeta{ 140 Name: common.ArgoCDSecretName, 141 Namespace: "default", 142 Labels: map[string]string{ 143 "app.kubernetes.io/part-of": "argocd", 144 }, 145 }, 146 Data: map[string][]byte{ 147 "admin.password": nil, 148 "server.secretkey": nil, 149 }, 150 }, 151 ) 152 kubeClientWithOIDCConfigButNoLogoutURL := fake.NewSimpleClientset( 153 &corev1.ConfigMap{ 154 ObjectMeta: metav1.ObjectMeta{ 155 Name: common.ArgoCDConfigMapName, 156 Namespace: "default", 157 Labels: map[string]string{ 158 "app.kubernetes.io/part-of": "argocd", 159 }, 160 }, 161 Data: map[string]string{ 162 "oidc.config": "name: Okta \n" + 163 "issuer: https://dev-5695098.okta.com \n" + 164 "requestedScopes: [\"openid\", \"profile\", \"email\", \"groups\"] \n" + 165 "requestedIDTokenClaims: {\"groups\": {\"essential\": true}} \n", 166 "url": "http://localhost:4000", 167 }, 168 }, 169 &corev1.Secret{ 170 ObjectMeta: metav1.ObjectMeta{ 171 Name: common.ArgoCDSecretName, 172 Namespace: "default", 173 Labels: map[string]string{ 174 "app.kubernetes.io/part-of": "argocd", 175 }, 176 }, 177 Data: map[string][]byte{ 178 "admin.password": nil, 179 "server.secretkey": nil, 180 }, 181 }, 182 ) 183 kubeClientWithoutOIDCConfig := fake.NewSimpleClientset( 184 &corev1.ConfigMap{ 185 ObjectMeta: metav1.ObjectMeta{ 186 Name: common.ArgoCDConfigMapName, 187 Namespace: "default", 188 Labels: map[string]string{ 189 "app.kubernetes.io/part-of": "argocd", 190 }, 191 }, 192 Data: map[string]string{ 193 "url": "http://localhost:4000", 194 }, 195 }, 196 &corev1.Secret{ 197 ObjectMeta: metav1.ObjectMeta{ 198 Name: common.ArgoCDSecretName, 199 Namespace: "default", 200 Labels: map[string]string{ 201 "app.kubernetes.io/part-of": "argocd", 202 }, 203 }, 204 Data: map[string][]byte{ 205 "admin.password": nil, 206 "server.secretkey": nil, 207 }, 208 }, 209 ) 210 211 settingsManagerWithOIDCConfig := settings.NewSettingsManager(context.Background(), kubeClientWithOIDCConfig, "default") 212 settingsManagerWithoutOIDCConfig := settings.NewSettingsManager(context.Background(), kubeClientWithoutOIDCConfig, "default") 213 settingsManagerWithOIDCConfigButNoLogoutURL := settings.NewSettingsManager(context.Background(), kubeClientWithOIDCConfigButNoLogoutURL, "default") 214 settingsManagerWithOIDCConfigButNoURL := settings.NewSettingsManager(context.Background(), kubeClientWithOIDCConfigButNoURL, "default") 215 216 sessionManager := session.NewSessionManager(settingsManagerWithOIDCConfig, test.NewFakeProjLister(), "", nil, session.NewUserStateStorage(nil)) 217 218 oidcHandler := NewHandler(appclientset.NewSimpleClientset(), settingsManagerWithOIDCConfig, sessionManager, rootPath, baseHRef, "default") 219 oidcHandler.verifyToken = func(tokenString string) (jwt.Claims, string, error) { 220 if !validJWTPattern.MatchString(tokenString) { 221 return nil, "", errors.New("invalid jwt") 222 } 223 return &jwt.RegisteredClaims{Issuer: "okta"}, "", nil 224 } 225 nonoidcHandler := NewHandler(appclientset.NewSimpleClientset(), settingsManagerWithoutOIDCConfig, sessionManager, "", baseHRef, "default") 226 nonoidcHandler.verifyToken = func(tokenString string) (jwt.Claims, string, error) { 227 if !validJWTPattern.MatchString(tokenString) { 228 return nil, "", errors.New("invalid jwt") 229 } 230 return &jwt.RegisteredClaims{Issuer: session.SessionManagerClaimsIssuer}, "", nil 231 } 232 oidcHandlerWithoutLogoutURL := NewHandler(appclientset.NewSimpleClientset(), settingsManagerWithOIDCConfigButNoLogoutURL, sessionManager, "", baseHRef, "default") 233 oidcHandlerWithoutLogoutURL.verifyToken = func(tokenString string) (jwt.Claims, string, error) { 234 if !validJWTPattern.MatchString(tokenString) { 235 return nil, "", errors.New("invalid jwt") 236 } 237 return &jwt.RegisteredClaims{Issuer: "okta"}, "", nil 238 } 239 240 oidcHandlerWithoutBaseURL := NewHandler(appclientset.NewSimpleClientset(), settingsManagerWithOIDCConfigButNoURL, sessionManager, "argocd", baseHRef, "default") 241 oidcHandlerWithoutBaseURL.verifyToken = func(tokenString string) (jwt.Claims, string, error) { 242 if !validJWTPattern.MatchString(tokenString) { 243 return nil, "", errors.New("invalid jwt") 244 } 245 return &jwt.RegisteredClaims{Issuer: "okta"}, "", nil 246 } 247 oidcTokenHeader := make(map[string][]string) 248 oidcTokenHeader["Cookie"] = []string{"argocd.token=" + oidcToken} 249 nonOidcTokenHeader := make(map[string][]string) 250 nonOidcTokenHeader["Cookie"] = []string{"argocd.token=" + nonOidcToken} 251 invalidHeader := make(map[string][]string) 252 invalidHeader["Cookie"] = []string{"argocd.token=" + invalidToken} 253 254 oidcRequest, err := http.NewRequest(http.MethodGet, "http://localhost:4000/api/logout", nil) 255 assert.NoError(t, err) 256 oidcRequest.Header = oidcTokenHeader 257 nonoidcRequest, err := http.NewRequest(http.MethodGet, "http://localhost:4000/api/logout", nil) 258 assert.NoError(t, err) 259 nonoidcRequest.Header = nonOidcTokenHeader 260 assert.NoError(t, err) 261 requestWithInvalidToken, err := http.NewRequest(http.MethodGet, "http://localhost:4000/api/logout", nil) 262 assert.NoError(t, err) 263 requestWithInvalidToken.Header = invalidHeader 264 invalidRequest, err := http.NewRequest(http.MethodGet, "http://localhost:4000/api/logout", nil) 265 assert.NoError(t, err) 266 267 tests := []struct { 268 name string 269 kubeClient *fake.Clientset 270 handler http.Handler 271 request *http.Request 272 responseRecorder *httptest.ResponseRecorder 273 expectedLogoutURL string 274 wantErr bool 275 }{ 276 { 277 name: "Case: OIDC logout request with valid token", 278 handler: oidcHandler, 279 request: oidcRequest, 280 responseRecorder: httptest.NewRecorder(), 281 expectedLogoutURL: expectedOIDCLogoutURL, 282 wantErr: false, 283 }, 284 { 285 name: "Case: OIDC logout request with valid token but missing URL", 286 handler: oidcHandlerWithoutBaseURL, 287 request: oidcRequest, 288 responseRecorder: httptest.NewRecorder(), 289 expectedLogoutURL: expectedOIDCLogoutURLWithRootPath, 290 wantErr: false, 291 }, 292 { 293 name: "Case: non-OIDC logout request with valid token", 294 handler: nonoidcHandler, 295 request: nonoidcRequest, 296 responseRecorder: httptest.NewRecorder(), 297 expectedLogoutURL: expectedNonOIDCLogoutURL, 298 wantErr: false, 299 }, 300 { 301 name: "Case: Logout request with invalid token", 302 handler: nonoidcHandler, 303 request: requestWithInvalidToken, 304 responseRecorder: httptest.NewRecorder(), 305 expectedLogoutURL: expectedNonOIDCLogoutURL, 306 wantErr: false, 307 }, 308 { 309 name: "Case: Logout request with missing token", 310 handler: oidcHandler, 311 request: invalidRequest, 312 responseRecorder: httptest.NewRecorder(), 313 expectedLogoutURL: expectedNonOIDCLogoutURL, 314 wantErr: true, 315 }, 316 { 317 name: "Case:OIDC Logout request with missing logout URL configuration in config map", 318 handler: oidcHandlerWithoutLogoutURL, 319 request: oidcRequest, 320 responseRecorder: httptest.NewRecorder(), 321 expectedLogoutURL: expectedNonOIDCLogoutURL, 322 wantErr: false, 323 }, 324 } 325 for _, tt := range tests { 326 t.Run(tt.name, func(t *testing.T) { 327 tt.handler.ServeHTTP(tt.responseRecorder, tt.request) 328 if status := tt.responseRecorder.Code; status != http.StatusSeeOther { 329 if !tt.wantErr { 330 t.Errorf(tt.responseRecorder.Body.String()) 331 t.Errorf("handler returned wrong status code: " + fmt.Sprintf("%d", tt.responseRecorder.Code)) 332 } 333 } else { 334 if tt.wantErr { 335 t.Errorf("expected error but did not get one") 336 } else { 337 assert.Equal(t, tt.expectedLogoutURL, tt.responseRecorder.Result().Header["Location"][0]) 338 } 339 } 340 }) 341 } 342 }