From fb64fad3210b90fddde32d831d144b8c09833690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 08:37:43 -0300 Subject: [PATCH 1/9] add BackendLogoutAllSessionsURL configuration --- pkg/apis/options/legacy_options.go | 5 +++++ pkg/apis/options/providers.go | 2 ++ providers/provider_data.go | 2 ++ providers/providers.go | 1 + 4 files changed, 10 insertions(+) diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index 3105528c8f..8fc5110efa 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -545,6 +545,8 @@ type LegacyProvider struct { AllowedRoles []string `flag:"allowed-role" cfg:"allowed_roles"` BackendLogoutURL string `flag:"backend-logout-url" cfg:"backend_logout_url"` + BackendLogoutAllSessionsURL string `flag:"backend-logout-all-sessions-url" cfg:"backend_logout_all_sessions_url"` + AcrValues string `flag:"acr-values" cfg:"acr_values"` JWTKey string `flag:"jwt-key" cfg:"jwt_key"` JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file"` @@ -613,6 +615,7 @@ func legacyProviderFlagSet() *pflag.FlagSet { flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)") flagSet.StringSlice("allowed-role", []string{}, "(keycloak-oidc) restrict logins to members of these roles (may be given multiple times)") flagSet.String("backend-logout-url", "", "url to perform a backend logout, {id_token} can be used as placeholder for the id_token") + flagSet.String("backend-logout-all-sessions-url", "", "url to perform a backend logout, {user_id} can be used as placeholder for the user_id") return flagSet } @@ -693,6 +696,8 @@ func (l *LegacyProvider) convert() (Providers, error) { AllowedGroups: l.AllowedGroups, CodeChallengeMethod: l.CodeChallengeMethod, BackendLogoutURL: l.BackendLogoutURL, + + BackendLogoutAllSessionsURL: l.BackendLogoutAllSessionsURL, } // This part is out of the switch section for all providers that support OIDC diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index a90b584c40..130dc803bb 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -88,6 +88,8 @@ type Provider struct { // URL to call to perform backend logout, `{id_token}` would be replaced by the actual `id_token` if available in the session BackendLogoutURL string `json:"backendLogoutURL"` + + BackendLogoutAllSessionsURL string `json:"backendLogoutAllSessionsURL"` } // ProviderType is used to enumerate the different provider type options diff --git a/providers/provider_data.go b/providers/provider_data.go index a967f17d9a..4744f67433 100644 --- a/providers/provider_data.go +++ b/providers/provider_data.go @@ -60,6 +60,8 @@ type ProviderData struct { loginURLParameterOverrides map[string]*regexp.Regexp BackendLogoutURL string + + BackendLogoutAllSessionsURL string } // Data returns the ProviderData diff --git a/providers/providers.go b/providers/providers.go index 153db12e4e..1c950aaf63 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -163,6 +163,7 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, p.setAllowedGroups(providerConfig.AllowedGroups) p.BackendLogoutURL = providerConfig.BackendLogoutURL + p.BackendLogoutAllSessionsURL = providerConfig.BackendLogoutAllSessionsURL return p, nil } From 2851483446916598d224b0e4af7e37a31ad51227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 08:38:10 -0300 Subject: [PATCH 2/9] add sign_out_all_sessions endpoint --- oauthproxy.go | 39 ++++++++++++++++++++-------- pics_oauthproxy.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 pics_oauthproxy.go diff --git a/oauthproxy.go b/oauthproxy.go index cfec6a1ec1..46f5b528f6 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -358,7 +358,16 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) { // The userinfo and logout endpoints needs to load sessions before handling the request s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo)) - s.Path(signOutPath).Handler(p.sessionChain.ThenFunc(p.SignOut)) + s.Path(signOutPath).Handler(p.sessionChain.ThenFunc( + func(w http.ResponseWriter, r *http.Request) { + p.SignOut(w, r, false) + }, + )) + s.Path(picsSignOutAllDevicesPath).Handler(p.sessionChain.ThenFunc( + func(w http.ResponseWriter, r *http.Request) { + p.SignOut(w, r, true) + }, + )) } // buildPreAuthChain constructs a chain that should process every request before @@ -758,7 +767,7 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { } // SignOut sends a response to clear the authentication cookie -func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { +func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request, signOutAllSessions bool) { redirect, err := p.appDirector.GetRedirect(req) if err != nil { logger.Errorf("Error obtaining redirect: %v", err) @@ -772,12 +781,12 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { return } - p.backendLogout(rw, req) + p.backendLogout(rw, req, signOutAllSessions) http.Redirect(rw, req, redirect, http.StatusFound) } -func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request) { +func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request, signOutAllSessions bool) { session, err := p.getAuthenticatedSession(rw, req) if err != nil { logger.Errorf("error getting authenticated session during backend logout: %v", err) @@ -789,14 +798,24 @@ func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request) { } providerData := p.provider.Data() - if providerData.BackendLogoutURL == "" { - return + var resp *http.Response + if signOutAllSessions { + if providerData.BackendLogoutAllSessionsURL == "" { + return + } + + resp, err = PicsSignOutAllSessions(providerData.BackendLogoutAllSessionsURL, session.IntrospectClaims, session.AccessToken) + } else { + if providerData.BackendLogoutURL == "" { + return + } + + backendLogoutURL := strings.ReplaceAll(providerData.BackendLogoutURL, "{id_token}", session.IDToken) + // security exception because URL is dynamic ({id_token} replacement) but + // base is not end-user provided but comes from configuration somewhat secure + resp, err = http.Get(backendLogoutURL) // #nosec G107 } - backendLogoutURL := strings.ReplaceAll(providerData.BackendLogoutURL, "{id_token}", session.IDToken) - // security exception because URL is dynamic ({id_token} replacement) but - // base is not end-user provided but comes from configuration somewhat secure - resp, err := http.Get(backendLogoutURL) // #nosec G107 if err != nil { logger.Errorf("error while calling backend logout: %v", err) return diff --git a/pics_oauthproxy.go b/pics_oauthproxy.go new file mode 100644 index 0000000000..17024a3642 --- /dev/null +++ b/pics_oauthproxy.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" +) + +const ( + picsSignOutAllDevicesPath = "/sign_out_all_sessions" +) + +func PicsSignOutAllSessions(backendLogoutAllSessionsURL string, introspectClaims string, accessToken string) (resp *http.Response, err error) { + userId, err := getUserId(introspectClaims) + if err != nil { + return nil, fmt.Errorf("error getting userId from instrospect claims: %v", err) + } + + backendLogoutURL := strings.ReplaceAll(backendLogoutAllSessionsURL, "{user_id}", userId) + + dummyBody := strings.NewReader(`{}`) + req, err := http.NewRequest("POST", backendLogoutURL, dummyBody) + if err != nil { + return nil, fmt.Errorf("error creating post request: %v", err) + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("API-Version", "1") + req.Header.Set("Accept", "application/json") + + resp, err = http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error logging out from IAM: %v", err) + } + + return resp, err +} + +func getUserId(introspectClaims string) (string, error) { + decodedClaims, err := base64.StdEncoding.DecodeString(introspectClaims) + if err != nil { + logger.Errorf("error decoding claims: %v", err) + return "", err + } + + var claims map[string]interface{} + err = json.Unmarshal(decodedClaims, &claims) + if err != nil { + logger.Errorf("error unmarshalling claims: %v", err) + return "", err + } + + userId, ok := claims["sub"].(string) + if !ok { + logger.Errorf("error extracting 'sub' from claims") + return "", err + } + + return userId, nil +} From 6cf267d368140159329bdaec965aa7bfc1f3433f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 08:38:23 -0300 Subject: [PATCH 3/9] clear session when refresh token fail to generate a new acess token --- pkg/middleware/stored_session.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/middleware/stored_session.go b/pkg/middleware/stored_session.go index f861c756fa..3d0bdacf74 100644 --- a/pkg/middleware/stored_session.go +++ b/pkg/middleware/stored_session.go @@ -191,6 +191,7 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req // If a preemptive refresh fails, we still keep the session // if validateSession succeeds. logger.Errorf("Unable to refresh session: %v", err) + return fmt.Errorf("unable to refresh session: %v", err) } // Validate all sessions after any Redeem/Refresh operation (fail or success) From 1da13b0effcc2bebc9e5b024393e988a73f09ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 09:50:43 -0300 Subject: [PATCH 4/9] fix userID lint --- pics_oauthproxy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pics_oauthproxy.go b/pics_oauthproxy.go index 17024a3642..f54e739551 100644 --- a/pics_oauthproxy.go +++ b/pics_oauthproxy.go @@ -15,12 +15,12 @@ const ( ) func PicsSignOutAllSessions(backendLogoutAllSessionsURL string, introspectClaims string, accessToken string) (resp *http.Response, err error) { - userId, err := getUserId(introspectClaims) + userID, err := getUserID(introspectClaims) if err != nil { return nil, fmt.Errorf("error getting userId from instrospect claims: %v", err) } - backendLogoutURL := strings.ReplaceAll(backendLogoutAllSessionsURL, "{user_id}", userId) + backendLogoutURL := strings.ReplaceAll(backendLogoutAllSessionsURL, "{user_id}", userID) dummyBody := strings.NewReader(`{}`) req, err := http.NewRequest("POST", backendLogoutURL, dummyBody) @@ -40,7 +40,7 @@ func PicsSignOutAllSessions(backendLogoutAllSessionsURL string, introspectClaims return resp, err } -func getUserId(introspectClaims string) (string, error) { +func getUserID(introspectClaims string) (string, error) { decodedClaims, err := base64.StdEncoding.DecodeString(introspectClaims) if err != nil { logger.Errorf("error decoding claims: %v", err) From 8bf8b0ecc2e7ab5bfeb26f4c08f92fe19197a683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 09:57:35 -0300 Subject: [PATCH 5/9] fix userID lint --- pics_oauthproxy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pics_oauthproxy.go b/pics_oauthproxy.go index f54e739551..6bfd1c0c56 100644 --- a/pics_oauthproxy.go +++ b/pics_oauthproxy.go @@ -54,11 +54,11 @@ func getUserID(introspectClaims string) (string, error) { return "", err } - userId, ok := claims["sub"].(string) + userID, ok := claims["sub"].(string) if !ok { logger.Errorf("error extracting 'sub' from claims") return "", err } - return userId, nil + return userID, nil } From 92e7960b6c59a30f0fe2767f3b225625a89d4bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 10:10:38 -0300 Subject: [PATCH 6/9] fix lint bodyclose --- oauthproxy.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 46f5b528f6..88fcdfb03c 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -805,6 +805,12 @@ func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request, si } resp, err = PicsSignOutAllSessions(providerData.BackendLogoutAllSessionsURL, session.IntrospectClaims, session.AccessToken) + if err != nil { + logger.Errorf("error while calling backend logout all sessions: %v", err) + return + } + + defer resp.Body.Close() } else { if providerData.BackendLogoutURL == "" { return @@ -814,14 +820,14 @@ func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request, si // security exception because URL is dynamic ({id_token} replacement) but // base is not end-user provided but comes from configuration somewhat secure resp, err = http.Get(backendLogoutURL) // #nosec G107 - } + if err != nil { + logger.Errorf("error while calling backend logout: %v", err) + return + } - if err != nil { - logger.Errorf("error while calling backend logout: %v", err) - return + defer resp.Body.Close() } - defer resp.Body.Close() if resp.StatusCode != 200 { logger.Errorf("error while calling backend logout url, returned error code %v", resp.StatusCode) } From 72f89818fa4e2617958ac4cb0d89d8f2ef550cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 10:19:59 -0300 Subject: [PATCH 7/9] add doc for new env --- docs/docs/configuration/alpha_config.md | 1 + pkg/apis/options/providers.go | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index ac5d7d4fda..5def6d1402 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -445,6 +445,7 @@ Provider holds all configuration for a single provider | `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group | | `code_challenge_method` | _string_ | The code challenge method | | `backendLogoutURL` | _string_ | URL to call to perform backend logout, `{id_token}` would be replaced by the actual `id_token` if available in the session | +| `backendLogoutAllSessionsURL` | _string_ | URL to call to perform backend logout, `{user_id}` would be replaced by the actual `user_id` if available in the session IntrospectClaims | ### ProviderType #### (`string` alias) diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index 130dc803bb..aefd3cc2c7 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -89,6 +89,7 @@ type Provider struct { // URL to call to perform backend logout, `{id_token}` would be replaced by the actual `id_token` if available in the session BackendLogoutURL string `json:"backendLogoutURL"` + // URL to call to perform backend logout, `{user_id}` would be replaced by the actual `user_id` if available in the session IntrospectClaims BackendLogoutAllSessionsURL string `json:"backendLogoutAllSessionsURL"` } From f46f7a39f3f1c437893b1d98e3d51434fc45ed41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 12:02:26 -0300 Subject: [PATCH 8/9] update test for refresh token change --- pkg/middleware/stored_session.go | 2 +- pkg/middleware/stored_session_test.go | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pkg/middleware/stored_session.go b/pkg/middleware/stored_session.go index 3d0bdacf74..92dbdb4e25 100644 --- a/pkg/middleware/stored_session.go +++ b/pkg/middleware/stored_session.go @@ -190,7 +190,7 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req if err := s.refreshSession(rw, req, session); err != nil { // If a preemptive refresh fails, we still keep the session // if validateSession succeeds. - logger.Errorf("Unable to refresh session: %v", err) + // PICS: We will clean the session if the refresh fails. return fmt.Errorf("unable to refresh session: %v", err) } diff --git a/pkg/middleware/stored_session_test.go b/pkg/middleware/stored_session_test.go index 904c2028fa..2d3a6f669d 100644 --- a/pkg/middleware/stored_session_test.go +++ b/pkg/middleware/stored_session_test.go @@ -295,17 +295,12 @@ var _ = Describe("Stored Session Suite", func() { refreshSession: defaultRefreshFunc, validateSession: defaultValidateFunc, }), - Entry("when the provider refresh fails but validation succeeds", storedSessionLoaderTableInput{ + Entry("when the provider refresh fails", storedSessionLoaderTableInput{ requestHeaders: http.Header{ "Cookie": []string{"_oauth2_proxy=RefreshError"}, }, existingSession: nil, - expectedSession: &sessionsapi.SessionState{ - RefreshToken: "RefreshError", - CreatedAt: &createdPast, - ExpiresOn: &createdFuture, - Lock: &sessionsapi.NoOpLock{}, - }, + expectedSession: nil, store: defaultSessionStore, refreshPeriod: 1 * time.Minute, refreshSession: defaultRefreshFunc, From 982e27fc00765770df5e16a40ad1e1eb14b39fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20Val=C3=A9rio?= Date: Fri, 27 Dec 2024 14:59:07 -0300 Subject: [PATCH 9/9] tests for pics_oauthproxy --- oauthproxy.go | 13 +++++----- pics_oauthproxy.go | 27 +++++++++----------- pics_oauthproxy_test.go | 55 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 pics_oauthproxy_test.go diff --git a/oauthproxy.go b/oauthproxy.go index 88fcdfb03c..6798b3ecff 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -804,13 +804,15 @@ func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request, si return } - resp, err = PicsSignOutAllSessions(providerData.BackendLogoutAllSessionsURL, session.IntrospectClaims, session.AccessToken) + resp, err := PicsSignOutAllSessions(providerData.BackendLogoutAllSessionsURL, session.IntrospectClaims, session.AccessToken) if err != nil { logger.Errorf("error while calling backend logout all sessions: %v", err) return } - defer resp.Body.Close() + if resp.StatusCode() != 200 { + logger.Errorf("error while calling backend logout url, returned error code %v", resp.StatusCode()) + } } else { if providerData.BackendLogoutURL == "" { return @@ -826,10 +828,9 @@ func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request, si } defer resp.Body.Close() - } - - if resp.StatusCode != 200 { - logger.Errorf("error while calling backend logout url, returned error code %v", resp.StatusCode) + if resp.StatusCode != 200 { + logger.Errorf("error while calling backend logout url, returned error code %v", resp.StatusCode) + } } } diff --git a/pics_oauthproxy.go b/pics_oauthproxy.go index 6bfd1c0c56..9464dc227a 100644 --- a/pics_oauthproxy.go +++ b/pics_oauthproxy.go @@ -4,36 +4,31 @@ import ( "encoding/base64" "encoding/json" "fmt" - "net/http" "strings" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests" ) const ( picsSignOutAllDevicesPath = "/sign_out_all_sessions" ) -func PicsSignOutAllSessions(backendLogoutAllSessionsURL string, introspectClaims string, accessToken string) (resp *http.Response, err error) { +func PicsSignOutAllSessions(backendLogoutAllSessionsURL string, introspectClaims string, accessToken string) (resp requests.Result, err error) { userID, err := getUserID(introspectClaims) if err != nil { - return nil, fmt.Errorf("error getting userId from instrospect claims: %v", err) + return nil, fmt.Errorf("error getting userID from instrospect claims: %v", err) } backendLogoutURL := strings.ReplaceAll(backendLogoutAllSessionsURL, "{user_id}", userID) - - dummyBody := strings.NewReader(`{}`) - req, err := http.NewRequest("POST", backendLogoutURL, dummyBody) - if err != nil { - return nil, fmt.Errorf("error creating post request: %v", err) - } - - req.Header.Set("Authorization", "Bearer "+accessToken) - req.Header.Set("API-Version", "1") - req.Header.Set("Accept", "application/json") - - resp, err = http.DefaultClient.Do(req) - if err != nil { + resp = requests.New(backendLogoutURL). + WithMethod("POST"). + SetHeader("Authorization", "Bearer "+accessToken). + SetHeader("API-Version", "1"). + SetHeader("Accept", "application/json"). + Do() + + if resp.Error() != nil { return nil, fmt.Errorf("error logging out from IAM: %v", err) } diff --git a/pics_oauthproxy_test.go b/pics_oauthproxy_test.go new file mode 100644 index 0000000000..0990679479 --- /dev/null +++ b/pics_oauthproxy_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func createIntrospectClaims() string { + claims := map[string]interface{}{ + "sub": "1234567890", + } + claimsBytes, err := json.Marshal(claims) + if err != nil { + return "" + } + + return base64.StdEncoding.EncodeToString(claimsBytes) +} + +func Test_PicsSignOutAllSessionsReturnsErrorWhenUserIDIsNotFound(t *testing.T) { + _, err := PicsSignOutAllSessions("http://localhost:8080/test", "", "") + + assert.Error(t, err) +} + +func Test_getUserID(t *testing.T) { + introspectClaims := createIntrospectClaims() + userID, err := getUserID(introspectClaims) + + assert.NoError(t, err) + assert.Equal(t, "1234567890", userID) +} + +func Test_PicsSignOutAllSessionsReturns200Ok(t *testing.T) { + introspectClaims := createIntrospectClaims() + accessToken := "validAccessToken" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer "+accessToken, r.Header.Get("Authorization")) + assert.Equal(t, "1", r.Header.Get("API-Version")) + assert.Equal(t, "application/json", r.Header.Get("Accept")) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + resp, err := PicsSignOutAllSessions(server.URL+"/{user_id}", introspectClaims, accessToken) + + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode()) +}