diff --git a/Makefile b/Makefile
index 3f29f25e5c..33a9eb57c9 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ assets:
test:
@(go test -race -v github.com/minio/mcs/restapi/...)
- @(go test -race -v github.com/minio/mcs/pkg/auth)
+ @(go test -race -v github.com/minio/mcs/pkg/auth/...)
coverage:
@(go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
diff --git a/go.mod b/go.mod
index dcdc23fce1..bc684cbf68 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/minio/mcs
go 1.14
require (
+ github.com/coreos/go-oidc v2.2.1+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/elazarl/go-bindata-assetfs v1.0.0
github.com/go-openapi/errors v0.19.4
@@ -19,9 +20,11 @@ require (
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22
+ github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1
github.com/unrolled/secure v1.0.7
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
+ golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
)
diff --git a/go.sum b/go.sum
index cfa9060b28..b7d4677ca2 100644
--- a/go.sum
+++ b/go.sum
@@ -69,6 +69,8 @@ github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.12+incompatible h1:pAWNwdf7QiT1zfaWyqCtNZQWCLByQyA3JrSQyuYAqnQ=
github.com/coreos/etcd v3.3.12+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
+github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
@@ -492,6 +494,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.2-0.20190702141536-6ffe496ea953 h1:oBvgW8IvwF278gJ3R4hH0gD3ZeJxjwBXVIScRR0dRc8=
github.com/posener/complete v1.2.2-0.20190702141536-6ffe496ea953/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
+github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
+github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA=
diff --git a/models/login_oauth2_auth_request.go b/models/login_oauth2_auth_request.go
new file mode 100644
index 0000000000..b1e67c5a43
--- /dev/null
+++ b/models/login_oauth2_auth_request.go
@@ -0,0 +1,98 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+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 (
+ "github.com/go-openapi/errors"
+ "github.com/go-openapi/strfmt"
+ "github.com/go-openapi/swag"
+ "github.com/go-openapi/validate"
+)
+
+// LoginOauth2AuthRequest login oauth2 auth request
+//
+// swagger:model loginOauth2AuthRequest
+type LoginOauth2AuthRequest struct {
+
+ // code
+ // Required: true
+ Code *string `json:"code"`
+
+ // state
+ // Required: true
+ State *string `json:"state"`
+}
+
+// Validate validates this login oauth2 auth request
+func (m *LoginOauth2AuthRequest) Validate(formats strfmt.Registry) error {
+ var res []error
+
+ if err := m.validateCode(formats); err != nil {
+ res = append(res, err)
+ }
+
+ if err := m.validateState(formats); err != nil {
+ res = append(res, err)
+ }
+
+ if len(res) > 0 {
+ return errors.CompositeValidationError(res...)
+ }
+ return nil
+}
+
+func (m *LoginOauth2AuthRequest) validateCode(formats strfmt.Registry) error {
+
+ if err := validate.Required("code", "body", m.Code); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (m *LoginOauth2AuthRequest) validateState(formats strfmt.Registry) error {
+
+ if err := validate.Required("state", "body", m.State); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// MarshalBinary interface implementation
+func (m *LoginOauth2AuthRequest) MarshalBinary() ([]byte, error) {
+ if m == nil {
+ return nil, nil
+ }
+ return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *LoginOauth2AuthRequest) UnmarshalBinary(b []byte) error {
+ var res LoginOauth2AuthRequest
+ if err := swag.ReadJSON(b, &res); err != nil {
+ return err
+ }
+ *m = res
+ return nil
+}
diff --git a/pkg/auth/idp.go b/pkg/auth/idp.go
new file mode 100644
index 0000000000..68768accc8
--- /dev/null
+++ b/pkg/auth/idp.go
@@ -0,0 +1,49 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+package auth
+
+import (
+ "context"
+
+ "github.com/minio/mcs/pkg/auth/idp/oauth2"
+)
+
+// IdentityProviderClient interface with all functions to be implemented
+// by mock when testing, it should include all IdentityProviderClient respective api calls
+// that are used within this project.
+type IdentityProviderClient interface {
+ VerifyIdentity(ctx context.Context, code, state string) (*oauth2.User, error)
+ GenerateLoginURL() string
+}
+
+// Interface implementation
+//
+// Define the structure of a IdentityProvider Client and define the functions that are actually used
+// during the authentication flow.
+type IdentityProvider struct {
+ Client IdentityProviderClient
+}
+
+// VerifyIdentity will verify the user identity against the idp using the authorization code flow
+func (c IdentityProvider) VerifyIdentity(ctx context.Context, code, state string) (*oauth2.User, error) {
+ return c.Client.VerifyIdentity(ctx, code, state)
+}
+
+// GenerateLoginURL returns a new URL used by the user to login against the idp
+func (c IdentityProvider) GenerateLoginURL() string {
+ return c.Client.GenerateLoginURL()
+}
diff --git a/pkg/auth/idp/oauth2/config.go b/pkg/auth/idp/oauth2/config.go
new file mode 100644
index 0000000000..95f0823c84
--- /dev/null
+++ b/pkg/auth/idp/oauth2/config.go
@@ -0,0 +1,71 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+// Package oauth2 contains all the necessary configurations to initialize the
+// idp communication using oauth2 protocol
+package oauth2
+
+import (
+ "github.com/minio/mcs/pkg/auth/utils"
+ "github.com/minio/minio/pkg/env"
+)
+
+func GetIdpURL() string {
+ return env.Get(McsIdpURL, "")
+}
+
+func GetIdpClientID() string {
+ return env.Get(McsIdpClientID, "")
+}
+
+func GetIdpSecret() string {
+ return env.Get(McsIdpSecret, "")
+}
+
+// Public endpoint used by the identity oidcProvider when redirecting the user after identity verification
+func GetIdpCallbackURL() string {
+ return env.Get(McsIdpCallbackURL, "")
+}
+
+func GetIdpAdminRoles() string {
+ return env.Get(McsIdpAdminRoles, "")
+}
+
+func IsIdpEnabled() bool {
+ return GetIdpURL() != "" &&
+ GetIdpClientID() != "" &&
+ GetIdpSecret() != "" &&
+ GetIdpCallbackURL() != ""
+}
+
+var defaultPassphraseForIdpHmac = utils.RandomCharString(64)
+
+// GetPassphraseForIdpHmac returns passphrase for the pbkdf2 function used to sign the oauth2 state parameter
+func getPassphraseForIdpHmac() string {
+ return env.Get(McsIdpHmacPassphrase, defaultPassphraseForIdpHmac)
+}
+
+var defaultSaltForIdpHmac = utils.RandomCharString(64)
+
+// GetSaltForIdpHmac returns salt for the pbkdf2 function used to sign the oauth2 state parameter
+func getSaltForIdpHmac() string {
+ return env.Get(McsIdpHmacSalt, defaultSaltForIdpHmac)
+}
+
+// GetSaltForIdpHmac returns the policy to be assigned to the users authenticating via an IDP
+func GetIDPPolicyForUser() string {
+ return env.Get(McsIdpPolicyUser, "mcsAdmin")
+}
diff --git a/pkg/auth/idp/oauth2/const.go b/pkg/auth/idp/oauth2/const.go
new file mode 100644
index 0000000000..7e2127de89
--- /dev/null
+++ b/pkg/auth/idp/oauth2/const.go
@@ -0,0 +1,29 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+package oauth2
+
+const (
+ // const for idp configuration
+ McsIdpURL = "MCS_IDP_URL"
+ McsIdpClientID = "MCS_IDP_CLIENT_ID"
+ McsIdpSecret = "MCS_IDP_SECRET"
+ McsIdpCallbackURL = "MCS_IDP_CALLBACK"
+ McsIdpAdminRoles = "MCS_IDP_ADMIN_ROLES"
+ McsIdpHmacPassphrase = "MCS_IDP_HMAC_PASSPHRASE"
+ McsIdpHmacSalt = "MCS_IDP_HMAC_SALT"
+ McsIdpPolicyUser = "MCS_IDP_POLICY_USER"
+)
diff --git a/pkg/auth/idp/oauth2/provider.go b/pkg/auth/idp/oauth2/provider.go
new file mode 100644
index 0000000000..7d6961dc1d
--- /dev/null
+++ b/pkg/auth/idp/oauth2/provider.go
@@ -0,0 +1,229 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+package oauth2
+
+import (
+ "context"
+ "crypto/sha1"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/coreos/go-oidc"
+ "github.com/minio/mcs/pkg/auth/utils"
+ "golang.org/x/crypto/pbkdf2"
+ xoauth2 "golang.org/x/oauth2"
+)
+
+var (
+ errGeneric = errors.New("an error occurred, please try again")
+)
+
+type Configuration interface {
+ Exchange(ctx context.Context, code string, opts ...xoauth2.AuthCodeOption) (*xoauth2.Token, error)
+ AuthCodeURL(state string, opts ...xoauth2.AuthCodeOption) string
+ PasswordCredentialsToken(ctx context.Context, username string, password string) (*xoauth2.Token, error)
+ Client(ctx context.Context, t *xoauth2.Token) *http.Client
+ TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.TokenSource
+}
+
+type Config struct {
+ xoauth2.Config
+}
+
+func (ac Config) Exchange(ctx context.Context, code string, opts ...xoauth2.AuthCodeOption) (*xoauth2.Token, error) {
+ return ac.Exchange(ctx, code, opts...)
+}
+
+func (ac Config) AuthCodeURL(state string, opts ...xoauth2.AuthCodeOption) string {
+ return ac.AuthCodeURL(state, opts...)
+}
+
+func (ac Config) PasswordCredentialsToken(ctx context.Context, username string, password string) (*xoauth2.Token, error) {
+ return ac.PasswordCredentialsToken(ctx, username, password)
+}
+
+func (ac Config) Client(ctx context.Context, t *xoauth2.Token) *http.Client {
+ return ac.Client(ctx, t)
+}
+
+func (ac Config) TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.TokenSource {
+ return ac.TokenSource(ctx, t)
+}
+
+// Provider is a wrapper of the oauth2 configuration and the oidc provider
+type Provider struct {
+ // oauth2Config is an interface configuration that contains the following fields
+ // Config{
+ // ClientID string
+ // ClientSecret string
+ // RedirectURL string
+ // Endpoint oauth2.Endpoint
+ // Scopes []string
+ // }
+ // - ClientID is the public identifier for this application
+ // - ClientSecret is a shared secret between this application and the authorization server
+ // - RedirectURL is the URL to redirect users going through
+ // the OAuth flow, after the resource owner's URLs.
+ // - Endpoint contains the resource server's token endpoint
+ // URLs. These are constants specific to each server and are
+ // often available via site-specific packages, such as
+ // google.Endpoint or github.Endpoint.
+ // - Scopes specifies optional requested permissions.
+ ClientID string
+ oauth2Config Configuration
+ oidcProvider *oidc.Provider
+}
+
+// derivedKey is the key used to compute the HMAC for signing the oauth state parameter
+// its derived using pbkdf on MCS_IDP_HMAC_PASSPHRASE with MCS_IDP_HMAC_SALT
+var derivedKey = pbkdf2.Key([]byte(getPassphraseForIdpHmac()), []byte(getSaltForIdpHmac()), 4096, 32, sha1.New)
+
+// NewOauth2ProviderClient instantiates a new oauth2 client using the configured credentials
+// it returns a *Provider object that contains the necessary configuration to initiate an
+// oauth2 authentication flow
+func NewOauth2ProviderClient(ctx context.Context, scopes []string) (*Provider, error) {
+ provider, err := oidc.NewProvider(ctx, GetIdpURL())
+ if err != nil {
+ return nil, err
+ }
+ // If provided scopes are empty we use a default list
+ if len(scopes) == 0 {
+ scopes = []string{oidc.ScopeOpenID, "profile", "app_metadata", "user_metadata", "email"}
+ }
+ client := new(Provider)
+ config := xoauth2.Config{
+ ClientID: GetIdpClientID(),
+ ClientSecret: GetIdpSecret(),
+ RedirectURL: GetIdpCallbackURL(),
+ Endpoint: provider.Endpoint(),
+ Scopes: scopes,
+ }
+ client.oauth2Config = &config
+ client.oidcProvider = provider
+ client.ClientID = GetIdpClientID()
+
+ return client, nil
+}
+
+type User struct {
+ AppMetadata map[string]interface{} `json:"app_metadata"`
+ Blocked bool `json:"blocked"`
+ CreatedAt string `json:"created_at"`
+ Email string `json:"email"`
+ EmailVerified bool `json:"email_verified"`
+ FamilyName string `json:"family_name"`
+ GivenName string `json:"given_name"`
+ Identities []interface{} `json:"identities"`
+ LastIP string `json:"last_ip"`
+ LastLogin string `json:"last_login"`
+ LastPasswordReset string `json:"last_password_reset"`
+ LoginsCount int `json:"logins_count"`
+ Mltifactor string `json:"multifactor"`
+ Name string `json:"name"`
+ Nickname string `json:"nickname"`
+ PhoneNumber string `json:"phone_number"`
+ PhoneVerified bool `json:"phone_verified"`
+ Picture string `json:"picture"`
+ UpdatedAt string `json:"updated_at"`
+ UserID string `json:"user_id"`
+ UserMetadata map[string]interface{} `json:"user_metadata"`
+ Username string `json:"username"`
+}
+
+// VerifyIdentity will contact the configured IDP and validate the user identity based on the authorization code
+func (client *Provider) VerifyIdentity(ctx context.Context, code, state string) (*User, error) {
+ // verify the provided state is valid (prevents CSRF attacks)
+ if !validateOauth2State(state) {
+ return nil, errGeneric
+ }
+ // verify the authorization code against the identity oidcProvider
+ // idp will return a token in exchange
+ token, err := client.oauth2Config.Exchange(ctx, code)
+ if err != nil {
+ log.Println("Failed to verify authorization code", err)
+ return nil, errGeneric
+ }
+ // extract and check id_token field is provided in the response
+ rawIDToken, ok := token.Extra("id_token").(string)
+ if !ok {
+ log.Println("No id_token field in oauth2 token")
+ return nil, errGeneric
+ }
+ config := &oidc.Config{
+ ClientID: client.ClientID,
+ }
+ idToken, err := client.oidcProvider.Verifier(config).Verify(ctx, rawIDToken)
+ if err != nil {
+ log.Println("Failed to verify ID token", err)
+ return nil, errGeneric
+ }
+ var profile User
+ // Populate the profile object using the claims included in the token
+ if err := idToken.Claims(&profile); err != nil {
+ log.Println("Failed to read profile information", err)
+ return nil, errGeneric
+ }
+ return &profile, nil
+}
+
+// validateOauth2State validates the provided state was originated using the same
+// instance (or one configured using the same secrets) of MCS, this is basically used to prevent CSRF attacks
+// https://security.stackexchange.com/questions/20187/oauth2-cross-site-request-forgery-and-state-parameter
+func validateOauth2State(state string) bool {
+ // state contains a base64 encoded string that may ends with "==", the browser encodes that to "%3D%3D"
+ // query unescape is need it before trying to decode the base64 string
+ encodedMessage, err := url.QueryUnescape(state)
+ if err != nil {
+ log.Println(err)
+ return false
+ }
+ // decode the state parameter value
+ message, err := base64.StdEncoding.DecodeString(encodedMessage)
+ if err != nil {
+ log.Println(err)
+ return false
+ }
+ s := strings.Split(string(message), ":")
+ // Validate that the decoded message has the right format "message:hmac"
+ if len(s) != 2 {
+ return false
+ }
+ // extract the state and hmac
+ incomingState, incomingHmac := s[0], s[1]
+ // validate that hmac(incomingState + pbkdf2(secret, salt)) == incomingHmac
+ return utils.ComputeHmac256(incomingState, derivedKey) == incomingHmac
+}
+
+// GetRandomStateWithHMAC computes message + hmac(message, pbkdf2(key, salt)) to be used as state during the oauth authorization
+func GetRandomStateWithHMAC(length int) string {
+ state := utils.RandomCharString(length)
+ hmac := utils.ComputeHmac256(state, derivedKey)
+ return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", state, hmac)))
+}
+
+// GenerateLoginURL returns a new login URL based on the configured IDP
+func (client *Provider) GenerateLoginURL() string {
+ // generates random state and sign it using HMAC256
+ state := GetRandomStateWithHMAC(25)
+ loginURL := client.oauth2Config.AuthCodeURL(state)
+ return strings.TrimSpace(loginURL)
+}
diff --git a/pkg/auth/idp/oauth2/provider_test.go b/pkg/auth/idp/oauth2/provider_test.go
new file mode 100644
index 0000000000..ce512e3c42
--- /dev/null
+++ b/pkg/auth/idp/oauth2/provider_test.go
@@ -0,0 +1,98 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+package oauth2
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/coreos/go-oidc"
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/oauth2"
+)
+
+type Oauth2configMock struct{}
+
+var oauth2ConfigExchangeMock func(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
+var oauth2ConfigAuthCodeURLMock func(state string, opts ...oauth2.AuthCodeOption) string
+var oauth2ConfigPasswordCredentialsTokenMock func(ctx context.Context, username string, password string) (*oauth2.Token, error)
+var oauth2ConfigClientMock func(ctx context.Context, t *oauth2.Token) *http.Client
+var oauth2ConfigokenSourceMock func(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
+
+func (ac Oauth2configMock) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
+ return oauth2ConfigExchangeMock(ctx, code, opts...)
+}
+
+func (ac Oauth2configMock) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
+ return oauth2ConfigAuthCodeURLMock(state, opts...)
+}
+
+func (ac Oauth2configMock) PasswordCredentialsToken(ctx context.Context, username string, password string) (*oauth2.Token, error) {
+ return oauth2ConfigPasswordCredentialsTokenMock(ctx, username, password)
+}
+
+func (ac Oauth2configMock) Client(ctx context.Context, t *oauth2.Token) *http.Client {
+ return oauth2ConfigClientMock(ctx, t)
+}
+
+func (ac Oauth2configMock) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource {
+ return oauth2ConfigokenSourceMock(ctx, t)
+}
+
+func TestGenerateLoginURL(t *testing.T) {
+ funcAssert := assert.New(t)
+ oauth2Provider := Provider{
+ oauth2Config: Oauth2configMock{},
+ oidcProvider: &oidc.Provider{},
+ }
+ // Test-1 : GenerateLoginURL() generates URL correctly with provided state
+ oauth2ConfigAuthCodeURLMock = func(state string, opts ...oauth2.AuthCodeOption) string {
+ // Internally we are testing the private method getRandomStateWithHMAC, this function should always returns
+ // a non-empty string
+ return state
+ }
+ url := oauth2Provider.GenerateLoginURL()
+ funcAssert.NotEqual("", url)
+}
+
+func TestVerifyIdentity(t *testing.T) {
+ ctx := context.Background()
+ funcAssert := assert.New(t)
+ // mock data
+ oauth2Provider := Provider{
+ oauth2Config: Oauth2configMock{},
+ oidcProvider: &oidc.Provider{},
+ }
+ // Test-1 : VerifyIdentity() should fail because of bad state token
+ _, err := oauth2Provider.VerifyIdentity(ctx, "AAABBBCCCDDDEEEFFF", "badtoken")
+ funcAssert.NotNil(err)
+ // Test-2 : VerifyIdentity() should fail because no id_token is provided by the idp
+ oauth2ConfigExchangeMock = func(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
+ return &oauth2.Token{}, nil
+ }
+ state := GetRandomStateWithHMAC(32)
+ code := "AAABBBCCCDDDEEEFFF"
+ _, err = oauth2Provider.VerifyIdentity(ctx, code, state)
+ funcAssert.NotNil(err)
+ // Test-3 : VerifyIdentity() should fail because no id_token is provided by the idp
+ // TODO
+ // Test-4 : VerifyIdentity() should fail because oidcProvider.Verifier returned an error
+ // TODO
+ // Test-5 : VerifyIdentity() should fail because idToken.Claims contains invalid fields
+ // TODO
+}
diff --git a/pkg/auth/jwt/config.go b/pkg/auth/jwt/config.go
index 1acb6ae93c..3ca080b476 100644
--- a/pkg/auth/jwt/config.go
+++ b/pkg/auth/jwt/config.go
@@ -17,45 +17,15 @@
package jwt
import (
- "crypto/rand"
- "io"
"strconv"
- "strings"
"time"
+ "github.com/minio/mcs/pkg/auth/utils"
"github.com/minio/minio/pkg/env"
)
-// Do not use:
-// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
-// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
-// for access/secret keys.
-
-// The alphabet of random character string. Each character must be unique.
-//
-// The RandomCharString implementation requires that: 256 / len(letters) is a natural numbers.
-// For example: 256 / 64 = 4. However, 5 > 256/62 > 4 and therefore we must not use a alphabet
-// of 62 characters.
-// The reason is that if 256 / len(letters) is not a natural number then certain characters become
-// more likely then others.
-const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
-
-func RandomCharString(n int) string {
- random := make([]byte, n)
- if _, err := io.ReadFull(rand.Reader, random); err != nil {
- panic(err) // Can only happen if we would run out of entropy.
- }
-
- var s strings.Builder
- for _, v := range random {
- j := v % byte(len(letters))
- s.WriteByte(letters[j])
- }
- return s.String()
-}
-
// defaultHmacJWTPassphrase will be used by default if application is not configured with a custom MCS_HMAC_JWT_SECRET secret
-var defaultHmacJWTPassphrase = RandomCharString(64)
+var defaultHmacJWTPassphrase = utils.RandomCharString(64)
// GetHmacJWTSecret returns the 64 bytes secret used for signing the generated JWT for the application
func GetHmacJWTSecret() string {
@@ -78,15 +48,14 @@ func GetMcsSTSAndJWTDurationTime() time.Duration {
return time.Duration(duration) * time.Second
}
-// defaultPBKDFPassphrase
-var defaultPBKDFPassphrase = RandomCharString(64)
+var defaultPBKDFPassphrase = utils.RandomCharString(64)
// GetPBKDFPassphrase returns passphrase for the pbkdf2 function used to encrypt JWT payload
func GetPBKDFPassphrase() string {
return env.Get(McsPBKDFPassphrase, defaultPBKDFPassphrase)
}
-var defaultPBKDFSalt = RandomCharString(64)
+var defaultPBKDFSalt = utils.RandomCharString(64)
// GetPBKDFSalt returns salt for the pbkdf2 function used to encrypt JWT payload
func GetPBKDFSalt() string {
diff --git a/pkg/auth/utils/utils.go b/pkg/auth/utils/utils.go
new file mode 100644
index 0000000000..b2d766a38b
--- /dev/null
+++ b/pkg/auth/utils/utils.go
@@ -0,0 +1,60 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+package utils
+
+import (
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "io"
+ "strings"
+)
+
+// Do not use:
+// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
+// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
+// for access/secret keys.
+
+// The alphabet of random character string. Each character must be unique.
+//
+// The RandomCharString implementation requires that: 256 / len(letters) is a natural numbers.
+// For example: 256 / 64 = 4. However, 5 > 256/62 > 4 and therefore we must not use a alphabet
+// of 62 characters.
+// The reason is that if 256 / len(letters) is not a natural number then certain characters become
+// more likely then others.
+const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
+
+func RandomCharString(n int) string {
+ random := make([]byte, n)
+ if _, err := io.ReadFull(rand.Reader, random); err != nil {
+ panic(err) // Can only happen if we would run out of entropy.
+ }
+
+ var s strings.Builder
+ for _, v := range random {
+ j := v % byte(len(letters))
+ s.WriteByte(letters[j])
+ }
+ return s.String()
+}
+
+func ComputeHmac256(message string, key []byte) string {
+ h := hmac.New(sha256.New, key)
+ h.Write([]byte(message))
+ return base64.StdEncoding.EncodeToString(h.Sum(nil))
+}
diff --git a/pkg/auth/utils/utils_test.go b/pkg/auth/utils/utils_test.go
new file mode 100644
index 0000000000..64e7a58aaa
--- /dev/null
+++ b/pkg/auth/utils/utils_test.go
@@ -0,0 +1,46 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+package utils
+
+import (
+ "crypto/sha1"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/crypto/pbkdf2"
+)
+
+func TestRandomCharString(t *testing.T) {
+ funcAssert := assert.New(t)
+ // Test-1 : RandomCharString() should return string with expected length
+ length := 32
+ token := RandomCharString(length)
+ funcAssert.Equal(length, len(token))
+ // Test-2 : RandomCharString() should output random string, new generated string should not be equal to the previous one
+ newToken := RandomCharString(length)
+ funcAssert.NotEqual(token, newToken)
+}
+
+func TestComputeHmac256(t *testing.T) {
+ funcAssert := assert.New(t)
+ // Test-1 : ComputeHmac256() should return the right Hmac256 string based on a derived key
+ var derivedKey = pbkdf2.Key([]byte("secret"), []byte("salt"), 4096, 32, sha1.New)
+ var message = "hello world"
+ var expectedHmac = "5r32q7W+0hcBnqzQwJJUDzVGoVivXGSodTcHSqG/9Q8="
+ hmac := ComputeHmac256(message, derivedKey)
+ funcAssert.Equal(hmac, expectedHmac)
+}
diff --git a/portal-ui/package.json b/portal-ui/package.json
index 0b352ca27e..fdb885add1 100644
--- a/portal-ui/package.json
+++ b/portal-ui/package.json
@@ -62,7 +62,7 @@
"last 1 safari version"
]
},
- "proxy": "http://localhost:9090",
+ "proxy": "http://localhost:9090/",
"devDependencies": {
"prettier": "^1.19.1"
}
diff --git a/portal-ui/src/Routes.tsx b/portal-ui/src/Routes.tsx
index 4fbd859e0b..33f820890b 100644
--- a/portal-ui/src/Routes.tsx
+++ b/portal-ui/src/Routes.tsx
@@ -17,13 +17,14 @@
import React from "react";
import { Redirect, Route, Router, Switch } from "react-router-dom";
import history from "./history";
-import Login from "./screens/LoginPage";
+import Login from "./screens/LoginPage/LoginPage";
import Console from "./screens/Console/Console";
import NotFoundPage from "./screens/NotFoundPage";
import storage from "local-storage-fallback";
import { connect } from "react-redux";
import { AppState } from "./store";
import { userLoggedIn } from "./actions";
+import LoginCallback from "./screens/LoginPage/LoginCallback";
const isLoggedIn = () => {
return (
@@ -55,6 +56,7 @@ class Routes extends React.Component {
return (
+
{this.props.loggedIn ? (
diff --git a/portal-ui/src/screens/LoginPage/LoginCallback.tsx b/portal-ui/src/screens/LoginPage/LoginCallback.tsx
new file mode 100644
index 0000000000..d721f9d5e3
--- /dev/null
+++ b/portal-ui/src/screens/LoginPage/LoginCallback.tsx
@@ -0,0 +1,43 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+import React, {FC, useEffect} from "react";
+import {RouteComponentProps} from "react-router";
+import storage from "local-storage-fallback";
+import api from "../../common/api";
+
+const LoginCallback: FC = ({location}) => {
+ useEffect(() => {
+ const code = (location.search.match(/code=([^&]+)/) || [])[1];
+ const state = (location.search.match(/state=([^&]+)/) || [])[1];
+ api
+ .invoke("POST", "/api/v1/login/oauth2/auth", {code, state})
+ .then((res: any) => {
+ if (res && res.sessionId) {
+ // store the jwt token
+ storage.setItem("token", res.sessionId);
+ // We push to history the new URL.
+ window.location.href = "/dashboard";
+ }
+ })
+ .catch((res: any) => {
+ window.location.href = "/login";
+ });
+ }, []);
+ return null;
+};
+
+export default LoginCallback;
diff --git a/portal-ui/src/screens/LoginPage.tsx b/portal-ui/src/screens/LoginPage/LoginPage.tsx
similarity index 68%
rename from portal-ui/src/screens/LoginPage.tsx
rename to portal-ui/src/screens/LoginPage/LoginPage.tsx
index fcbaa562b6..0aab89b961 100644
--- a/portal-ui/src/screens/LoginPage.tsx
+++ b/portal-ui/src/screens/LoginPage/LoginPage.tsx
@@ -22,18 +22,20 @@ import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
-import { Paper } from "@material-ui/core";
+import { CircularProgress, Paper } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
-import { SystemState } from "../types";
-import { userLoggedIn } from "../actions";
-import history from "../history";
+import { SystemState } from "../../types";
+import { userLoggedIn } from "../../actions";
+import history from "../../history";
+import api from "../../common/api";
+import { ILoginDetails } from "./types";
const styles = (theme: Theme) =>
createStyles({
"@global": {
body: {
- backgroundColor: "#F4F4F4"
- }
+ backgroundColor: "#F4F4F4",
+ },
},
paper: {
marginTop: theme.spacing(16),
@@ -42,45 +44,48 @@ const styles = (theme: Theme) =>
flexDirection: "column",
alignItems: "center",
width: "800px",
- margin: "auto"
+ margin: "auto",
},
avatar: {
margin: theme.spacing(1),
- backgroundColor: theme.palette.secondary.main
+ backgroundColor: theme.palette.secondary.main,
},
form: {
width: "100%", // Fix IE 11 issue.
- marginTop: theme.spacing(3)
+ marginTop: theme.spacing(3),
},
submit: {
- margin: theme.spacing(3, 0, 2)
+ margin: theme.spacing(3, 0, 2),
},
errorBlock: {
- color: "red"
+ color: "red",
},
mainContainer: {
- borderRadius: "3px"
+ borderRadius: "3px",
},
theOcean: {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
background:
- "transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;"
+ "transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;",
},
oceanBg: {
backgroundImage: "url(/images/BG_Illustration.svg)",
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left",
height: "100%",
- width: "100%"
+ width: "100%",
},
theLogin: {
- padding: "76px 62px 20px 62px"
- }
+ padding: "76px 62px 20px 62px",
+ },
+ loadingLoginStrategy: {
+ textAlign: "center",
+ },
});
const mapState = (state: SystemState) => ({
- loggedIn: state.loggedIn
+ loggedIn: state.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });
@@ -90,18 +95,51 @@ const connector = connect(mapState, { userLoggedIn });
type PropsFromRedux = ConnectedProps;
type Props = PropsFromRedux & {};
-interface LoginProps {
+interface ILoginProps {
userLoggedIn: typeof userLoggedIn;
classes: any;
}
-class Login extends React.Component {
- state = {
+interface ILoginState {
+ accessKey: string;
+ secretKey: string;
+ error: string;
+ loading: boolean;
+ loginStrategy: ILoginDetails;
+}
+
+class Login extends React.Component {
+ state: ILoginState = {
accessKey: "",
secretKey: "",
- error: ""
+ error: "",
+ loading: false,
+ loginStrategy: {
+ loginStrategy: "",
+ redirect: "",
+ },
};
+ fetchConfiguration() {
+ this.setState({ loading: true }, () => {
+ api
+ .invoke("GET", "/api/v1/login")
+ .then((loginDetails: ILoginDetails) => {
+ this.setState({
+ loading: false,
+ });
+ this.setState({
+ loading: false,
+ loginStrategy: loginDetails,
+ error: "",
+ });
+ })
+ .catch((err: any) => {
+ this.setState({ loading: false, error: err });
+ });
+ });
+ }
+
formSubmit = (e: React.FormEvent) => {
e.preventDefault();
const url = "/api/v1/login";
@@ -128,21 +166,25 @@ class Login extends React.Component {
// We push to history the new URL.
history.push("/dashboard");
})
- .catch(err => {
+ .catch((err) => {
this.setState({ error: `${err}` });
});
};
+ componentDidMount(): void {
+ this.fetchConfiguration();
+ }
+
render() {
- const { error, accessKey, secretKey } = this.state;
+ const { error, accessKey, secretKey, loginStrategy } = this.state;
const { classes } = this.props;
- return (
-
-
-
-
-
-
+
+ let loginComponent = null;
+
+ switch (loginStrategy.loginStrategy) {
+ case "form": {
+ loginComponent = (
+
Login
@@ -203,6 +245,45 @@ class Login extends React.Component {
Login
+
+ );
+ break;
+ }
+ case "redirect": {
+ loginComponent = (
+
+
+ Login
+
+
+
+ );
+ break;
+ }
+ default:
+ loginComponent = (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {loginComponent}
diff --git a/portal-ui/src/screens/LoginPage/types.ts b/portal-ui/src/screens/LoginPage/types.ts
new file mode 100644
index 0000000000..eff9161ebf
--- /dev/null
+++ b/portal-ui/src/screens/LoginPage/types.ts
@@ -0,0 +1,20 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+
+export interface ILoginDetails {
+ loginStrategy: string;
+ redirect: string;
+}
diff --git a/restapi/client.go b/restapi/client.go
index 4843aee591..366bae70f5 100644
--- a/restapi/client.go
+++ b/restapi/client.go
@@ -119,9 +119,6 @@ type MCSCredentials interface {
}
// Interface implementation
-//
-// Define the structure of a mc S3Client and define the functions that are actually used
-// from mcsCredentials api.
type mcsCredentials struct {
minioCredentials *credentials.Credentials
}
diff --git a/restapi/consts.go b/restapi/consts.go
index 9db7c9014e..9d739adf0b 100644
--- a/restapi/consts.go
+++ b/restapi/consts.go
@@ -17,6 +17,7 @@
package restapi
const (
+ // consts for common configuration
McsVersion = `0.1.0`
McsAccessKey = "MCS_ACCESS_KEY"
McsSecretKey = "MCS_SECRET_KEY"
@@ -27,6 +28,7 @@ const (
McsTLSHostname = "MCS_TLS_HOSTNAME"
McsTLSPort = "MCS_TLS_PORT"
+ // consts for Secure middleware
McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS"
McsSecureAllowedHostsAreRegex = "MCS_SECURE_ALLOWED_HOSTS_ARE_REGEX"
McsSecureFrameDeny = "MCS_SECURE_FRAME_DENY"
diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go
index 446d8526b7..1dacc48a34 100644
--- a/restapi/embedded_spec.go
+++ b/restapi/embedded_spec.go
@@ -754,6 +754,40 @@ func init() {
}
}
},
+ "/login/oauth2/auth": {
+ "post": {
+ "security": [],
+ "tags": [
+ "UserAPI"
+ ],
+ "summary": "Identity Provider oauth2 callback endpoint.",
+ "operationId": "LoginOauth2Auth",
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/loginOauth2AuthRequest"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "A successful login.",
+ "schema": {
+ "$ref": "#/definitions/loginResponse"
+ }
+ },
+ "default": {
+ "description": "Generic error response.",
+ "schema": {
+ "$ref": "#/definitions/error"
+ }
+ }
+ }
+ }
+ },
"/logout": {
"post": {
"tags": [
@@ -1630,6 +1664,21 @@ func init() {
}
}
},
+ "loginOauth2AuthRequest": {
+ "type": "object",
+ "required": [
+ "state",
+ "code"
+ ],
+ "properties": {
+ "code": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ }
+ }
+ },
"loginRequest": {
"type": "object",
"required": [
@@ -2745,6 +2794,40 @@ func init() {
}
}
},
+ "/login/oauth2/auth": {
+ "post": {
+ "security": [],
+ "tags": [
+ "UserAPI"
+ ],
+ "summary": "Identity Provider oauth2 callback endpoint.",
+ "operationId": "LoginOauth2Auth",
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/loginOauth2AuthRequest"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "A successful login.",
+ "schema": {
+ "$ref": "#/definitions/loginResponse"
+ }
+ },
+ "default": {
+ "description": "Generic error response.",
+ "schema": {
+ "$ref": "#/definitions/error"
+ }
+ }
+ }
+ }
+ },
"/logout": {
"post": {
"tags": [
@@ -3621,6 +3704,21 @@ func init() {
}
}
},
+ "loginOauth2AuthRequest": {
+ "type": "object",
+ "required": [
+ "state",
+ "code"
+ ],
+ "properties": {
+ "code": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ }
+ }
+ },
"loginRequest": {
"type": "object",
"required": [
diff --git a/restapi/operations/mcs_api.go b/restapi/operations/mcs_api.go
index ba8205e19f..d60e825156 100644
--- a/restapi/operations/mcs_api.go
+++ b/restapi/operations/mcs_api.go
@@ -135,6 +135,9 @@ func NewMcsAPI(spec *loads.Document) *McsAPI {
UserAPILoginDetailHandler: user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
return middleware.NotImplemented("operation user_api.LoginDetail has not yet been implemented")
}),
+ UserAPILoginOauth2AuthHandler: user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
+ return middleware.NotImplemented("operation user_api.LoginOauth2Auth has not yet been implemented")
+ }),
UserAPILogoutHandler: user_api.LogoutHandlerFunc(func(params user_api.LogoutParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.Logout has not yet been implemented")
}),
@@ -280,6 +283,8 @@ type McsAPI struct {
UserAPILoginHandler user_api.LoginHandler
// UserAPILoginDetailHandler sets the operation handler for the login detail operation
UserAPILoginDetailHandler user_api.LoginDetailHandler
+ // UserAPILoginOauth2AuthHandler sets the operation handler for the login oauth2 auth operation
+ UserAPILoginOauth2AuthHandler user_api.LoginOauth2AuthHandler
// UserAPILogoutHandler sets the operation handler for the logout operation
UserAPILogoutHandler user_api.LogoutHandler
// UserAPIMakeBucketHandler sets the operation handler for the make bucket operation
@@ -457,6 +462,9 @@ func (o *McsAPI) Validate() error {
if o.UserAPILoginDetailHandler == nil {
unregistered = append(unregistered, "user_api.LoginDetailHandler")
}
+ if o.UserAPILoginOauth2AuthHandler == nil {
+ unregistered = append(unregistered, "user_api.LoginOauth2AuthHandler")
+ }
if o.UserAPILogoutHandler == nil {
unregistered = append(unregistered, "user_api.LogoutHandler")
}
@@ -704,6 +712,10 @@ func (o *McsAPI) initHandlerCache() {
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}
+ o.handlers["POST"]["/login/oauth2/auth"] = user_api.NewLoginOauth2Auth(o.context, o.UserAPILoginOauth2AuthHandler)
+ if o.handlers["POST"] == nil {
+ o.handlers["POST"] = make(map[string]http.Handler)
+ }
o.handlers["POST"]["/logout"] = user_api.NewLogout(o.context, o.UserAPILogoutHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
diff --git a/restapi/operations/user_api/login_oauth2_auth.go b/restapi/operations/user_api/login_oauth2_auth.go
new file mode 100644
index 0000000000..509091ec99
--- /dev/null
+++ b/restapi/operations/user_api/login_oauth2_auth.go
@@ -0,0 +1,75 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+ "net/http"
+
+ "github.com/go-openapi/runtime/middleware"
+)
+
+// LoginOauth2AuthHandlerFunc turns a function with the right signature into a login oauth2 auth handler
+type LoginOauth2AuthHandlerFunc func(LoginOauth2AuthParams) middleware.Responder
+
+// Handle executing the request and returning a response
+func (fn LoginOauth2AuthHandlerFunc) Handle(params LoginOauth2AuthParams) middleware.Responder {
+ return fn(params)
+}
+
+// LoginOauth2AuthHandler interface for that can handle valid login oauth2 auth params
+type LoginOauth2AuthHandler interface {
+ Handle(LoginOauth2AuthParams) middleware.Responder
+}
+
+// NewLoginOauth2Auth creates a new http.Handler for the login oauth2 auth operation
+func NewLoginOauth2Auth(ctx *middleware.Context, handler LoginOauth2AuthHandler) *LoginOauth2Auth {
+ return &LoginOauth2Auth{Context: ctx, Handler: handler}
+}
+
+/*LoginOauth2Auth swagger:route POST /login/oauth2/auth UserAPI loginOauth2Auth
+
+Identity Provider oauth2 callback endpoint.
+
+*/
+type LoginOauth2Auth struct {
+ Context *middleware.Context
+ Handler LoginOauth2AuthHandler
+}
+
+func (o *LoginOauth2Auth) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+ route, rCtx, _ := o.Context.RouteInfo(r)
+ if rCtx != nil {
+ r = rCtx
+ }
+ var Params = NewLoginOauth2AuthParams()
+
+ if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
+ o.Context.Respond(rw, r, route.Produces, route, err)
+ return
+ }
+
+ res := o.Handler.Handle(Params) // actually handle the request
+
+ o.Context.Respond(rw, r, route.Produces, route, res)
+
+}
diff --git a/restapi/operations/user_api/login_oauth2_auth_parameters.go b/restapi/operations/user_api/login_oauth2_auth_parameters.go
new file mode 100644
index 0000000000..2dc27c67fd
--- /dev/null
+++ b/restapi/operations/user_api/login_oauth2_auth_parameters.go
@@ -0,0 +1,94 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// 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/minio/mcs/models"
+)
+
+// NewLoginOauth2AuthParams creates a new LoginOauth2AuthParams object
+// no default values defined in spec.
+func NewLoginOauth2AuthParams() LoginOauth2AuthParams {
+
+ return LoginOauth2AuthParams{}
+}
+
+// LoginOauth2AuthParams contains all the bound params for the login oauth2 auth operation
+// typically these are obtained from a http.Request
+//
+// swagger:parameters LoginOauth2Auth
+type LoginOauth2AuthParams struct {
+
+ // HTTP Request Object
+ HTTPRequest *http.Request `json:"-"`
+
+ /*
+ Required: true
+ In: body
+ */
+ Body *models.LoginOauth2AuthRequest
+}
+
+// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
+// for simple values it will use straight method calls.
+//
+// To ensure default values, the struct must have been initialized with NewLoginOauth2AuthParams() beforehand.
+func (o *LoginOauth2AuthParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
+ var res []error
+
+ o.HTTPRequest = r
+
+ if runtime.HasBody(r) {
+ defer r.Body.Close()
+ var body models.LoginOauth2AuthRequest
+ 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)
+ }
+
+ if len(res) == 0 {
+ o.Body = &body
+ }
+ }
+ } else {
+ res = append(res, errors.Required("body", "body"))
+ }
+ if len(res) > 0 {
+ return errors.CompositeValidationError(res...)
+ }
+ return nil
+}
diff --git a/restapi/operations/user_api/login_oauth2_auth_responses.go b/restapi/operations/user_api/login_oauth2_auth_responses.go
new file mode 100644
index 0000000000..acc3d4fed0
--- /dev/null
+++ b/restapi/operations/user_api/login_oauth2_auth_responses.go
@@ -0,0 +1,133 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "net/http"
+
+ "github.com/go-openapi/runtime"
+
+ "github.com/minio/mcs/models"
+)
+
+// LoginOauth2AuthCreatedCode is the HTTP code returned for type LoginOauth2AuthCreated
+const LoginOauth2AuthCreatedCode int = 201
+
+/*LoginOauth2AuthCreated A successful login.
+
+swagger:response loginOauth2AuthCreated
+*/
+type LoginOauth2AuthCreated struct {
+
+ /*
+ In: Body
+ */
+ Payload *models.LoginResponse `json:"body,omitempty"`
+}
+
+// NewLoginOauth2AuthCreated creates LoginOauth2AuthCreated with default headers values
+func NewLoginOauth2AuthCreated() *LoginOauth2AuthCreated {
+
+ return &LoginOauth2AuthCreated{}
+}
+
+// WithPayload adds the payload to the login oauth2 auth created response
+func (o *LoginOauth2AuthCreated) WithPayload(payload *models.LoginResponse) *LoginOauth2AuthCreated {
+ o.Payload = payload
+ return o
+}
+
+// SetPayload sets the payload to the login oauth2 auth created response
+func (o *LoginOauth2AuthCreated) SetPayload(payload *models.LoginResponse) {
+ o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *LoginOauth2AuthCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+ rw.WriteHeader(201)
+ if o.Payload != nil {
+ payload := o.Payload
+ if err := producer.Produce(rw, payload); err != nil {
+ panic(err) // let the recovery middleware deal with this
+ }
+ }
+}
+
+/*LoginOauth2AuthDefault Generic error response.
+
+swagger:response loginOauth2AuthDefault
+*/
+type LoginOauth2AuthDefault struct {
+ _statusCode int
+
+ /*
+ In: Body
+ */
+ Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewLoginOauth2AuthDefault creates LoginOauth2AuthDefault with default headers values
+func NewLoginOauth2AuthDefault(code int) *LoginOauth2AuthDefault {
+ if code <= 0 {
+ code = 500
+ }
+
+ return &LoginOauth2AuthDefault{
+ _statusCode: code,
+ }
+}
+
+// WithStatusCode adds the status to the login oauth2 auth default response
+func (o *LoginOauth2AuthDefault) WithStatusCode(code int) *LoginOauth2AuthDefault {
+ o._statusCode = code
+ return o
+}
+
+// SetStatusCode sets the status to the login oauth2 auth default response
+func (o *LoginOauth2AuthDefault) SetStatusCode(code int) {
+ o._statusCode = code
+}
+
+// WithPayload adds the payload to the login oauth2 auth default response
+func (o *LoginOauth2AuthDefault) WithPayload(payload *models.Error) *LoginOauth2AuthDefault {
+ o.Payload = payload
+ return o
+}
+
+// SetPayload sets the payload to the login oauth2 auth default response
+func (o *LoginOauth2AuthDefault) SetPayload(payload *models.Error) {
+ o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *LoginOauth2AuthDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+ rw.WriteHeader(o._statusCode)
+ if o.Payload != nil {
+ payload := o.Payload
+ if err := producer.Produce(rw, payload); err != nil {
+ panic(err) // let the recovery middleware deal with this
+ }
+ }
+}
diff --git a/restapi/operations/user_api/login_oauth2_auth_urlbuilder.go b/restapi/operations/user_api/login_oauth2_auth_urlbuilder.go
new file mode 100644
index 0000000000..0488a6dfa0
--- /dev/null
+++ b/restapi/operations/user_api/login_oauth2_auth_urlbuilder.go
@@ -0,0 +1,104 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+ "errors"
+ "net/url"
+ golangswaggerpaths "path"
+)
+
+// LoginOauth2AuthURL generates an URL for the login oauth2 auth operation
+type LoginOauth2AuthURL struct {
+ _basePath string
+}
+
+// WithBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *LoginOauth2AuthURL) WithBasePath(bp string) *LoginOauth2AuthURL {
+ o.SetBasePath(bp)
+ return o
+}
+
+// SetBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *LoginOauth2AuthURL) SetBasePath(bp string) {
+ o._basePath = bp
+}
+
+// Build a url path and query string
+func (o *LoginOauth2AuthURL) Build() (*url.URL, error) {
+ var _result url.URL
+
+ var _path = "/login/oauth2/auth"
+
+ _basePath := o._basePath
+ if _basePath == "" {
+ _basePath = "/api/v1"
+ }
+ _result.Path = golangswaggerpaths.Join(_basePath, _path)
+
+ return &_result, nil
+}
+
+// Must is a helper function to panic when the url builder returns an error
+func (o *LoginOauth2AuthURL) Must(u *url.URL, err error) *url.URL {
+ if err != nil {
+ panic(err)
+ }
+ if u == nil {
+ panic("url can't be nil")
+ }
+ return u
+}
+
+// String returns the string representation of the path with query string
+func (o *LoginOauth2AuthURL) String() string {
+ return o.Must(o.Build()).String()
+}
+
+// BuildFull builds a full url with scheme, host, path and query string
+func (o *LoginOauth2AuthURL) BuildFull(scheme, host string) (*url.URL, error) {
+ if scheme == "" {
+ return nil, errors.New("scheme is required for a full url on LoginOauth2AuthURL")
+ }
+ if host == "" {
+ return nil, errors.New("host is required for a full url on LoginOauth2AuthURL")
+ }
+
+ base, err := o.Build()
+ if err != nil {
+ return nil, err
+ }
+
+ base.Scheme = scheme
+ base.Host = host
+ return base, nil
+}
+
+// StringFull returns the string representation of a complete url
+func (o *LoginOauth2AuthURL) StringFull(scheme, host string) string {
+ return o.Must(o.BuildFull(scheme, host)).String()
+}
diff --git a/restapi/operations/user_api/login_oauth2_callback.go b/restapi/operations/user_api/login_oauth2_callback.go
new file mode 100644
index 0000000000..e68e31d7f8
--- /dev/null
+++ b/restapi/operations/user_api/login_oauth2_callback.go
@@ -0,0 +1,75 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+ "net/http"
+
+ "github.com/go-openapi/runtime/middleware"
+)
+
+// LoginOauth2CallbackHandlerFunc turns a function with the right signature into a login oauth2 callback handler
+type LoginOauth2CallbackHandlerFunc func(LoginOauth2CallbackParams) middleware.Responder
+
+// Handle executing the request and returning a response
+func (fn LoginOauth2CallbackHandlerFunc) Handle(params LoginOauth2CallbackParams) middleware.Responder {
+ return fn(params)
+}
+
+// LoginOauth2CallbackHandler interface for that can handle valid login oauth2 callback params
+type LoginOauth2CallbackHandler interface {
+ Handle(LoginOauth2CallbackParams) middleware.Responder
+}
+
+// NewLoginOauth2Callback creates a new http.Handler for the login oauth2 callback operation
+func NewLoginOauth2Callback(ctx *middleware.Context, handler LoginOauth2CallbackHandler) *LoginOauth2Callback {
+ return &LoginOauth2Callback{Context: ctx, Handler: handler}
+}
+
+/*LoginOauth2Callback swagger:route GET /login/oauth2/callback UserAPI loginOauth2Callback
+
+Identity Provider oauth2 callback endpoint.
+
+*/
+type LoginOauth2Callback struct {
+ Context *middleware.Context
+ Handler LoginOauth2CallbackHandler
+}
+
+func (o *LoginOauth2Callback) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+ route, rCtx, _ := o.Context.RouteInfo(r)
+ if rCtx != nil {
+ r = rCtx
+ }
+ var Params = NewLoginOauth2CallbackParams()
+
+ if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
+ o.Context.Respond(rw, r, route.Produces, route, err)
+ return
+ }
+
+ res := o.Handler.Handle(Params) // actually handle the request
+
+ o.Context.Respond(rw, r, route.Produces, route, res)
+
+}
diff --git a/restapi/operations/user_api/login_oauth2_callback_parameters.go b/restapi/operations/user_api/login_oauth2_callback_parameters.go
new file mode 100644
index 0000000000..cfa3ad4da8
--- /dev/null
+++ b/restapi/operations/user_api/login_oauth2_callback_parameters.go
@@ -0,0 +1,62 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "net/http"
+
+ "github.com/go-openapi/errors"
+ "github.com/go-openapi/runtime/middleware"
+)
+
+// NewLoginOauth2CallbackParams creates a new LoginOauth2CallbackParams object
+// no default values defined in spec.
+func NewLoginOauth2CallbackParams() LoginOauth2CallbackParams {
+
+ return LoginOauth2CallbackParams{}
+}
+
+// LoginOauth2CallbackParams contains all the bound params for the login oauth2 callback operation
+// typically these are obtained from a http.Request
+//
+// swagger:parameters LoginOauth2Callback
+type LoginOauth2CallbackParams struct {
+
+ // HTTP Request Object
+ HTTPRequest *http.Request `json:"-"`
+}
+
+// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
+// for simple values it will use straight method calls.
+//
+// To ensure default values, the struct must have been initialized with NewLoginOauth2CallbackParams() beforehand.
+func (o *LoginOauth2CallbackParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
+ var res []error
+
+ o.HTTPRequest = r
+
+ if len(res) > 0 {
+ return errors.CompositeValidationError(res...)
+ }
+ return nil
+}
diff --git a/restapi/operations/user_api/login_oauth2_callback_responses.go b/restapi/operations/user_api/login_oauth2_callback_responses.go
new file mode 100644
index 0000000000..63df5bfede
--- /dev/null
+++ b/restapi/operations/user_api/login_oauth2_callback_responses.go
@@ -0,0 +1,113 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "net/http"
+
+ "github.com/go-openapi/runtime"
+
+ "github.com/minio/mcs/models"
+)
+
+// LoginOauth2CallbackOKCode is the HTTP code returned for type LoginOauth2CallbackOK
+const LoginOauth2CallbackOKCode int = 200
+
+/*LoginOauth2CallbackOK A successful response.
+
+swagger:response loginOauth2CallbackOK
+*/
+type LoginOauth2CallbackOK struct {
+}
+
+// NewLoginOauth2CallbackOK creates LoginOauth2CallbackOK with default headers values
+func NewLoginOauth2CallbackOK() *LoginOauth2CallbackOK {
+
+ return &LoginOauth2CallbackOK{}
+}
+
+// WriteResponse to the client
+func (o *LoginOauth2CallbackOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+ rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
+
+ rw.WriteHeader(200)
+}
+
+/*LoginOauth2CallbackDefault Generic error response.
+
+swagger:response loginOauth2CallbackDefault
+*/
+type LoginOauth2CallbackDefault struct {
+ _statusCode int
+
+ /*
+ In: Body
+ */
+ Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewLoginOauth2CallbackDefault creates LoginOauth2CallbackDefault with default headers values
+func NewLoginOauth2CallbackDefault(code int) *LoginOauth2CallbackDefault {
+ if code <= 0 {
+ code = 500
+ }
+
+ return &LoginOauth2CallbackDefault{
+ _statusCode: code,
+ }
+}
+
+// WithStatusCode adds the status to the login oauth2 callback default response
+func (o *LoginOauth2CallbackDefault) WithStatusCode(code int) *LoginOauth2CallbackDefault {
+ o._statusCode = code
+ return o
+}
+
+// SetStatusCode sets the status to the login oauth2 callback default response
+func (o *LoginOauth2CallbackDefault) SetStatusCode(code int) {
+ o._statusCode = code
+}
+
+// WithPayload adds the payload to the login oauth2 callback default response
+func (o *LoginOauth2CallbackDefault) WithPayload(payload *models.Error) *LoginOauth2CallbackDefault {
+ o.Payload = payload
+ return o
+}
+
+// SetPayload sets the payload to the login oauth2 callback default response
+func (o *LoginOauth2CallbackDefault) SetPayload(payload *models.Error) {
+ o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *LoginOauth2CallbackDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+ rw.WriteHeader(o._statusCode)
+ if o.Payload != nil {
+ payload := o.Payload
+ if err := producer.Produce(rw, payload); err != nil {
+ panic(err) // let the recovery middleware deal with this
+ }
+ }
+}
diff --git a/restapi/operations/user_api/login_oauth2_callback_urlbuilder.go b/restapi/operations/user_api/login_oauth2_callback_urlbuilder.go
new file mode 100644
index 0000000000..827224591d
--- /dev/null
+++ b/restapi/operations/user_api/login_oauth2_callback_urlbuilder.go
@@ -0,0 +1,104 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+ "errors"
+ "net/url"
+ golangswaggerpaths "path"
+)
+
+// LoginOauth2CallbackURL generates an URL for the login oauth2 callback operation
+type LoginOauth2CallbackURL struct {
+ _basePath string
+}
+
+// WithBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *LoginOauth2CallbackURL) WithBasePath(bp string) *LoginOauth2CallbackURL {
+ o.SetBasePath(bp)
+ return o
+}
+
+// SetBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *LoginOauth2CallbackURL) SetBasePath(bp string) {
+ o._basePath = bp
+}
+
+// Build a url path and query string
+func (o *LoginOauth2CallbackURL) Build() (*url.URL, error) {
+ var _result url.URL
+
+ var _path = "/login/oauth2/callback"
+
+ _basePath := o._basePath
+ if _basePath == "" {
+ _basePath = "/api/v1"
+ }
+ _result.Path = golangswaggerpaths.Join(_basePath, _path)
+
+ return &_result, nil
+}
+
+// Must is a helper function to panic when the url builder returns an error
+func (o *LoginOauth2CallbackURL) Must(u *url.URL, err error) *url.URL {
+ if err != nil {
+ panic(err)
+ }
+ if u == nil {
+ panic("url can't be nil")
+ }
+ return u
+}
+
+// String returns the string representation of the path with query string
+func (o *LoginOauth2CallbackURL) String() string {
+ return o.Must(o.Build()).String()
+}
+
+// BuildFull builds a full url with scheme, host, path and query string
+func (o *LoginOauth2CallbackURL) BuildFull(scheme, host string) (*url.URL, error) {
+ if scheme == "" {
+ return nil, errors.New("scheme is required for a full url on LoginOauth2CallbackURL")
+ }
+ if host == "" {
+ return nil, errors.New("host is required for a full url on LoginOauth2CallbackURL")
+ }
+
+ base, err := o.Build()
+ if err != nil {
+ return nil, err
+ }
+
+ base.Scheme = scheme
+ base.Host = host
+ return base, nil
+}
+
+// StringFull returns the string representation of a complete url
+func (o *LoginOauth2CallbackURL) StringFull(scheme, host string) string {
+ return o.Must(o.BuildFull(scheme, host)).String()
+}
diff --git a/restapi/operations/user_api/oauth2_callback.go b/restapi/operations/user_api/oauth2_callback.go
new file mode 100644
index 0000000000..e87cb4fded
--- /dev/null
+++ b/restapi/operations/user_api/oauth2_callback.go
@@ -0,0 +1,75 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+ "net/http"
+
+ "github.com/go-openapi/runtime/middleware"
+)
+
+// Oauth2CallbackHandlerFunc turns a function with the right signature into a oauth2 callback handler
+type Oauth2CallbackHandlerFunc func(Oauth2CallbackParams) middleware.Responder
+
+// Handle executing the request and returning a response
+func (fn Oauth2CallbackHandlerFunc) Handle(params Oauth2CallbackParams) middleware.Responder {
+ return fn(params)
+}
+
+// Oauth2CallbackHandler interface for that can handle valid oauth2 callback params
+type Oauth2CallbackHandler interface {
+ Handle(Oauth2CallbackParams) middleware.Responder
+}
+
+// NewOauth2Callback creates a new http.Handler for the oauth2 callback operation
+func NewOauth2Callback(ctx *middleware.Context, handler Oauth2CallbackHandler) *Oauth2Callback {
+ return &Oauth2Callback{Context: ctx, Handler: handler}
+}
+
+/*Oauth2Callback swagger:route GET /login/oauth2/callback UserAPI oauth2Callback
+
+Identity Provider oauth2 callback endpoint.
+
+*/
+type Oauth2Callback struct {
+ Context *middleware.Context
+ Handler Oauth2CallbackHandler
+}
+
+func (o *Oauth2Callback) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+ route, rCtx, _ := o.Context.RouteInfo(r)
+ if rCtx != nil {
+ r = rCtx
+ }
+ var Params = NewOauth2CallbackParams()
+
+ if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
+ o.Context.Respond(rw, r, route.Produces, route, err)
+ return
+ }
+
+ res := o.Handler.Handle(Params) // actually handle the request
+
+ o.Context.Respond(rw, r, route.Produces, route, res)
+
+}
diff --git a/restapi/operations/user_api/oauth2_callback_parameters.go b/restapi/operations/user_api/oauth2_callback_parameters.go
new file mode 100644
index 0000000000..a3d2d6b6ce
--- /dev/null
+++ b/restapi/operations/user_api/oauth2_callback_parameters.go
@@ -0,0 +1,62 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "net/http"
+
+ "github.com/go-openapi/errors"
+ "github.com/go-openapi/runtime/middleware"
+)
+
+// NewOauth2CallbackParams creates a new Oauth2CallbackParams object
+// no default values defined in spec.
+func NewOauth2CallbackParams() Oauth2CallbackParams {
+
+ return Oauth2CallbackParams{}
+}
+
+// Oauth2CallbackParams contains all the bound params for the oauth2 callback operation
+// typically these are obtained from a http.Request
+//
+// swagger:parameters Oauth2Callback
+type Oauth2CallbackParams struct {
+
+ // HTTP Request Object
+ HTTPRequest *http.Request `json:"-"`
+}
+
+// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
+// for simple values it will use straight method calls.
+//
+// To ensure default values, the struct must have been initialized with NewOauth2CallbackParams() beforehand.
+func (o *Oauth2CallbackParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
+ var res []error
+
+ o.HTTPRequest = r
+
+ if len(res) > 0 {
+ return errors.CompositeValidationError(res...)
+ }
+ return nil
+}
diff --git a/restapi/operations/user_api/oauth2_callback_responses.go b/restapi/operations/user_api/oauth2_callback_responses.go
new file mode 100644
index 0000000000..158073fd4a
--- /dev/null
+++ b/restapi/operations/user_api/oauth2_callback_responses.go
@@ -0,0 +1,113 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "net/http"
+
+ "github.com/go-openapi/runtime"
+
+ "github.com/minio/mcs/models"
+)
+
+// Oauth2CallbackOKCode is the HTTP code returned for type Oauth2CallbackOK
+const Oauth2CallbackOKCode int = 200
+
+/*Oauth2CallbackOK A successful response.
+
+swagger:response oauth2CallbackOK
+*/
+type Oauth2CallbackOK struct {
+}
+
+// NewOauth2CallbackOK creates Oauth2CallbackOK with default headers values
+func NewOauth2CallbackOK() *Oauth2CallbackOK {
+
+ return &Oauth2CallbackOK{}
+}
+
+// WriteResponse to the client
+func (o *Oauth2CallbackOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+ rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
+
+ rw.WriteHeader(200)
+}
+
+/*Oauth2CallbackDefault Generic error response.
+
+swagger:response oauth2CallbackDefault
+*/
+type Oauth2CallbackDefault struct {
+ _statusCode int
+
+ /*
+ In: Body
+ */
+ Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewOauth2CallbackDefault creates Oauth2CallbackDefault with default headers values
+func NewOauth2CallbackDefault(code int) *Oauth2CallbackDefault {
+ if code <= 0 {
+ code = 500
+ }
+
+ return &Oauth2CallbackDefault{
+ _statusCode: code,
+ }
+}
+
+// WithStatusCode adds the status to the oauth2 callback default response
+func (o *Oauth2CallbackDefault) WithStatusCode(code int) *Oauth2CallbackDefault {
+ o._statusCode = code
+ return o
+}
+
+// SetStatusCode sets the status to the oauth2 callback default response
+func (o *Oauth2CallbackDefault) SetStatusCode(code int) {
+ o._statusCode = code
+}
+
+// WithPayload adds the payload to the oauth2 callback default response
+func (o *Oauth2CallbackDefault) WithPayload(payload *models.Error) *Oauth2CallbackDefault {
+ o.Payload = payload
+ return o
+}
+
+// SetPayload sets the payload to the oauth2 callback default response
+func (o *Oauth2CallbackDefault) SetPayload(payload *models.Error) {
+ o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *Oauth2CallbackDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+ rw.WriteHeader(o._statusCode)
+ if o.Payload != nil {
+ payload := o.Payload
+ if err := producer.Produce(rw, payload); err != nil {
+ panic(err) // let the recovery middleware deal with this
+ }
+ }
+}
diff --git a/restapi/operations/user_api/oauth2_callback_urlbuilder.go b/restapi/operations/user_api/oauth2_callback_urlbuilder.go
new file mode 100644
index 0000000000..a2dd37292d
--- /dev/null
+++ b/restapi/operations/user_api/oauth2_callback_urlbuilder.go
@@ -0,0 +1,104 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 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 .
+//
+
+package user_api
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+ "errors"
+ "net/url"
+ golangswaggerpaths "path"
+)
+
+// Oauth2CallbackURL generates an URL for the oauth2 callback operation
+type Oauth2CallbackURL struct {
+ _basePath string
+}
+
+// WithBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *Oauth2CallbackURL) WithBasePath(bp string) *Oauth2CallbackURL {
+ o.SetBasePath(bp)
+ return o
+}
+
+// SetBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *Oauth2CallbackURL) SetBasePath(bp string) {
+ o._basePath = bp
+}
+
+// Build a url path and query string
+func (o *Oauth2CallbackURL) Build() (*url.URL, error) {
+ var _result url.URL
+
+ var _path = "/login/oauth2/callback"
+
+ _basePath := o._basePath
+ if _basePath == "" {
+ _basePath = "/api/v1"
+ }
+ _result.Path = golangswaggerpaths.Join(_basePath, _path)
+
+ return &_result, nil
+}
+
+// Must is a helper function to panic when the url builder returns an error
+func (o *Oauth2CallbackURL) Must(u *url.URL, err error) *url.URL {
+ if err != nil {
+ panic(err)
+ }
+ if u == nil {
+ panic("url can't be nil")
+ }
+ return u
+}
+
+// String returns the string representation of the path with query string
+func (o *Oauth2CallbackURL) String() string {
+ return o.Must(o.Build()).String()
+}
+
+// BuildFull builds a full url with scheme, host, path and query string
+func (o *Oauth2CallbackURL) BuildFull(scheme, host string) (*url.URL, error) {
+ if scheme == "" {
+ return nil, errors.New("scheme is required for a full url on Oauth2CallbackURL")
+ }
+ if host == "" {
+ return nil, errors.New("host is required for a full url on Oauth2CallbackURL")
+ }
+
+ base, err := o.Build()
+ if err != nil {
+ return nil, err
+ }
+
+ base.Scheme = scheme
+ base.Host = host
+ return base, nil
+}
+
+// StringFull returns the string representation of a complete url
+func (o *Oauth2CallbackURL) StringFull(scheme, host string) string {
+ return o.Must(o.BuildFull(scheme, host)).String()
+}
diff --git a/restapi/user_login.go b/restapi/user_login.go
index 485556d4e9..4cec9b0c14 100644
--- a/restapi/user_login.go
+++ b/restapi/user_login.go
@@ -17,6 +17,7 @@
package restapi
import (
+ "context"
"errors"
"log"
@@ -24,6 +25,8 @@ import (
"github.com/go-openapi/swag"
"github.com/minio/mcs/models"
"github.com/minio/mcs/pkg/auth"
+ "github.com/minio/mcs/pkg/auth/idp/oauth2"
+ "github.com/minio/mcs/pkg/auth/utils"
"github.com/minio/mcs/restapi/operations"
"github.com/minio/mcs/restapi/operations/user_api"
)
@@ -31,7 +34,10 @@ import (
func registerLoginHandlers(api *operations.McsAPI) {
// get login strategy
api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
- loginDetails := getLoginDetailsResponse()
+ loginDetails, err := getLoginDetailsResponse()
+ if err != nil {
+ return user_api.NewLoginDetailDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
+ }
return user_api.NewLoginDetailOK().WithPayload(loginDetails)
})
// post login
@@ -42,6 +48,13 @@ func registerLoginHandlers(api *operations.McsAPI) {
}
return user_api.NewLoginCreated().WithPayload(loginResponse)
})
+ api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
+ loginResponse, err := getLoginOauth2AuthResponse(params.Body)
+ if err != nil {
+ return user_api.NewLoginOauth2AuthDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
+ }
+ return user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse)
+ })
}
var errInvalidCredentials = errors.New("invalid minioCredentials")
@@ -81,12 +94,95 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, error) {
return loginResponse, nil
}
-// getLoginDetailsResponse returns wether an IDP is configured or not.
-func getLoginDetailsResponse() *models.LoginDetails {
- // TODO: Add support for login using external IDPs
- // serialize output
+// getLoginDetailsResponse returns information regarding the MCS authentication mechanism.
+func getLoginDetailsResponse() (*models.LoginDetails, error) {
+ ctx := context.Background()
+ loginStrategy := models.LoginDetailsLoginStrategyForm
+ redirectURL := ""
+ if oauth2.IsIdpEnabled() {
+ loginStrategy = models.LoginDetailsLoginStrategyRedirect
+ // initialize new oauth2 client
+ oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
+ if err != nil {
+ return nil, err
+ }
+ // Validate user against IDP
+ identityProvider := &auth.IdentityProvider{Client: oauth2Client}
+ redirectURL = identityProvider.GenerateLoginURL()
+ }
loginDetails := &models.LoginDetails{
- LoginStrategy: models.LoginDetailsLoginStrategyForm,
+ LoginStrategy: loginStrategy,
+ Redirect: redirectURL,
+ }
+ return loginDetails, nil
+}
+
+func loginOauth2Auth(ctx context.Context, provider *auth.IdentityProvider, code, state string) (*oauth2.User, error) {
+ userIdentity, err := provider.VerifyIdentity(ctx, code, state)
+ if err != nil {
+ return nil, err
+ }
+ return userIdentity, nil
+}
+
+func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, error) {
+ ctx := context.Background()
+ if oauth2.IsIdpEnabled() {
+ // initialize new oauth2 client
+ oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
+ if err != nil {
+ return nil, err
+ }
+ // initialize new identity provider
+ identityProvider := &auth.IdentityProvider{Client: oauth2Client}
+ // Validate user against IDP
+ identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State)
+ if err != nil {
+ return nil, err
+ }
+ mAdmin, err := newSuperMAdminClient()
+ if err != nil {
+ log.Println("error creating Madmin Client:", err)
+ return nil, err
+ }
+ adminClient := adminClient{client: mAdmin}
+ accessKey := identity.Email
+ secretKey := utils.RandomCharString(32)
+ // Create user in MinIO
+ if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil {
+ log.Println("error adding user:", err)
+ return nil, err
+ }
+ // rollback user if there's an error after this point
+ defer func() {
+ if err != nil {
+ if errRemove := removeUser(ctx, adminClient, accessKey); errRemove != nil {
+ log.Println("error removing user:", errRemove)
+ }
+ }
+ }()
+ // assign the "mcsAdmin" policy to this user
+ if err := setPolicy(ctx, adminClient, oauth2.GetIDPPolicyForUser(), accessKey, models.PolicyEntityUser); err != nil {
+ log.Println("error setting policy:", err)
+ return nil, err
+ }
+ // User was created correctly, create a new session/JWT
+ creds, err := newMcsCredentials(accessKey, secretKey, "")
+ if err != nil {
+ log.Println("error login:", err)
+ return nil, err
+ }
+ credentials := mcsCredentials{minioCredentials: creds}
+ jwt, err := login(credentials)
+ if err != nil {
+ log.Println("error login:", err)
+ return nil, err
+ }
+ // serialize output
+ loginResponse := &models.LoginResponse{
+ SessionID: *jwt,
+ }
+ return loginResponse, nil
}
- return loginDetails
+ return nil, errors.New("an error occurred, please try again")
}
diff --git a/restapi/user_login_test.go b/restapi/user_login_test.go
index 10cc735d1d..e1884ff42c 100644
--- a/restapi/user_login_test.go
+++ b/restapi/user_login_test.go
@@ -17,9 +17,12 @@
package restapi
import (
+ "context"
"errors"
"testing"
+ "github.com/minio/mcs/pkg/auth"
+ "github.com/minio/mcs/pkg/auth/idp/oauth2"
"github.com/minio/minio-go/v6/pkg/credentials"
"github.com/stretchr/testify/assert"
)
@@ -58,3 +61,43 @@ func TestLogin(t *testing.T) {
_, err = login(mcsCredentials)
funcAssert.NotNil(err, "not error returned creating a session")
}
+
+type IdentityProviderClientMock struct{}
+
+var idpVerifyIdentityMock func(ctx context.Context, code, state string) (*oauth2.User, error)
+var idpGenerateLoginURLMock func() string
+
+func (ac IdentityProviderClientMock) VerifyIdentity(ctx context.Context, code, state string) (*oauth2.User, error) {
+ return idpVerifyIdentityMock(ctx, code, state)
+}
+
+func (ac IdentityProviderClientMock) GenerateLoginURL() string {
+ return idpGenerateLoginURLMock()
+}
+
+// TestLoginOauth2Auth is the main function that test the Oauth2 Authentication
+func TestLoginOauth2Auth(t *testing.T) {
+ ctx := context.Background()
+ funcAssert := assert.New(t)
+ // mock data
+ mockCode := "EAEAEAE"
+ mockState := "HUEHUEHUE"
+ idpClientMock := IdentityProviderClientMock{}
+ identityProvider := &auth.IdentityProvider{Client: idpClientMock}
+ // Test-1 : loginOauth2Auth() correctly authenticates the user
+ idpVerifyIdentityMock = func(ctx context.Context, code, state string) (*oauth2.User, error) {
+ return &oauth2.User{}, nil
+ }
+ function := "loginOauth2Auth()"
+ _, err := loginOauth2Auth(ctx, identityProvider, mockCode, mockState)
+ if err != nil {
+ t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
+ }
+ // Test-2 : loginOauth2Auth() returns an error
+ idpVerifyIdentityMock = func(ctx context.Context, code, state string) (*oauth2.User, error) {
+ return nil, errors.New("error")
+ }
+ if _, err := loginOauth2Auth(ctx, identityProvider, mockCode, mockState); funcAssert.Error(err) {
+ funcAssert.Equal("error", err.Error())
+ }
+}
diff --git a/swagger.yml b/swagger.yml
index de24e3a2ba..4d28a79249 100644
--- a/swagger.yml
+++ b/swagger.yml
@@ -61,6 +61,29 @@ paths:
tags:
- UserAPI
+ /login/oauth2/auth:
+ post:
+ summary: Identity Provider oauth2 callback endpoint.
+ operationId: LoginOauth2Auth
+ parameters:
+ - name: body
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/loginOauth2AuthRequest'
+ responses:
+ 201:
+ description: A successful login.
+ schema:
+ $ref: '#/definitions/loginResponse'
+ default:
+ description: Generic error response.
+ schema:
+ $ref: "#/definitions/error"
+ security: []
+ tags:
+ - UserAPI
+
/logout:
post:
summary: Logout from mcs.
@@ -761,7 +784,6 @@ paths:
$ref: "#/definitions/error"
tags:
- AdminAPI
-
/profiling/start:
post:
summary: Start recording profile data
@@ -1179,6 +1201,16 @@ definitions:
enum: [form,redirect]
redirect:
type: string
+ loginOauth2AuthRequest:
+ type: object
+ required:
+ - state
+ - code
+ properties:
+ state:
+ type: string
+ code:
+ type: string
loginRequest:
type: object
required: