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