From 44d8e9b9757c2ef101295ad7fd50a7b4e0b92837 Mon Sep 17 00:00:00 2001 From: Lenin Alevski Date: Fri, 1 May 2020 08:38:52 -0700 Subject: [PATCH] idp integration for mcs (#75) This PR adds support for oidc in mcs, to enable idp authentication you need to pass the following environment variables and restart mcs. ``` MCS_IDP_URL="" MCS_IDP_CLIENT_ID="" MCS_IDP_SECRET="" MCS_IDP_CALLBACK="" ``` --- Makefile | 2 +- go.mod | 3 + go.sum | 4 + models/login_oauth2_auth_request.go | 98 ++++++++ pkg/auth/idp.go | 49 ++++ pkg/auth/idp/oauth2/config.go | 71 ++++++ pkg/auth/idp/oauth2/const.go | 29 +++ pkg/auth/idp/oauth2/provider.go | 229 ++++++++++++++++++ pkg/auth/idp/oauth2/provider_test.go | 98 ++++++++ pkg/auth/jwt/config.go | 39 +-- pkg/auth/utils/utils.go | 60 +++++ pkg/auth/utils/utils_test.go | 46 ++++ portal-ui/package.json | 2 +- portal-ui/src/Routes.tsx | 4 +- .../src/screens/LoginPage/LoginCallback.tsx | 43 ++++ .../src/screens/{ => LoginPage}/LoginPage.tsx | 141 ++++++++--- portal-ui/src/screens/LoginPage/types.ts | 20 ++ restapi/client.go | 3 - restapi/consts.go | 2 + restapi/embedded_spec.go | 98 ++++++++ restapi/operations/mcs_api.go | 12 + .../operations/user_api/login_oauth2_auth.go | 75 ++++++ .../user_api/login_oauth2_auth_parameters.go | 94 +++++++ .../user_api/login_oauth2_auth_responses.go | 133 ++++++++++ .../user_api/login_oauth2_auth_urlbuilder.go | 104 ++++++++ .../user_api/login_oauth2_callback.go | 75 ++++++ .../login_oauth2_callback_parameters.go | 62 +++++ .../login_oauth2_callback_responses.go | 113 +++++++++ .../login_oauth2_callback_urlbuilder.go | 104 ++++++++ .../operations/user_api/oauth2_callback.go | 75 ++++++ .../user_api/oauth2_callback_parameters.go | 62 +++++ .../user_api/oauth2_callback_responses.go | 113 +++++++++ .../user_api/oauth2_callback_urlbuilder.go | 104 ++++++++ restapi/user_login.go | 110 ++++++++- restapi/user_login_test.go | 43 ++++ swagger.yml | 34 ++- 36 files changed, 2275 insertions(+), 79 deletions(-) create mode 100644 models/login_oauth2_auth_request.go create mode 100644 pkg/auth/idp.go create mode 100644 pkg/auth/idp/oauth2/config.go create mode 100644 pkg/auth/idp/oauth2/const.go create mode 100644 pkg/auth/idp/oauth2/provider.go create mode 100644 pkg/auth/idp/oauth2/provider_test.go create mode 100644 pkg/auth/utils/utils.go create mode 100644 pkg/auth/utils/utils_test.go create mode 100644 portal-ui/src/screens/LoginPage/LoginCallback.tsx rename portal-ui/src/screens/{ => LoginPage}/LoginPage.tsx (68%) create mode 100644 portal-ui/src/screens/LoginPage/types.ts create mode 100644 restapi/operations/user_api/login_oauth2_auth.go create mode 100644 restapi/operations/user_api/login_oauth2_auth_parameters.go create mode 100644 restapi/operations/user_api/login_oauth2_auth_responses.go create mode 100644 restapi/operations/user_api/login_oauth2_auth_urlbuilder.go create mode 100644 restapi/operations/user_api/login_oauth2_callback.go create mode 100644 restapi/operations/user_api/login_oauth2_callback_parameters.go create mode 100644 restapi/operations/user_api/login_oauth2_callback_responses.go create mode 100644 restapi/operations/user_api/login_oauth2_callback_urlbuilder.go create mode 100644 restapi/operations/user_api/oauth2_callback.go create mode 100644 restapi/operations/user_api/oauth2_callback_parameters.go create mode 100644 restapi/operations/user_api/oauth2_callback_responses.go create mode 100644 restapi/operations/user_api/oauth2_callback_urlbuilder.go 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: