Call end_session_endpoint in IDP provider when login out from Console (#2476)

This commit is contained in:
Javier Adriel
2022-12-05 18:14:41 -06:00
committed by GitHub
parent 262a601d21
commit e7a41b4cd9
13 changed files with 243 additions and 7 deletions

View File

@@ -116,8 +116,8 @@ func TestLogout(t *testing.T) {
log.Println("authentication token not found in cookies response")
return
}
request, err = http.NewRequest("POST", "http://localhost:9090/api/v1/logout", requestDataBody)
logoutRequest := bytes.NewReader([]byte("{}"))
request, err = http.NewRequest("POST", "http://localhost:9090/api/v1/logout", logoutRequest)
if err != nil {
log.Println(err)
return
@@ -126,7 +126,6 @@ func TestLogout(t *testing.T) {
request.Header.Add("Content-Type", "application/json")
response, err = client.Do(request)
assert.NotNil(response, "Logout response is nil")
assert.Nil(err, "Logout errored out")
assert.Equal(response.StatusCode, 200)

View File

@@ -34,6 +34,9 @@ import (
// swagger:model loginResponse
type LoginResponse struct {
// ID p refresh token
IDPRefreshToken string `json:"IDPRefreshToken,omitempty"`
// session Id
SessionID string `json:"sessionId,omitempty"`
}

67
models/logout_request.go Normal file
View File

@@ -0,0 +1,67 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// LogoutRequest logout request
//
// swagger:model logoutRequest
type LogoutRequest struct {
// state
State string `json:"state,omitempty"`
}
// Validate validates this logout request
func (m *LogoutRequest) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this logout request based on context it is used
func (m *LogoutRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *LogoutRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *LogoutRequest) UnmarshalBinary(b []byte) error {
var res LogoutRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -37,6 +37,7 @@ type ProviderConfig struct {
Userinfo bool
RedirectCallbackDynamic bool
RedirectCallback string
EndSessionEndpoint string
RoleArn string // can be empty
}

View File

@@ -110,6 +110,7 @@ type Provider struct {
IDPName string
// if enabled means that we need extrace access_token as well
UserInfo bool
RefreshToken string
oauth2Config Configuration
provHTTPClient *http.Client
}
@@ -319,6 +320,7 @@ func (client *Provider) VerifyIdentity(ctx context.Context, code, state, roleARN
getWebTokenExpiry := func() (*credentials.WebIdentityToken, error) {
customCtx := context.WithValue(ctx, oauth2.HTTPClient, client.provHTTPClient)
oauth2Token, err := client.oauth2Config.Exchange(customCtx, code)
client.RefreshToken = oauth2Token.RefreshToken
if err != nil {
return nil, err
}

View File

@@ -84,7 +84,9 @@ export const deleteCookie = (name: string) => {
export const clearSession = () => {
storage.removeItem("token");
storage.removeItem("auth-state");
deleteCookie("token");
deleteCookie("idp-refresh-token");
};
// timeFromDate gets time string from date input

View File

@@ -142,6 +142,9 @@ const LoginCallback = ({ classes }: ILoginCallBackProps) => {
targetPath = `${localStorage.getItem("redirect-path")}`;
localStorage.setItem("redirect-path", "");
}
if (state) {
localStorage.setItem("auth-state", state);
}
setLoading(false);
navigate(targetPath);
})

View File

@@ -35,8 +35,9 @@ const LogoutPage = () => {
dispatch(resetSession());
navigate(`login`);
};
const state = localStorage.getItem("auth-state");
api
.invoke("POST", `/api/v1/logout`)
.invoke("POST", `/api/v1/logout`, { state })
.then(() => {
deleteSession();
})

View File

@@ -3460,6 +3460,16 @@ func init() {
],
"summary": "Logout from Console.",
"operationId": "Logout",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/logoutRequest"
}
}
],
"responses": {
"200": {
"description": "A successful response."
@@ -6426,11 +6436,22 @@ func init() {
"loginResponse": {
"type": "object",
"properties": {
"IDPRefreshToken": {
"type": "string"
},
"sessionId": {
"type": "string"
}
}
},
"logoutRequest": {
"type": "object",
"properties": {
"state": {
"type": "string"
}
}
},
"makeBucketRequest": {
"type": "object",
"required": [
@@ -11615,6 +11636,16 @@ func init() {
],
"summary": "Logout from Console.",
"operationId": "Logout",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/logoutRequest"
}
}
],
"responses": {
"200": {
"description": "A successful response."
@@ -14707,11 +14738,22 @@ func init() {
"loginResponse": {
"type": "object",
"properties": {
"IDPRefreshToken": {
"type": "string"
},
"sessionId": {
"type": "string"
}
}
},
"logoutRequest": {
"type": "object",
"properties": {
"state": {
"type": "string"
}
}
},
"makeBucketRequest": {
"type": "object",
"required": [

View File

@@ -23,10 +23,15 @@ package auth
// Editing this file might prove futile when you re-run the swagger generate command
import (
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewLogoutParams creates a new LogoutParams object
@@ -45,6 +50,12 @@ type LogoutParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.LogoutRequest
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
@@ -56,6 +67,33 @@ func (o *LogoutParams) BindRequest(r *http.Request, route *middleware.MatchedRou
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.LogoutRequest
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}

View File

@@ -65,6 +65,14 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
http.SetCookie(w, &http.Cookie{
Path: "/",
Name: "idp-refresh-token",
Value: loginResponse.IDPRefreshToken,
HttpOnly: true,
Secure: len(GlobalPublicCerts) > 0,
SameSite: http.SameSiteLaxMode,
})
authApi.NewLoginOauth2AuthNoContent().WriteResponse(w, p)
})
})
@@ -252,7 +260,8 @@ func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams, openIDProv
}
// serialize output
loginResponse := &models.LoginResponse{
SessionID: *token,
SessionID: *token,
IDPRefreshToken: identityProvider.Client.RefreshToken,
}
return loginResponse, nil
}

View File

@@ -17,11 +17,17 @@
package restapi
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/url"
"time"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/pkg/auth/idp/oauth2"
"github.com/minio/console/restapi/operations"
authApi "github.com/minio/console/restapi/operations/auth"
)
@@ -29,13 +35,26 @@ import (
func registerLogoutHandlers(api *operations.ConsoleAPI) {
// logout from console
api.AuthLogoutHandler = authApi.LogoutHandlerFunc(func(params authApi.LogoutParams, session *models.Principal) middleware.Responder {
getLogoutResponse(session)
err := getLogoutResponse(session, params)
if err != nil {
return authApi.NewLogoutDefault(int(err.Code)).WithPayload(err)
}
// Custom response writer to expire the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
expiredCookie := ExpireSessionCookie()
// this will tell the browser to clear the cookie and invalidate user session
// additionally we are deleting the cookie from the client side
http.SetCookie(w, &expiredCookie)
http.SetCookie(w, &http.Cookie{
Path: "/",
Name: "idp-refresh-token",
Value: "",
MaxAge: -1,
Expires: time.Now().Add(-100 * time.Hour),
HttpOnly: true,
Secure: len(GlobalPublicCerts) > 0,
SameSite: http.SameSiteLaxMode,
})
authApi.NewLogoutOK().WriteResponse(w, p)
})
})
@@ -47,8 +66,45 @@ func logout(credentials ConsoleCredentialsI) {
}
// getLogoutResponse performs logout() and returns nil or errors
func getLogoutResponse(session *models.Principal) {
func getLogoutResponse(session *models.Principal, params authApi.LogoutParams) *models.Error {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
state := params.Body.State
if state != "" {
if err := logoutFromIDPProvider(params.HTTPRequest, state); err != nil {
return ErrorWithContext(ctx, err)
}
}
creds := getConsoleCredentialsFromSession(session)
credentials := ConsoleCredentials{ConsoleCredentials: creds}
logout(credentials)
return nil
}
func logoutFromIDPProvider(r *http.Request, state string) error {
decodedRState, err := base64.StdEncoding.DecodeString(state)
if err != nil {
return err
}
var requestItems oauth2.LoginURLParams
err = json.Unmarshal(decodedRState, &requestItems)
if err != nil {
return err
}
providerCfg := GlobalMinIOConfig.OpenIDProviders[requestItems.IDPName]
refreshToken, err := r.Cookie("idp-refresh-token")
if err != nil {
return err
}
if providerCfg.EndSessionEndpoint != "" {
params := url.Values{}
params.Add("client_id", providerCfg.ClientID)
params.Add("client_secret", providerCfg.ClientSecret)
params.Add("refresh_token", refreshToken.Value)
_, err := http.PostForm(providerCfg.EndSessionEndpoint, params)
if err != nil {
return err
}
}
return nil
}

View File

@@ -83,6 +83,12 @@ paths:
post:
summary: Logout from Console.
operationId: Logout
parameters:
- name: body
in: body
required: true
schema:
$ref: "#/definitions/logoutRequest"
responses:
200:
description: A successful response.
@@ -4080,6 +4086,13 @@ definitions:
properties:
sessionId:
type: string
IDPRefreshToken:
type: string
logoutRequest:
type: object
properties:
state:
type: string
# Structure that holds the `Bearer {TOKEN}` present on authenticated requests
principal:
type: object