Skip to content

Commit

Permalink
Invoke IDP endpoint to end session when logging out (#2126)
Browse files Browse the repository at this point in the history
Signed-off-by: pjuarezd <pjuarezd@users.noreply.github.com>
  • Loading branch information
pjuarezd authored May 21, 2024
1 parent 7ae65b2 commit 77bb482
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 10 deletions.
35 changes: 32 additions & 3 deletions api/cookies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion api/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
Expand Down Expand Up @@ -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
}
Expand Down
42 changes: 41 additions & 1 deletion api/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
6 changes: 6 additions & 0 deletions pkg/auth/idp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
32 changes: 27 additions & 5 deletions pkg/auth/idp/oauth2/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand Down

0 comments on commit 77bb482

Please sign in to comment.