Call end_session_endpoint in IDP provider when login out from Console (#2476)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
67
models/logout_request.go
Normal 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
|
||||
}
|
||||
@@ -37,6 +37,7 @@ type ProviderConfig struct {
|
||||
Userinfo bool
|
||||
RedirectCallbackDynamic bool
|
||||
RedirectCallback string
|
||||
EndSessionEndpoint string
|
||||
RoleArn string // can be empty
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user