Fixes to Multiple IDP support in console (#2392)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-10-20 20:08:54 -05:00
committed by GitHub
parent 139e90830f
commit dab4eb7664
19 changed files with 380 additions and 112 deletions

View File

@@ -25,6 +25,7 @@ package models
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"strconv"
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
@@ -37,9 +38,6 @@ import (
// swagger:model loginDetails // swagger:model loginDetails
type LoginDetails struct { type LoginDetails struct {
// display names
DisplayNames []string `json:"displayNames"`
// is direct p v // is direct p v
IsDirectPV bool `json:"isDirectPV,omitempty"` IsDirectPV bool `json:"isDirectPV,omitempty"`
@@ -47,8 +45,8 @@ type LoginDetails struct {
// Enum: [form redirect service-account redirect-service-account] // Enum: [form redirect service-account redirect-service-account]
LoginStrategy string `json:"loginStrategy,omitempty"` LoginStrategy string `json:"loginStrategy,omitempty"`
// redirect // redirect rules
Redirect []string `json:"redirect"` RedirectRules []*RedirectRule `json:"redirectRules"`
} }
// Validate validates this login details // Validate validates this login details
@@ -59,6 +57,10 @@ func (m *LoginDetails) Validate(formats strfmt.Registry) error {
res = append(res, err) res = append(res, err)
} }
if err := m.validateRedirectRules(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 { if len(res) > 0 {
return errors.CompositeValidationError(res...) return errors.CompositeValidationError(res...)
} }
@@ -113,8 +115,63 @@ func (m *LoginDetails) validateLoginStrategy(formats strfmt.Registry) error {
return nil return nil
} }
// ContextValidate validates this login details based on context it is used func (m *LoginDetails) validateRedirectRules(formats strfmt.Registry) error {
if swag.IsZero(m.RedirectRules) { // not required
return nil
}
for i := 0; i < len(m.RedirectRules); i++ {
if swag.IsZero(m.RedirectRules[i]) { // not required
continue
}
if m.RedirectRules[i] != nil {
if err := m.RedirectRules[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("redirectRules" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("redirectRules" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this login details based on the context it is used
func (m *LoginDetails) ContextValidate(ctx context.Context, formats strfmt.Registry) error { func (m *LoginDetails) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateRedirectRules(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *LoginDetails) contextValidateRedirectRules(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.RedirectRules); i++ {
if m.RedirectRules[i] != nil {
if err := m.RedirectRules[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("redirectRules" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("redirectRules" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil return nil
} }

70
models/redirect_rule.go Normal file
View File

@@ -0,0 +1,70 @@
// 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"
)
// RedirectRule redirect rule
//
// swagger:model redirectRule
type RedirectRule struct {
// display name
DisplayName string `json:"displayName,omitempty"`
// redirect
Redirect string `json:"redirect,omitempty"`
}
// Validate validates this redirect rule
func (m *RedirectRule) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this redirect rule based on context it is used
func (m *RedirectRule) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *RedirectRule) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *RedirectRule) UnmarshalBinary(b []byte) error {
var res RedirectRule
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -3623,12 +3623,6 @@ func init() {
"loginDetails": { "loginDetails": {
"type": "object", "type": "object",
"properties": { "properties": {
"displayNames": {
"type": "array",
"items": {
"type": "string"
}
},
"isDirectPV": { "isDirectPV": {
"type": "boolean" "type": "boolean"
}, },
@@ -3641,10 +3635,10 @@ func init() {
"redirect-service-account" "redirect-service-account"
] ]
}, },
"redirect": { "redirectRules": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/redirectRule"
} }
} }
} }
@@ -4396,6 +4390,17 @@ func init() {
} }
} }
}, },
"redirectRule": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"redirect": {
"type": "string"
}
}
},
"resourceQuota": { "resourceQuota": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -9614,12 +9619,6 @@ func init() {
"loginDetails": { "loginDetails": {
"type": "object", "type": "object",
"properties": { "properties": {
"displayNames": {
"type": "array",
"items": {
"type": "string"
}
},
"isDirectPV": { "isDirectPV": {
"type": "boolean" "type": "boolean"
}, },
@@ -9632,10 +9631,10 @@ func init() {
"redirect-service-account" "redirect-service-account"
] ]
}, },
"redirect": { "redirectRules": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/redirectRule"
} }
} }
} }
@@ -10252,6 +10251,17 @@ func init() {
} }
} }
}, },
"redirectRule": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"redirect": {
"type": "string"
}
}
},
"resourceQuota": { "resourceQuota": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -101,7 +101,8 @@ func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDet
r := params.HTTPRequest r := params.HTTPRequest
loginStrategy := models.LoginDetailsLoginStrategyServiceDashAccount loginStrategy := models.LoginDetailsLoginStrategyServiceDashAccount
redirectURL := []string{}
var redirectRules []*models.RedirectRule
if oauth2.IsIDPEnabled() { if oauth2.IsIDPEnabled() {
loginStrategy = models.LoginDetailsLoginStrategyRedirectDashServiceDashAccount loginStrategy = models.LoginDetailsLoginStrategyRedirectDashServiceDashAccount
@@ -115,12 +116,18 @@ func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDet
KeyFunc: oauth2.DefaultDerivedKey, KeyFunc: oauth2.DefaultDerivedKey,
Client: oauth2Client, Client: oauth2Client,
} }
redirectURL = append(redirectURL, identityProvider.GenerateLoginURL())
newRedirectRule := &models.RedirectRule{
Redirect: identityProvider.GenerateLoginURL(),
DisplayName: "Login with SSO",
}
redirectRules = append(redirectRules, newRedirectRule)
} }
loginDetails := &models.LoginDetails{ loginDetails := &models.LoginDetails{
LoginStrategy: loginStrategy, LoginStrategy: loginStrategy,
Redirect: redirectURL, RedirectRules: redirectRules,
IsDirectPV: getDirectPVEnabled(), IsDirectPV: getDirectPVEnabled(),
} }
return loginDetails, nil return loginDetails, nil

View File

@@ -40,11 +40,12 @@ type IdentityProviderI interface {
type IdentityProvider struct { type IdentityProvider struct {
KeyFunc oauth2.StateKeyFunc KeyFunc oauth2.StateKeyFunc
Client *oauth2.Provider Client *oauth2.Provider
RoleARN string
} }
// VerifyIdentity will verify the user identity against the idp using the authorization code flow // VerifyIdentity will verify the user identity against the idp using the authorization code flow
func (c IdentityProvider) VerifyIdentity(ctx context.Context, code, state string) (*credentials.Credentials, error) { func (c IdentityProvider) VerifyIdentity(ctx context.Context, code, state string) (*credentials.Credentials, error) {
return c.Client.VerifyIdentity(ctx, code, state, c.KeyFunc) return c.Client.VerifyIdentity(ctx, code, state, c.RoleARN, c.KeyFunc)
} }
// VerifyIdentityForOperator will verify the user identity against the idp using the authorization code flow // VerifyIdentityForOperator will verify the user identity against the idp using the authorization code flow
@@ -54,5 +55,5 @@ func (c IdentityProvider) VerifyIdentityForOperator(ctx context.Context, code, s
// GenerateLoginURL returns a new URL used by the user to login against the idp // GenerateLoginURL returns a new URL used by the user to login against the idp
func (c IdentityProvider) GenerateLoginURL() string { func (c IdentityProvider) GenerateLoginURL() string {
return c.Client.GenerateLoginURL(c.KeyFunc) return c.Client.GenerateLoginURL(c.KeyFunc, c.Client.IDPName)
} }

View File

@@ -48,9 +48,11 @@ func (pc ProviderConfig) GetStateKeyFunc() StateKeyFunc {
} }
} }
type OpenIDPCfg map[string]ProviderConfig func (pc ProviderConfig) GetARNInf() string {
return pc.RoleArn
}
var DefaultIDPConfig = "_" type OpenIDPCfg map[string]ProviderConfig
func GetSTSEndpoint() string { func GetSTSEndpoint() string {
return strings.TrimSpace(env.Get(ConsoleMinIOServer, "http://localhost:9000")) return strings.TrimSpace(env.Get(ConsoleMinIOServer, "http://localhost:9000"))

View File

@@ -29,4 +29,5 @@ const (
ConsoleIDPScopes = "CONSOLE_IDP_SCOPES" ConsoleIDPScopes = "CONSOLE_IDP_SCOPES"
ConsoleIDPUserInfo = "CONSOLE_IDP_USERINFO" ConsoleIDPUserInfo = "CONSOLE_IDP_USERINFO"
ConsoleIDPTokenExpiration = "CONSOLE_IDP_TOKEN_EXPIRATION" ConsoleIDPTokenExpiration = "CONSOLE_IDP_TOKEN_EXPIRATION"
ConsoleIDPRoleARN = "CONSOLE_IDP_ROLE_ARN"
) )

View File

@@ -92,13 +92,13 @@ func (ac Config) TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.Toke
type Provider struct { type Provider struct {
// oauth2Config is an interface configuration that contains the following fields // oauth2Config is an interface configuration that contains the following fields
// Config{ // Config{
// ClientID string // IDPName string
// ClientSecret string // ClientSecret string
// RedirectURL string // RedirectURL string
// Endpoint oauth2.Endpoint // Endpoint oauth2.Endpoint
// Scopes []string // Scopes []string
// } // }
// - ClientID is the public identifier for this application // - IDPName is the public identifier for this application
// - ClientSecret is a shared secret between this application and the authorization server // - ClientSecret is a shared secret between this application and the authorization server
// - RedirectURL is the URL to redirect users going through // - RedirectURL is the URL to redirect users going through
// the OAuth flow, after the resource owner's URLs. // the OAuth flow, after the resource owner's URLs.
@@ -107,7 +107,7 @@ type Provider struct {
// often available via site-specific packages, such as // often available via site-specific packages, such as
// google.Endpoint or github.Endpoint. // google.Endpoint or github.Endpoint.
// - Scopes specifies optional requested permissions. // - Scopes specifies optional requested permissions.
ClientID string IDPName string
// if enabled means that we need extrace access_token as well // if enabled means that we need extrace access_token as well
UserInfo bool UserInfo bool
oauth2Config Configuration oauth2Config Configuration
@@ -178,6 +178,7 @@ func NewOauth2ProviderClient(scopes []string, r *http.Request, httpClient *http.
} }
redirectURL := GetIDPCallbackURL() redirectURL := GetIDPCallbackURL()
if GetIDPCallbackURLDynamic() { if GetIDPCallbackURLDynamic() {
// dynamic redirect if set, will generate redirect URLs // dynamic redirect if set, will generate redirect URLs
// dynamically based on incoming requests. // dynamically based on incoming requests.
@@ -199,7 +200,7 @@ func NewOauth2ProviderClient(scopes []string, r *http.Request, httpClient *http.
Scopes: scopes, Scopes: scopes,
} }
client.ClientID = GetIDPClientID() client.IDPName = GetIDPClientID()
client.UserInfo = GetIDPUserInfo() client.UserInfo = GetIDPUserInfo()
client.provHTTPClient = httpClient client.provHTTPClient = httpClient
@@ -273,7 +274,7 @@ func (o OpenIDPCfg) NewOauth2ProviderClient(name string, scopes []string, r *htt
Scopes: scopes, Scopes: scopes,
} }
client.ClientID = o[name].ClientID client.IDPName = name
client.UserInfo = o[name].Userinfo client.UserInfo = o[name].Userinfo
client.provHTTPClient = httpClient client.provHTTPClient = httpClient
return client, nil return client, nil
@@ -310,9 +311,10 @@ type StateKeyFunc func() []byte
// VerifyIdentity will contact the configured IDP to the user identity based on the authorization code and state // VerifyIdentity will contact the configured IDP to the user identity based on the authorization code and state
// if the user is valid, then it will contact MinIO to get valid sts credentials based on the identity provided by the IDP // if the user is valid, then it will contact MinIO to get valid sts credentials based on the identity provided by the IDP
func (client *Provider) VerifyIdentity(ctx context.Context, code, state string, keyFunc StateKeyFunc) (*credentials.Credentials, error) { func (client *Provider) VerifyIdentity(ctx context.Context, code, state, roleARN string, keyFunc StateKeyFunc) (*credentials.Credentials, error) {
// verify the provided state is valid (prevents CSRF attacks) // verify the provided state is valid (prevents CSRF attacks)
if err := validateOauth2State(state, keyFunc); err != nil { if err := validateOauth2State(state, keyFunc); err != nil {
fmt.Println("err1", err)
return nil, err return nil, err
} }
getWebTokenExpiry := func() (*credentials.WebIdentityToken, error) { getWebTokenExpiry := func() (*credentials.WebIdentityToken, error) {
@@ -352,10 +354,12 @@ func (client *Provider) VerifyIdentity(ctx context.Context, code, state string,
return token, nil return token, nil
} }
stsEndpoint := GetSTSEndpoint() stsEndpoint := GetSTSEndpoint()
sts := credentials.New(&credentials.STSWebIdentity{ sts := credentials.New(&credentials.STSWebIdentity{
Client: client.provHTTPClient, Client: client.provHTTPClient,
STSEndpoint: stsEndpoint, STSEndpoint: stsEndpoint,
GetWebIDTokenExpiry: getWebTokenExpiry, GetWebIDTokenExpiry: getWebTokenExpiry,
RoleARN: roleARN,
}) })
return sts, nil return sts, nil
} }
@@ -439,10 +443,34 @@ func GetRandomStateWithHMAC(length int, keyFunc StateKeyFunc) string {
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", state, hmac))) return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", state, hmac)))
} }
type LoginURLParams struct {
State string `json:"state"`
IDPName string `json:"idp_name"`
}
// GenerateLoginURL returns a new login URL based on the configured IDP // GenerateLoginURL returns a new login URL based on the configured IDP
func (client *Provider) GenerateLoginURL(keyFunc StateKeyFunc) string { func (client *Provider) GenerateLoginURL(keyFunc StateKeyFunc, iDPName string) string {
// generates random state and sign it using HMAC256 // generates random state and sign it using HMAC256
state := GetRandomStateWithHMAC(25, keyFunc) state := GetRandomStateWithHMAC(25, keyFunc)
loginURL := client.oauth2Config.AuthCodeURL(state)
configureID := "_"
if iDPName != "" {
configureID = iDPName
}
lgParams := LoginURLParams{
State: state,
IDPName: configureID,
}
jsonEnc, err := json.Marshal(lgParams)
if err != nil {
return ""
}
stEncode := base64.StdEncoding.EncodeToString(jsonEnc)
loginURL := client.oauth2Config.AuthCodeURL(stEncode)
return strings.TrimSpace(loginURL) return strings.TrimSpace(loginURL)
} }

View File

@@ -66,6 +66,6 @@ func TestGenerateLoginURL(t *testing.T) {
// a non-empty string // a non-empty string
return state return state
} }
url := oauth2Provider.GenerateLoginURL(DefaultDerivedKey) url := oauth2Provider.GenerateLoginURL(DefaultDerivedKey, "testIDP")
funcAssert.NotEqual("", url) funcAssert.NotEqual("", url)
} }

View File

@@ -15,21 +15,20 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
Box, Box,
InputAdornment, InputAdornment,
LinearProgress, LinearProgress,
Select,
MenuItem, MenuItem,
Select,
} from "@mui/material"; } from "@mui/material";
import { Button } from "mds"; import { Button } from "mds";
import { Theme, useTheme } from "@mui/material/styles"; import { Theme, useTheme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import { loginStrategyType } from "./types"; import { loginStrategyType, redirectRule } from "./types";
import LogoutIcon from "../../icons/LogoutIcon"; import LogoutIcon from "../../icons/LogoutIcon";
import RefreshIcon from "../../icons/RefreshIcon"; import RefreshIcon from "../../icons/RefreshIcon";
import MainError from "../Console/Common/MainError/MainError"; import MainError from "../Console/Common/MainError/MainError";
@@ -58,6 +57,7 @@ import { resetForm, setJwt } from "./loginSlice";
import StrategyForm from "./StrategyForm"; import StrategyForm from "./StrategyForm";
import { LoginField } from "./LoginField"; import { LoginField } from "./LoginField";
import DirectPVLogo from "../../icons/DirectPVLogo"; import DirectPVLogo from "../../icons/DirectPVLogo";
import { redirectRules } from "../../utils/sortFunctions";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -344,7 +344,19 @@ const Login = () => {
} }
case loginStrategyType.redirect: case loginStrategyType.redirect:
case loginStrategyType.redirectServiceAccount: { case loginStrategyType.redirectServiceAccount: {
if (loginStrategy.redirect.length > 1) { let redirectItems: redirectRule[] = [];
if (
loginStrategy.redirectRules &&
loginStrategy.redirectRules.length > 0
) {
redirectItems = [...loginStrategy.redirectRules].sort(redirectRules);
}
if (
loginStrategy.redirectRules &&
loginStrategy.redirectRules.length > 1
) {
loginComponent = ( loginComponent = (
<React.Fragment> <React.Fragment>
<div className={classes.loginSsoText}>Login with SSO:</div> <div className={classes.loginSsoText}>Login with SSO:</div>
@@ -361,21 +373,21 @@ const Login = () => {
className={classes.ssoSelect} className={classes.ssoSelect}
renderValue={() => "Select Provider"} renderValue={() => "Select Provider"}
> >
{loginStrategy.redirect.map((r, idx) => ( {redirectItems.map((r, idx) => (
<MenuItem <MenuItem
value={r} value={r.redirect}
key={`sso-login-option-${idx}`} key={`sso-login-option-${idx}`}
className={classes.ssoMenuItem} className={classes.ssoMenuItem}
divider={true} divider={true}
> >
<LogoutIcon className={classes.ssoLoginIcon} /> <LogoutIcon className={classes.ssoLoginIcon} />
{loginStrategy.displayNames[idx]} {r.displayName}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</React.Fragment> </React.Fragment>
); );
} else if (loginStrategy.redirect.length === 1) { } else if (redirectItems.length === 1) {
loginComponent = ( loginComponent = (
<div className={clsx(classes.submit, classes.ssoSubmit)}> <div className={clsx(classes.submit, classes.ssoSubmit)}>
<Button <Button
@@ -383,12 +395,11 @@ const Login = () => {
variant="callAction" variant="callAction"
id="sso-login" id="sso-login"
label={ label={
loginStrategy.displayNames && redirectItems[0].displayName === ""
loginStrategy.displayNames.length > 0 ? "Login with SSO"
? loginStrategy.displayNames[0] : redirectItems[0].displayName
: "Login with SSO"
} }
onClick={() => (window.location.href = loginStrategy.redirect[0])} onClick={() => (window.location.href = redirectItems[0].redirect)}
fullWidth fullWidth
/> />
</div> </div>

View File

@@ -50,8 +50,7 @@ const initialState: LoginState = {
jwt: "", jwt: "",
loginStrategy: { loginStrategy: {
loginStrategy: loginStrategyType.unknown, loginStrategy: loginStrategyType.unknown,
redirect: [], redirectRules: [],
displayNames: [],
}, },
loginSending: false, loginSending: false,
loadingFetchConfiguration: true, loadingFetchConfiguration: true,

View File

@@ -16,11 +16,15 @@
export interface ILoginDetails { export interface ILoginDetails {
loginStrategy: loginStrategyType; loginStrategy: loginStrategyType;
redirect: string[]; redirectRules: redirectRule[];
displayNames: string[];
isDirectPV?: boolean; isDirectPV?: boolean;
} }
export interface redirectRule {
redirect: string;
displayName: string;
}
export enum loginStrategyType { export enum loginStrategyType {
unknown = "unknown", unknown = "unknown",
form = "form", form = "form",

View File

@@ -14,6 +14,8 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import { redirectRule } from "../screens/LoginPage/types";
interface userInterface { interface userInterface {
accessKey: string; accessKey: string;
} }
@@ -72,3 +74,13 @@ export const policyDetailsSort = (
// a must be equal to b // a must be equal to b
return 0; return 0;
}; };
export const redirectRules = (a: redirectRule, b: redirectRule) => {
if (a.displayName > b.displayName) {
return 1;
}
if (a.displayName < b.displayName) {
return -1;
}
return 0;
};

View File

@@ -6349,12 +6349,6 @@ func init() {
"loginDetails": { "loginDetails": {
"type": "object", "type": "object",
"properties": { "properties": {
"displayNames": {
"type": "array",
"items": {
"type": "string"
}
},
"isDirectPV": { "isDirectPV": {
"type": "boolean" "type": "boolean"
}, },
@@ -6367,10 +6361,10 @@ func init() {
"redirect-service-account" "redirect-service-account"
] ]
}, },
"redirect": { "redirectRules": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/redirectRule"
} }
} }
} }
@@ -7038,6 +7032,17 @@ func init() {
} }
} }
}, },
"redirectRule": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"redirect": {
"type": "string"
}
}
},
"remoteBucket": { "remoteBucket": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -14605,12 +14610,6 @@ func init() {
"loginDetails": { "loginDetails": {
"type": "object", "type": "object",
"properties": { "properties": {
"displayNames": {
"type": "array",
"items": {
"type": "string"
}
},
"isDirectPV": { "isDirectPV": {
"type": "boolean" "type": "boolean"
}, },
@@ -14623,10 +14622,10 @@ func init() {
"redirect-service-account" "redirect-service-account"
] ]
}, },
"redirect": { "redirectRules": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/redirectRule"
} }
} }
} }
@@ -15294,6 +15293,17 @@ func init() {
} }
} }
}, },
"redirectRule": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"redirect": {
"type": "string"
}
}
},
"remoteBucket": { "remoteBucket": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -66,7 +66,6 @@ func replaceJwtVariables(rawPolicy []byte, claims map[string]interface{}) json.R
for _, field := range jwtFields { for _, field := range jwtFields {
if val, ok := claims[field]; ok { if val, ok := claims[field]; ok {
variable := fmt.Sprintf("${jwt:%s}", field) variable := fmt.Sprintf("${jwt:%s}", field)
fmt.Println("found", variable)
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte(variable), []byte(fmt.Sprintf("%v", val))) rawPolicy = bytes.ReplaceAll(rawPolicy, []byte(variable), []byte(fmt.Sprintf("%v", val)))
} }
} }

View File

@@ -18,11 +18,10 @@ package restapi
import ( import (
"context" "context"
"encoding/base64"
"encoding/json"
"net/http" "net/http"
"github.com/minio/madmin-go"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/go-openapi/runtime" "github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models" "github.com/minio/console/models"
@@ -30,12 +29,14 @@ import (
"github.com/minio/console/pkg/auth/idp/oauth2" "github.com/minio/console/pkg/auth/idp/oauth2"
"github.com/minio/console/restapi/operations" "github.com/minio/console/restapi/operations"
authApi "github.com/minio/console/restapi/operations/auth" authApi "github.com/minio/console/restapi/operations/auth"
"github.com/minio/madmin-go"
"github.com/minio/minio-go/v7/pkg/credentials"
) )
func registerLoginHandlers(api *operations.ConsoleAPI) { func registerLoginHandlers(api *operations.ConsoleAPI) {
// GET login strategy // GET login strategy
api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder { api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder {
loginDetails, err := getLoginDetailsResponse(params, GlobalMinIOConfig.OpenIDProviders, oauth2.DefaultIDPConfig) loginDetails, err := getLoginDetailsResponse(params, GlobalMinIOConfig.OpenIDProviders)
if err != nil { if err != nil {
return authApi.NewLoginDetailDefault(int(err.Code)).WithPayload(err) return authApi.NewLoginDetailDefault(int(err.Code)).WithPayload(err)
} }
@@ -56,7 +57,7 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
}) })
// POST login using external IDP // POST login using external IDP
api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder { api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder {
loginResponse, err := getLoginOauth2AuthResponse(params, GlobalMinIOConfig.OpenIDProviders, oauth2.DefaultIDPConfig) loginResponse, err := getLoginOauth2AuthResponse(params, GlobalMinIOConfig.OpenIDProviders)
if err != nil { if err != nil {
return authApi.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err) return authApi.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
} }
@@ -145,12 +146,12 @@ func getLoginResponse(params authApi.LoginParams) (*models.LoginResponse, *model
} }
// getLoginDetailsResponse returns information regarding the Console authentication mechanism. // getLoginDetailsResponse returns information regarding the Console authentication mechanism.
func getLoginDetailsResponse(params authApi.LoginDetailParams, openIDProviders oauth2.OpenIDPCfg, idpName string) (*models.LoginDetails, *models.Error) { func getLoginDetailsResponse(params authApi.LoginDetailParams, openIDProviders oauth2.OpenIDPCfg) (*models.LoginDetails, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel() defer cancel()
loginStrategy := models.LoginDetailsLoginStrategyForm loginStrategy := models.LoginDetailsLoginStrategyForm
redirectURL := []string{} var redirectRules []*models.RedirectRule
displayNames := []string{}
r := params.HTTPRequest r := params.HTTPRequest
var loginDetails *models.LoginDetails var loginDetails *models.LoginDetails
if len(openIDProviders) >= 1 { if len(openIDProviders) >= 1 {
@@ -166,18 +167,24 @@ func getLoginDetailsResponse(params authApi.LoginDetailParams, openIDProviders o
KeyFunc: provider.GetStateKeyFunc(), KeyFunc: provider.GetStateKeyFunc(),
Client: oauth2Client, Client: oauth2Client,
} }
redirectURL = append(redirectURL, identityProvider.GenerateLoginURL())
displayName := "Login with SSO"
if provider.DisplayName != "" { if provider.DisplayName != "" {
displayNames = append(displayNames, provider.DisplayName) displayName = provider.DisplayName
} else {
displayNames = append(displayNames, "Login with SSO")
} }
redirectRule := models.RedirectRule{
Redirect: identityProvider.GenerateLoginURL(),
DisplayName: displayName,
}
redirectRules = append(redirectRules, &redirectRule)
} }
} }
loginDetails = &models.LoginDetails{ loginDetails = &models.LoginDetails{
LoginStrategy: loginStrategy, LoginStrategy: loginStrategy,
Redirect: redirectURL, RedirectRules: redirectRules,
DisplayNames: displayNames,
} }
return loginDetails, nil return loginDetails, nil
} }
@@ -187,29 +194,50 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI,
userCredentials, err := provider.VerifyIdentity(ctx, code, state) userCredentials, err := provider.VerifyIdentity(ctx, code, state)
if err != nil { if err != nil {
LogError("error validating user identity against idp: %v", err) LogError("error validating user identity against idp: %v", err)
return nil, ErrInvalidLogin return nil, err
} }
return userCredentials, nil return userCredentials, nil
} }
func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams, openIDProviders oauth2.OpenIDPCfg, idpName string) (*models.LoginResponse, *models.Error) { func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams, openIDProviders oauth2.OpenIDPCfg) (*models.LoginResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel() defer cancel()
r := params.HTTPRequest r := params.HTTPRequest
lr := params.Body lr := params.Body
if openIDProviders != nil { if openIDProviders != nil {
// initialize new oauth2 client // we read state
oauth2Client, err := openIDProviders.NewOauth2ProviderClient(idpName, nil, r, GetConsoleHTTPClient("")) rState := *lr.State
decodedRState, err := base64.StdEncoding.DecodeString(rState)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
var requestItems oauth2.LoginURLParams
err = json.Unmarshal(decodedRState, &requestItems)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
IDPName := requestItems.IDPName
state := requestItems.State
providerCfg := openIDProviders[IDPName]
oauth2Client, err := openIDProviders.NewOauth2ProviderClient(IDPName, nil, r, GetConsoleHTTPClient(""))
if err != nil { if err != nil {
return nil, ErrorWithContext(ctx, err) return nil, ErrorWithContext(ctx, err)
} }
// initialize new identity provider // initialize new identity provider
identityProvider := auth.IdentityProvider{ identityProvider := auth.IdentityProvider{
KeyFunc: openIDProviders[idpName].GetStateKeyFunc(), KeyFunc: providerCfg.GetStateKeyFunc(),
Client: oauth2Client, Client: oauth2Client,
RoleARN: providerCfg.RoleArn,
} }
// Validate user against IDP // Validate user against IDP
userCredentials, err := verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, *lr.State) userCredentials, err := verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, state)
if err != nil { if err != nil {
return nil, ErrorWithContext(ctx, err) return nil, ErrorWithContext(ctx, err)
} }

View File

@@ -18,6 +18,7 @@ package ssointegration
import ( import (
"bytes" "bytes"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -44,7 +45,7 @@ var token string
func initConsoleServer(consoleIDPURL string) (*restapi.Server, error) { func initConsoleServer(consoleIDPURL string) (*restapi.Server, error) {
// Configure Console Server with vars to get the idp config from the container // Configure Console Server with vars to get the idp config from the container
pcfg := map[string]consoleoauth2.ProviderConfig{ pcfg := map[string]consoleoauth2.ProviderConfig{
consoleoauth2.DefaultIDPConfig: { "_": {
URL: consoleIDPURL, URL: consoleIDPURL,
ClientID: "minio-client-app", ClientID: "minio-client-app",
ClientSecret: "minio-client-app-secret", ClientSecret: "minio-client-app-secret",
@@ -130,11 +131,18 @@ func TestMain(t *testing.T) {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var jsonMap map[string][]interface{} var jsonMap models.LoginDetails
json.Unmarshal(body, &jsonMap)
fmt.Println(jsonMap["redirect"][0]) fmt.Println(body)
redirect := jsonMap["redirect"][0]
redirectAsString := fmt.Sprint(redirect) err = json.Unmarshal(body, &jsonMap)
if err != nil {
fmt.Printf("error JSON Unmarshal %s\n", err)
}
redirectRule := jsonMap.RedirectRules[0]
redirectAsString := fmt.Sprint(redirectRule.Redirect)
fmt.Println(redirectAsString) fmt.Println(redirectAsString)
// execute script to get the code and state // execute script to get the code and state
@@ -238,12 +246,25 @@ func TestBadLogin(t *testing.T) {
Timeout: 2 * time.Second, Timeout: 2 * time.Second,
} }
encodeItem := consoleoauth2.LoginURLParams{
State: "invalidState",
IDPName: "_",
}
jsonState, err := json.Marshal(encodeItem)
if err != nil {
log.Println(err)
assert.Nil(err)
}
// get login credentials // get login credentials
stateVarIable := base64.StdEncoding.EncodeToString(jsonState)
codeVarIable := "invalidCode" codeVarIable := "invalidCode"
stateVarIabl := "invalidState"
requestData := map[string]string{ requestData := map[string]string{
"code": codeVarIable, "code": codeVarIable,
"state": stateVarIabl, "state": stateVarIable,
} }
requestDataJSON, _ := json.Marshal(requestData) requestDataJSON, _ := json.Marshal(requestData)

View File

@@ -4045,14 +4045,10 @@ definitions:
loginStrategy: loginStrategy:
type: string type: string
enum: [ form, redirect, service-account, redirect-service-account ] enum: [ form, redirect, service-account, redirect-service-account ]
redirect: redirectRules:
type: array type: array
items: items:
type: string $ref: "#/definitions/redirectRule"
displayNames:
type: array
items:
type: string
isDirectPV: isDirectPV:
type: boolean type: boolean
loginOauth2AuthRequest: loginOauth2AuthRequest:
@@ -5567,3 +5563,11 @@ definitions:
type: integer type: integer
maxConcurrentDownloads: maxConcurrentDownloads:
type: integer type: integer
redirectRule:
type: object
properties:
redirect:
type: string
displayName:
type: string

View File

@@ -1639,14 +1639,10 @@ definitions:
loginStrategy: loginStrategy:
type: string type: string
enum: [form, redirect, service-account, redirect-service-account] enum: [form, redirect, service-account, redirect-service-account]
redirect: redirectRules:
type: array type: array
items: items:
type: string $ref: "#/definitions/redirectRule"
displayNames:
type: array
items:
type: string
isDirectPV: isDirectPV:
type: boolean type: boolean
loginRequest: loginRequest:
@@ -3745,3 +3741,11 @@ definitions:
properties: properties:
registered: registered:
type: boolean type: boolean
redirectRule:
type: object
properties:
redirect:
type: string
displayName:
type: string