-
Notifications
You must be signed in to change notification settings - Fork 6.6k
fix(server): JWT token checks (#14930) #15004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 27 commits
fa512bc
4293e23
56fc1e5
24a3bbc
35677d6
928762d
97b5e11
a0bf6e6
b453d70
ee14927
9b8e541
d5bb535
0a7c0f1
c5301aa
39b5243
a4618be
7c98a72
ac2b9b5
6648a92
e3c6364
1a0be29
327490a
a55f129
12d668a
7fcb68f
7adca52
d42c9ad
dfa649d
6af5ad8
b24b41c
2637a2e
f148828
84e599d
f2628ef
3d957d2
7e22d09
7a4e873
71748bf
e202055
ff685c4
6cbd501
fc90a95
c656b3a
00f6c2b
1c80b69
904d927
bc98b19
5853c0b
bdf0271
694219f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,21 @@ | ||||||||||||||||||||||||||||||||||||||
| package commands | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||
| "crypto/tls" | ||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| log "github.com/sirupsen/logrus" | ||||||||||||||||||||||||||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| "github.com/argoproj/argo-cd/v2/common" | ||||||||||||||||||||||||||||||||||||||
| argocdclient "github.com/argoproj/argo-cd/v2/pkg/apiclient" | ||||||||||||||||||||||||||||||||||||||
| "github.com/argoproj/argo-cd/v2/util/cli" | ||||||||||||||||||||||||||||||||||||||
| "github.com/argoproj/argo-cd/v2/util/errors" | ||||||||||||||||||||||||||||||||||||||
| grpc_util "github.com/argoproj/argo-cd/v2/util/grpc" | ||||||||||||||||||||||||||||||||||||||
| "github.com/argoproj/argo-cd/v2/util/localconfig" | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -35,6 +42,55 @@ $ argocd logout | |||||||||||||||||||||||||||||||||||||
| log.Fatalf("Nothing to logout from") | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| token := localCfg.GetToken(context) | ||||||||||||||||||||||||||||||||||||||
| if token == "" { | ||||||||||||||||||||||||||||||||||||||
| log.Fatalf("Error in getting token from context") | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| client := &http.Client{} | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| dialTime := 30 * time.Second | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| tlsTestResult, err := grpc_util.TestTLS(context, dialTime) | ||||||||||||||||||||||||||||||||||||||
| errors.CheckError(err) | ||||||||||||||||||||||||||||||||||||||
| if !tlsTestResult.TLS { | ||||||||||||||||||||||||||||||||||||||
| if !globalClientOpts.PlainText { | ||||||||||||||||||||||||||||||||||||||
| if !cli.AskToProceed("WARNING: server is not configured with TLS. Proceed (y/n)? ") { | ||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| globalClientOpts.PlainText = true | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } else if tlsTestResult.InsecureErr != nil { | ||||||||||||||||||||||||||||||||||||||
| if !globalClientOpts.Insecure { | ||||||||||||||||||||||||||||||||||||||
| if !cli.AskToProceed(fmt.Sprintf("WARNING: server certificate had error: %s. Proceed insecurely (y/n)? ", tlsTestResult.InsecureErr)) { | ||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| globalClientOpts.Insecure = true | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| scheme := "https" | ||||||||||||||||||||||||||||||||||||||
| if globalClientOpts.PlainText { | ||||||||||||||||||||||||||||||||||||||
| scheme = strings.TrimSuffix(scheme, "s") | ||||||||||||||||||||||||||||||||||||||
| } else if globalClientOpts.Insecure { | ||||||||||||||||||||||||||||||||||||||
| client.Transport = &http.Transport{ | ||||||||||||||||||||||||||||||||||||||
| TLSClientConfig: &tls.Config{ | ||||||||||||||||||||||||||||||||||||||
| InsecureSkipVerify: true, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| dialTime := 30 * time.Second | |
| tlsTestResult, err := grpc_util.TestTLS(server, dialTime) | |
| errors.CheckError(err) | |
| if !tlsTestResult.TLS { | |
| if !globalClientOpts.PlainText { | |
| if !cli.AskToProceed("WARNING: server is not configured with TLS. Proceed (y/n)? ") { | |
| os.Exit(1) | |
| } | |
| globalClientOpts.PlainText = true | |
| } | |
| } else if tlsTestResult.InsecureErr != nil { | |
| if !globalClientOpts.Insecure { | |
| if !cli.AskToProceed(fmt.Sprintf("WARNING: server certificate had error: %s. Proceed insecurely (y/n)? ", tlsTestResult.InsecureErr)) { | |
| os.Exit(1) | |
| } | |
| globalClientOpts.Insecure = true | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This http call needs to be made after reconfirming from the user, needs to be moved after checking thecanLogout.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invalidate the current token with /logout request, mostly used because it adds the token's ID to the Redis in a revokation list with the remaining revoked tokens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method will cause a fatal error and cause the program to terminate abruptly. If the remote server is unavailable or errors out due to network issue, it is probably its better to give a warning and logout locally and clean up the local storage.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -109,14 +109,19 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| return | ||
| } | ||
|
|
||
| issuer := jwtutil.StringField(mapClaims, "iss") | ||
| id := jwtutil.StringField(mapClaims, "jti") | ||
| // Workaround for Dex token, because does not have jti. | ||
| if id == "" { | ||
| id = jwtutil.StringField(mapClaims, "at_hash") | ||
| } | ||
|
Comment on lines
+110
to
+113
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using "at_hash" as "jti" because Dex token does not have a "jti" and it leads to not working revokation of Dex tokens. I was not able to add "jti" to the Dex token when it is created. |
||
|
|
||
| if exp, err := jwtutil.ExpirationTime(mapClaims); err == nil && id != "" { | ||
| if err := h.revokeToken(context.Background(), id, time.Until(exp)); err != nil { | ||
| log.Warnf("failed to invalidate token '%s': %v", id, err) | ||
| } | ||
| } | ||
|
|
||
| issuer := jwtutil.StringField(mapClaims, "iss") | ||
| if argoCDSettings.OIDCConfig() == nil || argoCDSettings.OIDCConfig().LogoutURL == "" || issuer == session.SessionManagerClaimsIssuer { | ||
| http.Redirect(w, r, logoutRedirectURL, http.StatusSeeOther) | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,9 +32,11 @@ var ( | |
| baseLogoutURLwithRedirectURL = "http://localhost:4000/logout?post_logout_redirect_uri={{logoutRedirectURL}}" | ||
| baseLogoutURLwithTokenAndRedirectURL = "http://localhost:4000/logout?id_token_hint={{token}}&post_logout_redirect_uri={{logoutRedirectURL}}" | ||
| invalidToken = "sample-token" | ||
| dexToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ijc5YTFlNTgzOWM2ZTRjMTZlOTIwYzM1YTU0MmMwNmZhIn0.eyJzdWIiOiIwMHVqNnM1NDVyNU5peVNLcjVkNSIsIm5hbWUiOiJqZCByIiwiZW1haWwiOiJqYWlkZWVwMTdydWx6QGdtYWlsLmNvbSIsInZlciI6MSwiaXNzIjoiaHR0cHM6Ly9kZXYtNTY5NTA5OC5va3RhLmNvbSIsImF1ZCI6IjBvYWowM2FmSEtqN3laWXJwNWQ1IiwiaWF0IjoxNjA1NTcyMzU5LCJleHAiOjE2MDU1NzU5NTksImFtciI6WyJwd2QiXSwiaWRwIjoiMDBvaWdoZmZ2SlFMNjNaOGg1ZDUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqYWlkZWVwMTdydWx6QGdtYWlsLmNvbSIsImF1dGhfdGltZSI6MTYwNTU3MjM1NywiYXRfaGFzaCI6ImplUTBGaXZqT2c0YjZNSldEMjE5bGcifQ.Xt_5G-4dNZef1egOYmvruszudlAvUXVQzqrI4YwkWJeZ0zZDk4lyhPUVuxVGjB3pCCUCUMloTL6xC7IVFNj53Eb7WNH_hxsFqemJ80HZYbUpo2G9fMjkPmFTaeFVMC4p3qxIaBAT9_uJbTRSyRGYLV-95KDpU-GNDFXlbFq-2bVvhppiYmKszyHbREZkB87Pi7K3Bk0NxAlDOJ7O5lhwjpwuOJ1WGCJptUetePm5MnpVT2ZCyjvntlzwHlIhMSKNlFZuFS_JMca5Ww0fQSBUlarQU9MMyZKBw-QuD5sJw3xjwQpxOG-T9mJz7F8VA5znLi_LJNutHVgcpt3T_TW_0NbgqsHe8Lw" | ||
| oidcToken = "eyJraWQiOiJYQi1MM3ZFdHhYWXJLcmRSQnVEV0NwdnZsSnk3SEJVb2d5N253M1U1Z1ZZIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIwMHVqNnM1NDVyNU5peVNLcjVkNSIsIm5hbWUiOiJqZCByIiwiZW1haWwiOiJqYWlkZWVwMTdydWx6QGdtYWlsLmNvbSIsInZlciI6MSwiaXNzIjoiaHR0cHM6Ly9kZXYtNTY5NTA5OC5va3RhLmNvbSIsImF1ZCI6IjBvYWowM2FmSEtqN3laWXJwNWQ1IiwiaWF0IjoxNjA1NTcyMzU5LCJleHAiOjE2MDU1NzU5NTksImp0aSI6IklELl9ORDJxVG5iREFtc3hIZUt2U2ZHeVBqTXRicXFEQXdkdlRQTDZCTnpfR3ciLCJhbXIiOlsicHdkIl0sImlkcCI6IjAwb2lnaGZmdkpRTDYzWjhoNWQ1IiwicHJlZmVycmVkX3VzZXJuYW1lIjoiamFpZGVlcDE3cnVsekBnbWFpbC5jb20iLCJhdXRoX3RpbWUiOjE2MDU1NzIzNTcsImF0X2hhc2giOiJqZVEwRml2ak9nNGI2TUpXRDIxOWxnIn0.GHkqwXgW-lrAhJdypW7SVjW0YdNLFQiRL8iwgT6DHJxP9Nb0OtkH2NKcBYAA5N6bTPLRQUHgYwWcgm5zSXmvqa7ciIgPF3tiQI8UmJA9VFRRDR-x9ExX15nskCbXfiQ67MriLslUrQUyzSCfUrSjXKwnDxbKGQncrtmRsh5asfCzJFb9excn311W9HKbT3KA0Ot7eOMnVS6V7SGfXxnKs6szcXIEMa_FhB4zDAVLr-dnxvSG_uuWcHrAkLTUVhHbdQQXF7hXIEfyr5lkMJN-drjdz-bn40GaYulEmUvO1bjcL9toCVQ3Ismypyr0b8phj4w3uRsLDZQxTxK7jAXlyQ" | ||
| nonOidcToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MDU1NzQyMTIsImlzcyI6ImFyZ29jZCIsIm5iZiI6MTYwNTU3NDIxMiwic3ViIjoiYWRtaW4ifQ.zDJ4piwWnwsHON-oPusHMXWINlnrRDTQykYogT7afeE" | ||
| expectedNonOIDCLogoutURL = "http://localhost:4000" | ||
| expectedDexLogoutURL = "http://localhost:4000" | ||
| expectedOIDCLogoutURL = "https://dev-5695098.okta.com/oauth2/v1/logout?id_token_hint=" + oidcToken + "&post_logout_redirect_uri=" + baseURL | ||
| expectedOIDCLogoutURLWithRootPath = "https://dev-5695098.okta.com/oauth2/v1/logout?id_token_hint=" + oidcToken + "&post_logout_redirect_uri=" + baseURL + "/" + rootPath | ||
| ) | ||
|
|
@@ -85,6 +87,40 @@ func TestConstructLogoutURL(t *testing.T) { | |
| } | ||
|
|
||
| func TestHandlerConstructLogoutURL(t *testing.T) { | ||
| kubeClientWithDexConfig := fake.NewSimpleClientset( | ||
| &corev1.ConfigMap{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: common.ArgoCDConfigMapName, | ||
| Namespace: "default", | ||
| Labels: map[string]string{ | ||
| "app.kubernetes.io/part-of": "argocd", | ||
| }, | ||
| }, | ||
| Data: map[string]string{ | ||
| "dex.config": "connectors: \n" + | ||
| "- type: dev \n" + | ||
| "name: Dev \n" + | ||
| "config: \n" + | ||
| "issuer: https://dev-5695098.okta.com \n" + | ||
| "clientID: aabbccddeeff00112233 \n" + | ||
| "clientSecret: aabbccddeeff00112233", | ||
| "url": "http://localhost:4000", | ||
| }, | ||
| }, | ||
| &corev1.Secret{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: common.ArgoCDSecretName, | ||
| Namespace: "default", | ||
| Labels: map[string]string{ | ||
| "app.kubernetes.io/part-of": "argocd", | ||
| }, | ||
| }, | ||
| Data: map[string][]byte{ | ||
| "admin.password": nil, | ||
| "server.secretkey": nil, | ||
| }, | ||
| }, | ||
| ) | ||
| kubeClientWithOIDCConfig := fake.NewSimpleClientset( | ||
| &corev1.ConfigMap{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
|
|
@@ -208,13 +244,23 @@ func TestHandlerConstructLogoutURL(t *testing.T) { | |
| }, | ||
| ) | ||
|
|
||
| settingsManagerWithDexConfig := settings.NewSettingsManager(context.Background(), kubeClientWithDexConfig, "default") | ||
| settingsManagerWithOIDCConfig := settings.NewSettingsManager(context.Background(), kubeClientWithOIDCConfig, "default") | ||
| settingsManagerWithoutOIDCConfig := settings.NewSettingsManager(context.Background(), kubeClientWithoutOIDCConfig, "default") | ||
| settingsManagerWithOIDCConfigButNoLogoutURL := settings.NewSettingsManager(context.Background(), kubeClientWithOIDCConfigButNoLogoutURL, "default") | ||
| settingsManagerWithOIDCConfigButNoURL := settings.NewSettingsManager(context.Background(), kubeClientWithOIDCConfigButNoURL, "default") | ||
|
|
||
| sessionManager := session.NewSessionManager(settingsManagerWithOIDCConfig, test.NewFakeProjLister(), "", nil, session.NewUserStateStorage(nil)) | ||
| redisClient, closer := test.NewInMemoryRedis() | ||
| defer closer() | ||
| sessionManager := session.NewSessionManager(settingsManagerWithOIDCConfig, test.NewFakeProjLister(), "", nil, session.NewUserStateStorage(redisClient)) | ||
|
|
||
| dexHandler := NewHandler(appclientset.NewSimpleClientset(), settingsManagerWithDexConfig, sessionManager, rootPath, baseHRef, "default") | ||
| dexHandler.verifyToken = func(tokenString string) (jwt.Claims, string, error) { | ||
| if !validJWTPattern.MatchString(tokenString) { | ||
| return nil, "", errors.New("invalid jwt") | ||
| } | ||
| return &jwt.RegisteredClaims{Issuer: "dev"}, "", nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is this intended?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is used for testing. |
||
| } | ||
| oidcHandler := NewHandler(appclientset.NewSimpleClientset(), settingsManagerWithOIDCConfig, sessionManager, rootPath, baseHRef, "default") | ||
| oidcHandler.verifyToken = func(tokenString string) (jwt.Claims, string, error) { | ||
| if !validJWTPattern.MatchString(tokenString) { | ||
|
|
@@ -236,21 +282,26 @@ func TestHandlerConstructLogoutURL(t *testing.T) { | |
| } | ||
| return &jwt.RegisteredClaims{Issuer: "okta"}, "", nil | ||
| } | ||
|
|
||
| oidcHandlerWithoutBaseURL := NewHandler(appclientset.NewSimpleClientset(), settingsManagerWithOIDCConfigButNoURL, sessionManager, "argocd", baseHRef, "default") | ||
| oidcHandlerWithoutBaseURL.verifyToken = func(tokenString string) (jwt.Claims, string, error) { | ||
| if !validJWTPattern.MatchString(tokenString) { | ||
| return nil, "", errors.New("invalid jwt") | ||
| } | ||
| return &jwt.RegisteredClaims{Issuer: "okta"}, "", nil | ||
| } | ||
|
|
||
| dexTokenHeader := make(map[string][]string) | ||
| dexTokenHeader["Cookie"] = []string{"argocd.token=" + dexToken} | ||
| oidcTokenHeader := make(map[string][]string) | ||
| oidcTokenHeader["Cookie"] = []string{"argocd.token=" + oidcToken} | ||
| nonOidcTokenHeader := make(map[string][]string) | ||
| nonOidcTokenHeader["Cookie"] = []string{"argocd.token=" + nonOidcToken} | ||
| invalidHeader := make(map[string][]string) | ||
| invalidHeader["Cookie"] = []string{"argocd.token=" + invalidToken} | ||
|
|
||
| dexRequest, err := http.NewRequest(http.MethodGet, "http://localhost:4000/api/logout", nil) | ||
| assert.NoError(t, err) | ||
| dexRequest.Header = dexTokenHeader | ||
| oidcRequest, err := http.NewRequest(http.MethodGet, "http://localhost:4000/api/logout", nil) | ||
| assert.NoError(t, err) | ||
| oidcRequest.Header = oidcTokenHeader | ||
|
|
@@ -273,6 +324,14 @@ func TestHandlerConstructLogoutURL(t *testing.T) { | |
| expectedLogoutURL string | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "Case: Dex logout request with valid token", | ||
| handler: dexHandler, | ||
| request: dexRequest, | ||
| responseRecorder: httptest.NewRecorder(), | ||
| expectedLogoutURL: expectedDexLogoutURL, | ||
| wantErr: false, | ||
| }, | ||
| { | ||
| name: "Case: OIDC logout request with valid token", | ||
| handler: oidcHandler, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting JWT token for the current context from the file system at local config file.