diff --git a/api/cookies.go b/api/cookies.go index 9750457d112..1954f31e99f 100644 --- a/api/cookies.go +++ b/api/cookies.go @@ -27,7 +27,7 @@ import ( func NewSessionCookieForConsole(token string) http.Cookie { sessionDuration := xjwt.GetConsoleSTSDuration() return http.Cookie{ - Path: "/", + Path: "/api/v1/", Name: "token", Value: token, MaxAge: int(sessionDuration.Seconds()), // default 1 hr @@ -41,14 +41,43 @@ func NewSessionCookieForConsole(token string) http.Cookie { } } +// NewIDPSessionCookie creates a cookie for a refresh token +func NewIDPSessionCookie(token string) http.Cookie { + return http.Cookie{ + Path: "/api/v1/", + Name: "idp-refresh-token", + Value: token, + HttpOnly: true, + Secure: len(GlobalPublicCerts) > 0, + SameSite: http.SameSiteLaxMode, + } +} + // ExpireSessionCookie expires a cookie func ExpireSessionCookie() http.Cookie { return http.Cookie{ - Path: "/", + Path: "/api/v1/", Name: "token", Value: "", MaxAge: -1, - Expires: time.Now().Add(-100 * time.Hour), + Expires: time.Unix(0, 0), + HttpOnly: true, + // if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser + // should not leak any cookie if we access the site using HTTP + Secure: len(GlobalPublicCerts) > 0, + // read more: https://web.dev/samesite-cookies-explained/ + SameSite: http.SameSiteLaxMode, + } +} + +// ExpireIDPSessionCookie expires a cookie for idp +func ExpireIDPSessionCookie() http.Cookie { + return http.Cookie{ + Path: "/api/v1/", + Name: "idp-refresh-token", + Value: "", + MaxAge: -1, + Expires: time.Unix(0, 0), HttpOnly: true, // if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser // should not leak any cookie if we access the site using HTTP diff --git a/api/login.go b/api/login.go index 2a836c0019c..5c70cc73ba7 100644 --- a/api/login.go +++ b/api/login.go @@ -71,7 +71,9 @@ func registerLoginHandlers(api *operations.OperatorAPI) { // Custom response writer to set the session cookies return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) { cookie := NewSessionCookieForConsole(loginResponse.SessionID) + idpCookie := NewIDPSessionCookie(loginResponse.IDPRefreshToken) http.SetCookie(w, &cookie) + http.SetCookie(w, &idpCookie) authApi.NewLoginOauth2AuthNoContent().WriteResponse(w, p) }) }) @@ -199,7 +201,8 @@ func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams) (*models.L } // serialize output loginResponse := &models.LoginResponse{ - SessionID: *token, + SessionID: *token, + IDPRefreshToken: identityProvider.Client.RefreshToken, } return loginResponse, nil } diff --git a/api/logout.go b/api/logout.go index eb588f655a5..75e2ab2edf0 100644 --- a/api/logout.go +++ b/api/logout.go @@ -17,8 +17,13 @@ package api import ( + "context" + "fmt" "net/http" + "github.com/minio/operator/pkg/auth" + "github.com/minio/operator/pkg/auth/idp/oauth2" + "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/minio/operator/api/operations" @@ -31,11 +36,46 @@ func registerLogoutHandlers(api *operations.OperatorAPI) { api.AuthLogoutHandler = authApi.LogoutHandlerFunc(func(params authApi.LogoutParams, session *models.Principal) middleware.Responder { // Custom response writer to expire the session cookies return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) { + if oauth2.IsIDPEnabled() { + err := logoutIDP(params.HTTPRequest) + if err != nil { + api.Logger("IDP logout failed: %v", err.DetailedMessage) + w.Header().Set("IDP-Logout", fmt.Sprintf("%v", err.DetailedMessage)) + } + } expiredCookie := ExpireSessionCookie() - // this will tell the browser to clear the cookie and invalidate user session + expiredIDPCookie := ExpireIDPSessionCookie() + // this will tell the browser to clear the cookies and invalidate user session // additionally we are deleting the cookie from the client side http.SetCookie(w, &expiredCookie) + http.SetCookie(w, &expiredIDPCookie) authApi.NewLogoutOK().WriteResponse(w, p) }) }) } + +func logoutIDP(r *http.Request) *models.Error { + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + + // initialize new oauth2 client + oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient("")) + if err != nil { + return ErrorWithContext(ctx, err) + } + // initialize new identity provider + identityProvider := auth.IdentityProvider{ + KeyFunc: oauth2.DefaultDerivedKey, + Client: oauth2Client, + } + refreshToken, err := r.Cookie("idp-refresh-token") + if err != nil { + return ErrorWithContext(ctx, err) + } + + err = identityProvider.Logout(refreshToken.Value) + if err != nil { + return ErrorWithContext(ctx, ErrDefault, nil, err) + } + return nil +} diff --git a/pkg/auth/idp.go b/pkg/auth/idp.go index ba14e9d5556..311c1dc62d0 100644 --- a/pkg/auth/idp.go +++ b/pkg/auth/idp.go @@ -31,6 +31,7 @@ type IdentityProviderI interface { VerifyIdentity(ctx context.Context, code, state string) (*credentials.Credentials, error) VerifyIdentityForOperator(ctx context.Context, code, state string) (*xoauth2.Token, error) GenerateLoginURL() string + Logout(refreshToken string) error } // IdentityProvider Identity implementation @@ -57,3 +58,8 @@ func (c IdentityProvider) VerifyIdentityForOperator(ctx context.Context, code, s func (c IdentityProvider) GenerateLoginURL() string { return c.Client.GenerateLoginURL(c.KeyFunc, c.Client.IDPName) } + +// Logout ends session on IDP +func (c IdentityProvider) Logout(refreshToken string) error { + return c.Client.EndSession(refreshToken) +} diff --git a/pkg/auth/idp/oauth2/provider.go b/pkg/auth/idp/oauth2/provider.go index 28e419bbb1b..bcc5631c54a 100644 --- a/pkg/auth/idp/oauth2/provider.go +++ b/pkg/auth/idp/oauth2/provider.go @@ -64,6 +64,7 @@ type DiscoveryDoc struct { TokenEndpoint string `json:"token_endpoint,omitempty"` UserInfoEndpoint string `json:"userinfo_endpoint,omitempty"` RevocationEndpoint string `json:"revocation_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` JwksURI string `json:"jwks_uri,omitempty"` ResponseTypesSupported []string `json:"response_types_supported,omitempty"` SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` @@ -120,11 +121,12 @@ type Provider struct { // - Scopes specifies optional requested permissions. IDPName string // if enabled means that we need extrace access_token as well - UserInfo bool - RefreshToken string - oauth2Config Configuration - provHTTPClient *http.Client - stsHTTPClient *http.Client + UserInfo bool + RefreshToken string + endSessionEndpoint string + oauth2Config Configuration + provHTTPClient *http.Client + stsHTTPClient *http.Client } // DefaultDerivedKey is the key used to compute the HMAC for signing the oauth state parameter @@ -216,6 +218,7 @@ func NewOauth2ProviderClient(scopes []string, r *http.Request, httpClient *http. client.IDPName = GetIDPClientID() client.UserInfo = GetIDPUserInfo() client.provHTTPClient = httpClient + client.endSessionEndpoint = ddoc.EndSessionEndpoint return client, nil } @@ -380,6 +383,24 @@ func (client *Provider) VerifyIdentity(ctx context.Context, code, state, roleARN return sts, nil } +// EndSession to invoke endsession_endpoint +func (client *Provider) EndSession(refreshToken string) error { + if client.endSessionEndpoint != "" { + params := url.Values{} + params.Add("client_id", client.IDPName) + params.Add("client_secret", GetIDPSecret()) + params.Add("refresh_token", refreshToken) + clnt := &http.Client{ + Transport: client.provHTTPClient.Transport, + } + _, err := clnt.PostForm(client.endSessionEndpoint, params) + if err != nil { + return err + } + } + return nil +} + // VerifyIdentityForOperator will contact the configured IDP and validate the user identity based on the authorization code and state func (client *Provider) VerifyIdentityForOperator(ctx context.Context, code, state string, keyFunc StateKeyFunc) (*xoauth2.Token, error) { // verify the provided state is valid (prevents CSRF attacks) @@ -394,6 +415,7 @@ func (client *Provider) VerifyIdentityForOperator(ctx context.Context, code, sta if !oauth2Token.Valid() { return nil, errors.New("invalid token") } + client.RefreshToken = oauth2Token.RefreshToken return oauth2Token, nil }