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  }