support oidc identity provider
Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
@@ -37,7 +37,7 @@ type aliyunIDaaS struct {
|
||||
ClientID string `json:"clientID" yaml:"clientID"`
|
||||
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string `json:"-" yaml:"clientSecret"`
|
||||
ClientSecret string `json:"clientSecret" yaml:"clientSecret"`
|
||||
|
||||
// Endpoint contains the resource server's token endpoint
|
||||
// URLs. These are constants specific to each server and are
|
||||
@@ -51,6 +51,8 @@ type aliyunIDaaS struct {
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string `json:"scopes" yaml:"scopes"`
|
||||
|
||||
Config *oauth2.Config `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// endpoint represents an OAuth 2.0 provider's authorization and token
|
||||
@@ -58,7 +60,7 @@ type aliyunIDaaS struct {
|
||||
type endpoint struct {
|
||||
AuthURL string `json:"authURL" yaml:"authURL"`
|
||||
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
|
||||
UserInfoURL string `json:"user_info_url" yaml:"userInfoUrl"`
|
||||
UserInfoURL string `json:"userInfoURL" yaml:"userInfoURL"`
|
||||
}
|
||||
|
||||
type idaasIdentity struct {
|
||||
@@ -81,15 +83,26 @@ type userInfoResp struct {
|
||||
type idaasProviderFactory struct {
|
||||
}
|
||||
|
||||
func (g *idaasProviderFactory) Type() string {
|
||||
return "AliyunIDaasProvider"
|
||||
func (f *idaasProviderFactory) Type() string {
|
||||
return "AliyunIDaaSProvider"
|
||||
}
|
||||
|
||||
func (g *idaasProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
func (f *idaasProviderFactory) Create(options oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
var idaas aliyunIDaaS
|
||||
if err := mapstructure.Decode(options, &idaas); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idaas.Config = &oauth2.Config{
|
||||
ClientID: idaas.ClientID,
|
||||
ClientSecret: idaas.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: idaas.Endpoint.AuthURL,
|
||||
TokenURL: idaas.Endpoint.TokenURL,
|
||||
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||
},
|
||||
RedirectURL: idaas.RedirectURL,
|
||||
Scopes: idaas.Scopes,
|
||||
}
|
||||
return &idaas, nil
|
||||
}
|
||||
|
||||
@@ -105,28 +118,13 @@ func (a idaasIdentity) GetEmail() string {
|
||||
return a.Email
|
||||
}
|
||||
|
||||
func (a idaasIdentity) GetDisplayName() string {
|
||||
return a.Nickname
|
||||
}
|
||||
|
||||
func (a *aliyunIDaaS) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
config := oauth2.Config{
|
||||
ClientID: a.ClientID,
|
||||
ClientSecret: a.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: a.Endpoint.AuthURL,
|
||||
TokenURL: a.Endpoint.TokenURL,
|
||||
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||
},
|
||||
RedirectURL: a.RedirectURL,
|
||||
Scopes: a.Scopes,
|
||||
}
|
||||
token, err := config.Exchange(context.Background(), code)
|
||||
token, err := a.Config.Exchange(context.TODO(), code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(a.Endpoint.UserInfoURL)
|
||||
resp, err := oauth2.NewClient(context.TODO(), oauth2.StaticTokenSource(token)).Get(a.Endpoint.UserInfoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package aliyunidaas
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/yaml.v3"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_idaasProviderFactory_Create(t *testing.T) {
|
||||
type args struct {
|
||||
options oauth.DynamicOptions
|
||||
}
|
||||
|
||||
mustUnmarshalYAML := func(data string) oauth.DynamicOptions {
|
||||
var dynamicOptions oauth.DynamicOptions
|
||||
_ = yaml.Unmarshal([]byte(data), &dynamicOptions)
|
||||
return dynamicOptions
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want identityprovider.OAuthProvider
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should create successfully",
|
||||
args: args{options: mustUnmarshalYAML(`
|
||||
clientID: xxxx
|
||||
clientSecret: xxxx
|
||||
endpoint:
|
||||
userInfoUrl: "https://xxxxx.login.aliyunidaas.com/api/bff/v1.2/oauth2/userinfo"
|
||||
authURL: "https://xxxx.login.aliyunidaas.com/oauth/authorize"
|
||||
tokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token"
|
||||
redirectURL: "http://ks-console/oauth/redirect"
|
||||
scopes:
|
||||
- read
|
||||
`)},
|
||||
want: &aliyunIDaaS{
|
||||
ClientID: "xxxx",
|
||||
ClientSecret: "xxxx",
|
||||
Endpoint: endpoint{
|
||||
AuthURL: "https://xxxx.login.aliyunidaas.com/oauth/authorize",
|
||||
TokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token",
|
||||
UserInfoURL: "https://xxxxx.login.aliyunidaas.com/api/bff/v1.2/oauth2/userinfo",
|
||||
},
|
||||
RedirectURL: "http://ks-console/oauth/redirect",
|
||||
Scopes: []string{"read"},
|
||||
Config: &oauth2.Config{
|
||||
ClientID: "xxxx",
|
||||
ClientSecret: "xxxx",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://xxxx.login.aliyunidaas.com/oauth/authorize",
|
||||
TokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token",
|
||||
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||
},
|
||||
RedirectURL: "http://ks-console/oauth/redirect",
|
||||
Scopes: []string{"read"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &idaasProviderFactory{}
|
||||
got, err := f.Create(tt.args.options)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Create() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
@@ -20,10 +22,6 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
var (
|
||||
builtinGenericProviders = make(map[string]GenericProviderFactory)
|
||||
)
|
||||
|
||||
type GenericProvider interface {
|
||||
// Authenticate from remote server
|
||||
Authenticate(username string, password string) (Identity, error)
|
||||
@@ -33,16 +31,5 @@ type GenericProviderFactory interface {
|
||||
// Type unique type of the provider
|
||||
Type() string
|
||||
// Apply the dynamic options from kubesphere-config
|
||||
Create(options *oauth.DynamicOptions) (GenericProvider, error)
|
||||
}
|
||||
|
||||
func CreateGenericProvider(providerType string, options *oauth.DynamicOptions) (GenericProvider, error) {
|
||||
if factory, ok := builtinGenericProviders[providerType]; ok {
|
||||
return factory.Create(options)
|
||||
}
|
||||
return nil, identityProviderNotFound
|
||||
}
|
||||
|
||||
func RegisterGenericProvider(factory GenericProviderFactory) {
|
||||
builtinGenericProviders[factory.Type()] = factory
|
||||
Create(options oauth.DynamicOptions) (GenericProvider, error)
|
||||
}
|
||||
|
||||
@@ -18,21 +18,25 @@ package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"golang.org/x/oauth2"
|
||||
"io/ioutil"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UserInfoURL = "https://api.github.com/user"
|
||||
userInfoURL = "https://api.github.com/user"
|
||||
authURL = "https://github.com/login/oauth/authorize"
|
||||
tokenURL = "https://github.com/login/oauth/access_token"
|
||||
)
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&githubProviderFactory{})
|
||||
identityprovider.RegisterOAuthProvider(&ldapProviderFactory{})
|
||||
}
|
||||
|
||||
type github struct {
|
||||
@@ -52,15 +56,21 @@ type github struct {
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
RedirectURL string `json:"redirectURL" yaml:"redirectURL"`
|
||||
|
||||
// Used to turn off TLS certificate checks
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"`
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string `json:"scopes" yaml:"scopes"`
|
||||
|
||||
Config *oauth2.Config `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// endpoint represents an OAuth 2.0 provider's authorization and token
|
||||
// endpoint URLs.
|
||||
type endpoint struct {
|
||||
AuthURL string `json:"authURL" yaml:"authURL"`
|
||||
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
|
||||
AuthURL string `json:"authURL" yaml:"authURL"`
|
||||
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
|
||||
UserInfoURL string `json:"userInfoURL" yaml:"userInfoURL"`
|
||||
}
|
||||
|
||||
type githubIdentity struct {
|
||||
@@ -102,18 +112,44 @@ type githubIdentity struct {
|
||||
Collaborators int `json:"collaborators"`
|
||||
}
|
||||
|
||||
type githubProviderFactory struct {
|
||||
type ldapProviderFactory struct {
|
||||
}
|
||||
|
||||
func (g *githubProviderFactory) Type() string {
|
||||
func (g *ldapProviderFactory) Type() string {
|
||||
return "GitHubIdentityProvider"
|
||||
}
|
||||
|
||||
func (g *githubProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
func (g *ldapProviderFactory) Create(options oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
var github github
|
||||
if err := mapstructure.Decode(options, &github); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if github.Endpoint.AuthURL == "" {
|
||||
github.Endpoint.AuthURL = authURL
|
||||
}
|
||||
if github.Endpoint.TokenURL == "" {
|
||||
github.Endpoint.TokenURL = tokenURL
|
||||
}
|
||||
if github.Endpoint.UserInfoURL == "" {
|
||||
github.Endpoint.UserInfoURL = userInfoURL
|
||||
}
|
||||
// fixed options
|
||||
options["endpoint"] = oauth.DynamicOptions{
|
||||
"authURL": github.Endpoint.AuthURL,
|
||||
"tokenURL": github.Endpoint.TokenURL,
|
||||
"userInfoURL": github.Endpoint.UserInfoURL,
|
||||
}
|
||||
github.Config = &oauth2.Config{
|
||||
ClientID: github.ClientID,
|
||||
ClientSecret: github.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: github.Endpoint.AuthURL,
|
||||
TokenURL: github.Endpoint.TokenURL,
|
||||
},
|
||||
RedirectURL: github.RedirectURL,
|
||||
Scopes: github.Scopes,
|
||||
}
|
||||
return &github, nil
|
||||
}
|
||||
|
||||
@@ -129,29 +165,23 @@ func (g githubIdentity) GetEmail() string {
|
||||
return g.Email
|
||||
}
|
||||
|
||||
func (g githubIdentity) GetDisplayName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (g *github) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
config := oauth2.Config{
|
||||
ClientID: g.ClientID,
|
||||
ClientSecret: g.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: g.Endpoint.AuthURL,
|
||||
TokenURL: g.Endpoint.TokenURL,
|
||||
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||
},
|
||||
RedirectURL: g.RedirectURL,
|
||||
Scopes: g.Scopes,
|
||||
ctx := context.TODO()
|
||||
if g.InsecureSkipVerify {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
|
||||
}
|
||||
|
||||
token, err := config.Exchange(context.Background(), code)
|
||||
token, err := g.Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(UserInfoURL)
|
||||
resp, err := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)).Get(g.Endpoint.UserInfoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
package github
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/yaml.v3"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var githubServer *httptest.Server
|
||||
|
||||
func TestGithub(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "GitHub Identity Provider Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func(done Done) {
|
||||
githubServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var data map[string]interface{}
|
||||
switch r.RequestURI {
|
||||
case "/login/oauth/access_token":
|
||||
data = map[string]interface{}{
|
||||
"access_token": "e72e16c7e42f292c6912e7710c838347ae178b4a",
|
||||
"scope": "user,repo,gist",
|
||||
"token_type": "bearer",
|
||||
}
|
||||
case "/user":
|
||||
data = map[string]interface{}{
|
||||
"login": "test",
|
||||
"email": "test@kubesphere.io",
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("not implemented"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}))
|
||||
close(done)
|
||||
}, 60)
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
gexec.KillAndWait(5 * time.Second)
|
||||
githubServer.Close()
|
||||
})
|
||||
|
||||
var _ = Describe("GitHub", func() {
|
||||
Context("GitHub", func() {
|
||||
var (
|
||||
provider identityprovider.OAuthProvider
|
||||
err error
|
||||
)
|
||||
It("should configure successfully", func() {
|
||||
configYAML := `
|
||||
clientID: de6ff8bed0304e487b6e
|
||||
clientSecret: 2b70536f79ec8d2939863509d05e2a71c268b9af
|
||||
redirectURL: "http://ks-console/oauth/redirect"
|
||||
scopes:
|
||||
- user
|
||||
`
|
||||
config := mustUnmarshalYAML(configYAML)
|
||||
factory := ldapProviderFactory{}
|
||||
provider, err = factory.Create(config)
|
||||
Expect(err).Should(BeNil())
|
||||
expected := &github{
|
||||
ClientID: "de6ff8bed0304e487b6e",
|
||||
ClientSecret: "2b70536f79ec8d2939863509d05e2a71c268b9af",
|
||||
Endpoint: endpoint{
|
||||
AuthURL: authURL,
|
||||
TokenURL: tokenURL,
|
||||
UserInfoURL: userInfoURL,
|
||||
},
|
||||
RedirectURL: "http://ks-console/oauth/redirect",
|
||||
Scopes: []string{"user"},
|
||||
Config: &oauth2.Config{
|
||||
ClientID: "de6ff8bed0304e487b6e",
|
||||
ClientSecret: "2b70536f79ec8d2939863509d05e2a71c268b9af",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authURL,
|
||||
TokenURL: tokenURL,
|
||||
},
|
||||
RedirectURL: "http://ks-console/oauth/redirect",
|
||||
Scopes: []string{"user"},
|
||||
},
|
||||
}
|
||||
Expect(provider).Should(Equal(expected))
|
||||
})
|
||||
It("should configure successfully", func() {
|
||||
config := oauth.DynamicOptions{
|
||||
"clientID": "de6ff8bed0304e487b6e",
|
||||
"clientSecret": "2b70536f79ec8d2939863509d05e2a71c268b9af",
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"insecureSkipVerify": true,
|
||||
"endpoint": oauth.DynamicOptions{
|
||||
"authURL": fmt.Sprintf("%s/login/oauth/authorize", githubServer.URL),
|
||||
"tokenURL": fmt.Sprintf("%s/login/oauth/access_token", githubServer.URL),
|
||||
"userInfoURL": fmt.Sprintf("%s/user", githubServer.URL),
|
||||
},
|
||||
}
|
||||
factory := ldapProviderFactory{}
|
||||
provider, err = factory.Create(config)
|
||||
Expect(err).Should(BeNil())
|
||||
expected := oauth.DynamicOptions{
|
||||
"clientID": "de6ff8bed0304e487b6e",
|
||||
"clientSecret": "2b70536f79ec8d2939863509d05e2a71c268b9af",
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"insecureSkipVerify": true,
|
||||
"endpoint": oauth.DynamicOptions{
|
||||
"authURL": fmt.Sprintf("%s/login/oauth/authorize", githubServer.URL),
|
||||
"tokenURL": fmt.Sprintf("%s/login/oauth/access_token", githubServer.URL),
|
||||
"userInfoURL": fmt.Sprintf("%s/user", githubServer.URL),
|
||||
},
|
||||
}
|
||||
Expect(config).Should(Equal(expected))
|
||||
})
|
||||
It("should login successfully", func() {
|
||||
identity, err := provider.IdentityExchange("3389")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(identity.GetUserID()).Should(Equal("test"))
|
||||
Expect(identity.GetUsername()).Should(Equal("test"))
|
||||
Expect(identity.GetEmail()).Should(Equal("test@kubesphere.io"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func mustUnmarshalYAML(data string) oauth.DynamicOptions {
|
||||
var dynamicOptions oauth.DynamicOptions
|
||||
_ = yaml.Unmarshal([]byte(data), &dynamicOptions)
|
||||
return dynamicOptions
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
type Identity interface {
|
||||
// required
|
||||
GetUserID() string
|
||||
// optional
|
||||
GetUsername() string
|
||||
// optional
|
||||
GetDisplayName() string
|
||||
// optional
|
||||
GetEmail() string
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"k8s.io/klog"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
var (
|
||||
oauthProviderFactories = make(map[string]OAuthProviderFactory)
|
||||
genericProviderFactories = make(map[string]GenericProviderFactory)
|
||||
identityProviderNotFound = errors.New("identity provider not found")
|
||||
oauthProviders = make(map[string]OAuthProvider)
|
||||
genericProviders = make(map[string]GenericProvider)
|
||||
)
|
||||
|
||||
// Identity represents the account mapped to kubesphere
|
||||
type Identity interface {
|
||||
// required
|
||||
// Identifier for the End-User at the Issuer.
|
||||
GetUserID() string
|
||||
// optional
|
||||
// The username which the End-User wishes to be referred to kubesphere.
|
||||
GetUsername() string
|
||||
// optional
|
||||
GetEmail() string
|
||||
}
|
||||
|
||||
// SetupWithOptions will verify the configuration and initialize the identityProviders
|
||||
func SetupWithOptions(options []oauth.IdentityProviderOptions) error {
|
||||
for _, o := range options {
|
||||
if oauthProviders[o.Name] != nil || genericProviders[o.Name] != nil {
|
||||
err := fmt.Errorf("duplicate identity provider found: %s, name must be unique", o.Name)
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
if genericProviderFactories[o.Type] == nil && oauthProviderFactories[o.Type] == nil {
|
||||
err := fmt.Errorf("identity provider %s with type %s is not supported", o.Name, o.Type)
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
if factory, ok := oauthProviderFactories[o.Type]; ok {
|
||||
if provider, err := factory.Create(o.Provider); err != nil {
|
||||
// don’t return errors, decoupling external dependencies
|
||||
klog.Error(fmt.Sprintf("failed to create identity provider %s: %s", o.Name, err))
|
||||
} else {
|
||||
oauthProviders[o.Name] = provider
|
||||
klog.V(4).Infof("create identity provider %s successfully", o.Name)
|
||||
}
|
||||
}
|
||||
if factory, ok := genericProviderFactories[o.Type]; ok {
|
||||
if provider, err := factory.Create(o.Provider); err != nil {
|
||||
klog.Error(fmt.Sprintf("failed to create identity provider %s: %s", o.Name, err))
|
||||
} else {
|
||||
genericProviders[o.Name] = provider
|
||||
klog.V(4).Infof("create identity provider %s successfully", o.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGenericProvider returns GenericProvider with given name
|
||||
func GetGenericProvider(providerName string) (GenericProvider, error) {
|
||||
if provider, ok := genericProviders[providerName]; ok {
|
||||
return provider, nil
|
||||
}
|
||||
return nil, identityProviderNotFound
|
||||
}
|
||||
|
||||
// GetGenericProvider returns OAuthProvider with given name
|
||||
func GetOAuthProvider(providerName string) (OAuthProvider, error) {
|
||||
if provider, ok := oauthProviders[providerName]; ok {
|
||||
return provider, nil
|
||||
}
|
||||
return nil, identityProviderNotFound
|
||||
}
|
||||
|
||||
// RegisterOAuthProvider register OAuthProviderFactory with the specified type
|
||||
func RegisterOAuthProvider(factory OAuthProviderFactory) {
|
||||
oauthProviderFactories[factory.Type()] = factory
|
||||
}
|
||||
|
||||
// RegisterOAuthProvider register GenericProviderFactory with the specified type
|
||||
func RegisterGenericProvider(factory GenericProviderFactory) {
|
||||
genericProviderFactories[factory.Type()] = factory
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type emptyOAuthProviderFactory struct {
|
||||
typeName string
|
||||
}
|
||||
|
||||
func (e emptyOAuthProviderFactory) Type() string {
|
||||
return e.typeName
|
||||
}
|
||||
|
||||
type emptyOAuthProvider struct {
|
||||
}
|
||||
|
||||
type emptyIdentity struct {
|
||||
}
|
||||
|
||||
func (e emptyIdentity) GetUserID() string {
|
||||
return "test"
|
||||
}
|
||||
|
||||
func (e emptyIdentity) GetUsername() string {
|
||||
return "test"
|
||||
}
|
||||
|
||||
func (e emptyIdentity) GetEmail() string {
|
||||
return "test@test.com"
|
||||
}
|
||||
|
||||
func (e emptyOAuthProvider) IdentityExchange(code string) (Identity, error) {
|
||||
return emptyIdentity{}, nil
|
||||
}
|
||||
|
||||
func (e emptyOAuthProviderFactory) Create(options oauth.DynamicOptions) (OAuthProvider, error) {
|
||||
return emptyOAuthProvider{}, nil
|
||||
}
|
||||
|
||||
type emptyGenericProviderFactory struct {
|
||||
typeName string
|
||||
}
|
||||
|
||||
func (e emptyGenericProviderFactory) Type() string {
|
||||
return e.typeName
|
||||
}
|
||||
|
||||
type emptyGenericProvider struct {
|
||||
}
|
||||
|
||||
func (e emptyGenericProvider) Authenticate(username string, password string) (Identity, error) {
|
||||
return emptyIdentity{}, nil
|
||||
}
|
||||
|
||||
func (e emptyGenericProviderFactory) Create(options oauth.DynamicOptions) (GenericProvider, error) {
|
||||
return emptyGenericProvider{}, nil
|
||||
}
|
||||
|
||||
func TestSetupWith(t *testing.T) {
|
||||
RegisterOAuthProvider(emptyOAuthProviderFactory{typeName: "GitHubIdentityProvider"})
|
||||
RegisterOAuthProvider(emptyOAuthProviderFactory{typeName: "OIDCIdentityProvider"})
|
||||
RegisterGenericProvider(emptyGenericProviderFactory{typeName: "LDAPIdentityProvider"})
|
||||
type args struct {
|
||||
options []oauth.IdentityProviderOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ldap",
|
||||
args: args{options: []oauth.IdentityProviderOptions{
|
||||
{
|
||||
Name: "ldap",
|
||||
MappingMethod: "auto",
|
||||
Type: "LDAPIdentityProvider",
|
||||
Provider: oauth.DynamicOptions{},
|
||||
},
|
||||
}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "conflict",
|
||||
args: args{options: []oauth.IdentityProviderOptions{
|
||||
{
|
||||
Name: "ldap",
|
||||
MappingMethod: "auto",
|
||||
Type: "LDAPIdentityProvider",
|
||||
Provider: oauth.DynamicOptions{},
|
||||
},
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not supported",
|
||||
args: args{options: []oauth.IdentityProviderOptions{
|
||||
{
|
||||
Name: "test",
|
||||
MappingMethod: "auto",
|
||||
Type: "NotSupported",
|
||||
Provider: oauth.DynamicOptions{},
|
||||
},
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := SetupWithOptions(tt.args.options); (err != nil) != tt.wantErr {
|
||||
t.Errorf("SetupWithOptions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -72,9 +72,8 @@ type ldapProvider struct {
|
||||
GroupMemberAttribute string `json:"groupMemberAttribute,omitempty" yaml:"groupMemberAttribute"`
|
||||
// The following three fields are direct mappings of attributes on the user entry.
|
||||
// login attribute used for comparing user entries.
|
||||
LoginAttribute string `json:"loginAttribute" yaml:"loginAttribute"`
|
||||
MailAttribute string `json:"mailAttribute" yaml:"mailAttribute"`
|
||||
DisplayNameAttribute string `json:"displayNameAttribute" yaml:"displayNameAttribute"`
|
||||
LoginAttribute string `json:"loginAttribute" yaml:"loginAttribute"`
|
||||
MailAttribute string `json:"mailAttribute" yaml:"mailAttribute"`
|
||||
}
|
||||
|
||||
type ldapProviderFactory struct {
|
||||
@@ -84,7 +83,7 @@ func (l *ldapProviderFactory) Type() string {
|
||||
return ldapIdentityProvider
|
||||
}
|
||||
|
||||
func (l *ldapProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.GenericProvider, error) {
|
||||
func (l *ldapProviderFactory) Create(options oauth.DynamicOptions) (identityprovider.GenericProvider, error) {
|
||||
var ldapProvider ldapProvider
|
||||
if err := mapstructure.Decode(options, &ldapProvider); err != nil {
|
||||
return nil, err
|
||||
@@ -96,9 +95,8 @@ func (l *ldapProviderFactory) Create(options *oauth.DynamicOptions) (identitypro
|
||||
}
|
||||
|
||||
type ldapIdentity struct {
|
||||
Username string
|
||||
Email string
|
||||
DisplayName string
|
||||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (l *ldapIdentity) GetUserID() string {
|
||||
@@ -113,10 +111,6 @@ func (l *ldapIdentity) GetEmail() string {
|
||||
return l.Email
|
||||
}
|
||||
|
||||
func (l *ldapIdentity) GetDisplayName() string {
|
||||
return l.DisplayName
|
||||
}
|
||||
|
||||
func (l ldapProvider) Authenticate(username string, password string) (identityprovider.Identity, error) {
|
||||
conn, err := l.newConn()
|
||||
if err != nil {
|
||||
@@ -141,7 +135,7 @@ func (l ldapProvider) Authenticate(username string, password string) (identitypr
|
||||
TimeLimit: 0,
|
||||
TypesOnly: false,
|
||||
Filter: filter,
|
||||
Attributes: []string{l.LoginAttribute, l.MailAttribute, l.DisplayNameAttribute},
|
||||
Attributes: []string{l.LoginAttribute, l.MailAttribute},
|
||||
})
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
@@ -161,11 +155,9 @@ func (l ldapProvider) Authenticate(username string, password string) (identitypr
|
||||
return nil, err
|
||||
}
|
||||
email := entry.GetAttributeValue(l.MailAttribute)
|
||||
displayName := entry.GetAttributeValue(l.DisplayNameAttribute)
|
||||
return &ldapIdentity{
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
Email: email,
|
||||
Username: username,
|
||||
Email: email,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ mailAttribute: mail
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := new(ldapProviderFactory).Create(&dynamicOptions)
|
||||
got, err := new(ldapProviderFactory).Create(dynamicOptions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -61,7 +61,6 @@ mailAttribute: mail
|
||||
GroupMemberAttribute: "",
|
||||
LoginAttribute: "uid",
|
||||
MailAttribute: "mail",
|
||||
DisplayNameAttribute: "",
|
||||
}
|
||||
if diff := cmp.Diff(got, expected); diff != "" {
|
||||
t.Errorf("%T differ (-got, +want): %s", expected, diff)
|
||||
@@ -81,7 +80,7 @@ func TestLdapProvider_Authenticate(t *testing.T) {
|
||||
if err = yaml.Unmarshal(options, &dynamicOptions); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldapProvider, err := new(ldapProviderFactory).Create(&dynamicOptions)
|
||||
ldapProvider, err := new(ldapProviderFactory).Create(dynamicOptions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -16,15 +16,9 @@ limitations under the License.
|
||||
package identityprovider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
var (
|
||||
builtinOAuthProviders = make(map[string]OAuthProviderFactory)
|
||||
identityProviderNotFound = errors.New("identity provider not found")
|
||||
)
|
||||
|
||||
type OAuthProvider interface {
|
||||
// IdentityExchange exchange identity from remote server
|
||||
IdentityExchange(code string) (Identity, error)
|
||||
@@ -34,16 +28,5 @@ type OAuthProviderFactory interface {
|
||||
// Type unique type of the provider
|
||||
Type() string
|
||||
// Apply the dynamic options from kubesphere-config
|
||||
Create(options *oauth.DynamicOptions) (OAuthProvider, error)
|
||||
}
|
||||
|
||||
func CreateOAuthProvider(providerType string, options *oauth.DynamicOptions) (OAuthProvider, error) {
|
||||
if provider, ok := builtinOAuthProviders[providerType]; ok {
|
||||
return provider.Create(options)
|
||||
}
|
||||
return nil, identityProviderNotFound
|
||||
}
|
||||
|
||||
func RegisterOAuthProvider(factory OAuthProviderFactory) {
|
||||
builtinOAuthProviders[factory.Type()] = factory
|
||||
Create(options oauth.DynamicOptions) (OAuthProvider, error)
|
||||
}
|
||||
|
||||
282
pkg/apiserver/authentication/identityprovider/oidc/oidc.go
Normal file
282
pkg/apiserver/authentication/identityprovider/oidc/oidc.go
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"golang.org/x/oauth2"
|
||||
"io/ioutil"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&oidcProviderFactory{})
|
||||
}
|
||||
|
||||
type oidcProvider struct {
|
||||
// Defines how Clients dynamically discover information about OpenID Providers
|
||||
// See also, https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
|
||||
Issuer string `json:"issuer,omitempty" yaml:"issuer,omitempty"`
|
||||
|
||||
// ClientID is the application's ID.
|
||||
ClientID string `json:"clientID" yaml:"clientID"`
|
||||
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string `json:"-" yaml:"clientSecret"`
|
||||
|
||||
// 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.
|
||||
Endpoint endpoint `json:"endpoint" yaml:"endpoint"`
|
||||
|
||||
// RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
RedirectURL string `json:"redirectURL" yaml:"redirectURL"`
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string `json:"scopes" yaml:"scopes"`
|
||||
|
||||
// GetUserInfo uses the userinfo endpoint to get additional claims for the token.
|
||||
// This is especially useful where upstreams return "thin" id tokens
|
||||
// See also, https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
GetUserInfo bool `json:"getUserInfo" yaml:"getUserInfo"`
|
||||
|
||||
// Used to turn off TLS certificate checks
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"`
|
||||
|
||||
// Configurable key which contains the email claims
|
||||
EmailKey string `json:"emailKey" yaml:"emailKey"`
|
||||
|
||||
// Configurable key which contains the preferred username claims
|
||||
PreferredUsernameKey string `json:"preferredUsernameKey" yaml:"preferredUsernameKey"`
|
||||
|
||||
Provider *oidc.Provider `json:"-" yaml:"-"`
|
||||
OAuth2Config *oauth2.Config `json:"-" yaml:"-"`
|
||||
Verifier *oidc.IDTokenVerifier `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// endpoint represents an OAuth 2.0 provider's authorization and token
|
||||
// endpoint URLs.
|
||||
type endpoint struct {
|
||||
// URL of the OP's OAuth 2.0 Authorization Endpoint [OpenID.Core](https://openid.net/specs/openid-connect-discovery-1_0.html#OpenID.Core).
|
||||
AuthURL string `json:"authURL" yaml:"authURL"`
|
||||
// URL of the OP's OAuth 2.0 Token Endpoint [OpenID.Core](https://openid.net/specs/openid-connect-discovery-1_0.html#OpenID.Core).
|
||||
// This is REQUIRED unless only the Implicit Flow is used.
|
||||
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
|
||||
// URL of the OP's UserInfo Endpoint [OpenID.Core](https://openid.net/specs/openid-connect-discovery-1_0.html#OpenID.Core).
|
||||
// This URL MUST use the https scheme and MAY contain port, path, and query parameter components.
|
||||
UserInfoURL string `json:"userInfoURL" yaml:"userInfoURL"`
|
||||
// URL of the OP's JSON Web Key Set [JWK](https://openid.net/specs/openid-connect-discovery-1_0.html#JWK) document.
|
||||
JWKSURL string `json:"jwksURL"`
|
||||
}
|
||||
|
||||
type oidcIdentity struct {
|
||||
// Subject - Identifier for the End-User at the Issuer.
|
||||
Sub string `json:"sub"`
|
||||
// Shorthand name by which the End-User wishes to be referred to at the RP,
|
||||
// such as janedoe or j.doe. This value MAY be any valid JSON string including special characters such as @, /, or whitespace.
|
||||
// The RP MUST NOT rely upon this value being unique
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
// End-User's preferred e-mail address.
|
||||
// Its value MUST conform to the RFC 5322 [RFC5322] addr-spec syntax.
|
||||
// The RP MUST NOT rely upon this value being unique.
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func (o oidcIdentity) GetUserID() string {
|
||||
return o.Sub
|
||||
}
|
||||
|
||||
func (o oidcIdentity) GetUsername() string {
|
||||
return o.PreferredUsername
|
||||
}
|
||||
|
||||
func (o oidcIdentity) GetEmail() string {
|
||||
return o.Email
|
||||
}
|
||||
|
||||
type oidcProviderFactory struct {
|
||||
}
|
||||
|
||||
func (f *oidcProviderFactory) Type() string {
|
||||
return "OIDCIdentityProvider"
|
||||
}
|
||||
|
||||
func (f *oidcProviderFactory) Create(options oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
var oidcProvider oidcProvider
|
||||
if err := mapstructure.Decode(options, &oidcProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// dynamically discover
|
||||
if oidcProvider.Issuer != "" {
|
||||
ctx := context.TODO()
|
||||
if oidcProvider.InsecureSkipVerify {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx = oidc.ClientContext(ctx, client)
|
||||
}
|
||||
provider, err := oidc.NewProvider(ctx, oidcProvider.Issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create oidc provider: %v", err)
|
||||
}
|
||||
var providerJSON map[string]interface{}
|
||||
if err = provider.Claims(&providerJSON); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode oidc provider claims: %v", err)
|
||||
}
|
||||
oidcProvider.Endpoint.AuthURL, _ = providerJSON["authorization_endpoint"].(string)
|
||||
oidcProvider.Endpoint.TokenURL, _ = providerJSON["token_endpoint"].(string)
|
||||
oidcProvider.Endpoint.UserInfoURL, _ = providerJSON["userinfo_endpoint"].(string)
|
||||
oidcProvider.Endpoint.JWKSURL, _ = providerJSON["jwks_uri"].(string)
|
||||
oidcProvider.Provider = provider
|
||||
oidcProvider.Verifier = provider.Verifier(&oidc.Config{
|
||||
// TODO: support HS256
|
||||
ClientID: oidcProvider.ClientID,
|
||||
})
|
||||
options["endpoint"] = oauth.DynamicOptions{
|
||||
"authURL": oidcProvider.Endpoint.AuthURL,
|
||||
"tokenURL": oidcProvider.Endpoint.TokenURL,
|
||||
"userInfoURL": oidcProvider.Endpoint.UserInfoURL,
|
||||
"jwksURL": oidcProvider.Endpoint.JWKSURL,
|
||||
}
|
||||
}
|
||||
scopes := []string{oidc.ScopeOpenID}
|
||||
if len(oidcProvider.Scopes) > 0 {
|
||||
scopes = append(scopes, oidcProvider.Scopes...)
|
||||
} else {
|
||||
scopes = append(scopes, "openid", "profile", "email")
|
||||
}
|
||||
oidcProvider.Scopes = scopes
|
||||
oidcProvider.OAuth2Config = &oauth2.Config{
|
||||
ClientID: oidcProvider.ClientID,
|
||||
ClientSecret: oidcProvider.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
TokenURL: oidcProvider.Endpoint.TokenURL,
|
||||
AuthURL: oidcProvider.Endpoint.AuthURL,
|
||||
},
|
||||
RedirectURL: oidcProvider.RedirectURL,
|
||||
Scopes: oidcProvider.Scopes,
|
||||
}
|
||||
|
||||
return &oidcProvider, nil
|
||||
}
|
||||
|
||||
func (o *oidcProvider) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
ctx := context.TODO()
|
||||
if o.InsecureSkipVerify {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
|
||||
}
|
||||
token, err := o.OAuth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: failed to get token: %v", err)
|
||||
}
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("no id_token in token response")
|
||||
}
|
||||
var claims jwt.MapClaims
|
||||
if o.Verifier != nil {
|
||||
idToken, err := o.Verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify id token: %v", err)
|
||||
}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode id token claims: %v", err)
|
||||
}
|
||||
} else {
|
||||
_, _, err := new(jwt.Parser).ParseUnverified(rawIDToken, &claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode id token claims: %v", err)
|
||||
}
|
||||
if err := claims.Valid(); err != nil {
|
||||
return nil, fmt.Errorf("failed to verify id token: %v", err)
|
||||
}
|
||||
}
|
||||
if o.GetUserInfo {
|
||||
if o.Provider != nil {
|
||||
userInfo, err := o.Provider.UserInfo(ctx, oauth2.StaticTokenSource(token))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch userinfo: %v", err)
|
||||
}
|
||||
if err := userInfo.Claims(&claims); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode userinfo claims: %v", err)
|
||||
}
|
||||
} else {
|
||||
resp, err := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)).Get(o.Endpoint.UserInfoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch userinfo: %v", err)
|
||||
}
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch userinfo: %v", err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
if err := json.Unmarshal(data, &claims); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode userinfo claims: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subject, ok := claims["sub"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("missing required claim \"sub\"")
|
||||
}
|
||||
|
||||
var email string
|
||||
emailKey := "email"
|
||||
if o.EmailKey != "" {
|
||||
emailKey = o.EmailKey
|
||||
}
|
||||
email, _ = claims[emailKey].(string)
|
||||
|
||||
var preferredUsername string
|
||||
preferredUsernameKey := "preferred_username"
|
||||
if o.PreferredUsernameKey != "" {
|
||||
preferredUsernameKey = o.PreferredUsernameKey
|
||||
}
|
||||
preferredUsername, _ = claims[preferredUsernameKey].(string)
|
||||
|
||||
if preferredUsername == "" {
|
||||
preferredUsername, _ = claims["name"].(string)
|
||||
}
|
||||
|
||||
return &oidcIdentity{
|
||||
Sub: subject,
|
||||
PreferredUsername: preferredUsername,
|
||||
Email: email,
|
||||
}, nil
|
||||
}
|
||||
214
pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go
Normal file
214
pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
oidcServer *httptest.Server
|
||||
)
|
||||
|
||||
func TestOIDC(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "OIDC Identity Provider Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func(done Done) {
|
||||
privateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
|
||||
Expect(err).Should(BeNil())
|
||||
jwk := jose.JSONWebKey{
|
||||
Key: privateKey,
|
||||
KeyID: "keyID",
|
||||
Algorithm: "RSA",
|
||||
}
|
||||
oidcServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var data interface{}
|
||||
switch r.RequestURI {
|
||||
case "/.well-known/openid-configuration":
|
||||
data = map[string]interface{}{
|
||||
"issuer": oidcServer.URL,
|
||||
"token_endpoint": fmt.Sprintf("%s/token", oidcServer.URL),
|
||||
"authorization_endpoint": fmt.Sprintf("%s/authorize", oidcServer.URL),
|
||||
"userinfo_endpoint": fmt.Sprintf("%s/userinfo", oidcServer.URL),
|
||||
"jwks_uri": fmt.Sprintf("%s/keys", oidcServer.URL),
|
||||
"response_types_supported": []string{
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"none",
|
||||
},
|
||||
"id_token_signing_alg_values_supported": []string{
|
||||
"RS256",
|
||||
},
|
||||
"scopes_supported": []string{
|
||||
"openid",
|
||||
"email",
|
||||
"profile",
|
||||
},
|
||||
"token_endpoint_auth_methods_supported": []string{
|
||||
"client_secret_post",
|
||||
"client_secret_basic",
|
||||
},
|
||||
"claims_supported": []string{
|
||||
"aud",
|
||||
"email",
|
||||
"email_verified",
|
||||
"exp",
|
||||
"iat",
|
||||
"iss",
|
||||
"name",
|
||||
"sub",
|
||||
},
|
||||
"code_challenge_methods_supported": []string{
|
||||
"plain",
|
||||
"S256",
|
||||
},
|
||||
"grant_types_supported": []string{
|
||||
"authorization_code",
|
||||
"refresh_token",
|
||||
},
|
||||
}
|
||||
case "/user":
|
||||
data = map[string]interface{}{
|
||||
"login": "test",
|
||||
"email": "test@kubesphere.io",
|
||||
}
|
||||
case "/keys":
|
||||
data = map[string]interface{}{
|
||||
"keys": []map[string]interface{}{{
|
||||
"alg": jwk.Algorithm,
|
||||
"kty": jwk.Algorithm,
|
||||
"kid": jwk.KeyID,
|
||||
"n": n(&privateKey.PublicKey),
|
||||
"e": e(&privateKey.PublicKey),
|
||||
}},
|
||||
}
|
||||
case "/token":
|
||||
claims := jwt.MapClaims{
|
||||
"iss": oidcServer.URL,
|
||||
"sub": "110169484474386276334",
|
||||
"aud": "kubesphere",
|
||||
"email": "test@kubesphere.io",
|
||||
"email_verified": "true",
|
||||
"name": "test",
|
||||
"iat": time.Now().Unix(),
|
||||
"exp": time.Now().Add(10 * time.Hour).Unix(),
|
||||
}
|
||||
idToken, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(privateKey)
|
||||
data = map[string]interface{}{
|
||||
"access_token": "e72e16c7e42f292c6912e7710c838347ae178b4a",
|
||||
"id_token": idToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
default:
|
||||
fmt.Println(r.URL)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("not implemented"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}))
|
||||
close(done)
|
||||
}, 60)
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
gexec.KillAndWait(5 * time.Second)
|
||||
oidcServer.Close()
|
||||
})
|
||||
|
||||
var _ = Describe("OIDC", func() {
|
||||
Context("OIDC", func() {
|
||||
var (
|
||||
provider identityprovider.OAuthProvider
|
||||
err error
|
||||
)
|
||||
It("should configure successfully", func() {
|
||||
config := oauth.DynamicOptions{
|
||||
"issuer": oidcServer.URL,
|
||||
"clientID": "kubesphere",
|
||||
"clientSecret": "c53e80ab92d48ab12f4e7f1f6976d1bdc996e0d7",
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"insecureSkipVerify": true,
|
||||
}
|
||||
factory := oidcProviderFactory{}
|
||||
provider, err = factory.Create(config)
|
||||
Expect(err).Should(BeNil())
|
||||
expected := oauth.DynamicOptions{
|
||||
"issuer": oidcServer.URL,
|
||||
"clientID": "kubesphere",
|
||||
"clientSecret": "c53e80ab92d48ab12f4e7f1f6976d1bdc996e0d7",
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"insecureSkipVerify": true,
|
||||
"endpoint": oauth.DynamicOptions{
|
||||
"authURL": fmt.Sprintf("%s/authorize", oidcServer.URL),
|
||||
"tokenURL": fmt.Sprintf("%s/token", oidcServer.URL),
|
||||
"userInfoURL": fmt.Sprintf("%s/userinfo", oidcServer.URL),
|
||||
"jwksURL": fmt.Sprintf("%s/keys", oidcServer.URL),
|
||||
},
|
||||
}
|
||||
Expect(config).Should(Equal(expected))
|
||||
})
|
||||
It("should login successfully", func() {
|
||||
identity, err := provider.IdentityExchange("3389")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(identity.GetUserID()).Should(Equal("110169484474386276334"))
|
||||
Expect(identity.GetUsername()).Should(Equal("test"))
|
||||
Expect(identity.GetEmail()).Should(Equal("test@kubesphere.io"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func n(pub *rsa.PublicKey) string {
|
||||
return encode(pub.N.Bytes())
|
||||
}
|
||||
|
||||
func e(pub *rsa.PublicKey) string {
|
||||
data := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(data, uint64(pub.E))
|
||||
return encode(bytes.TrimLeft(data, "\x00"))
|
||||
}
|
||||
|
||||
func encode(payload []byte) string {
|
||||
result := base64.URLEncoding.EncodeToString(payload)
|
||||
return strings.TrimRight(result, "=")
|
||||
}
|
||||
@@ -145,7 +145,7 @@ type IdentityProviderOptions struct {
|
||||
Type string `json:"type" yaml:"type"`
|
||||
|
||||
// The options of identify provider
|
||||
Provider *DynamicOptions `json:"provider" yaml:"provider"`
|
||||
Provider DynamicOptions `json:"provider" yaml:"provider"`
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
@@ -231,6 +231,7 @@ func (o *Options) OAuthClient(name string) (Client, error) {
|
||||
}
|
||||
return Client{}, ErrorClientNotFound
|
||||
}
|
||||
|
||||
func (o *Options) IdentityProviderOptions(name string) (*IdentityProviderOptions, error) {
|
||||
for _, found := range o.IdentityProviders {
|
||||
if found.Name == name {
|
||||
|
||||
@@ -49,7 +49,6 @@ func TestDefaultAuthOptions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientResolveRedirectURL(t *testing.T) {
|
||||
|
||||
options := NewOptions()
|
||||
defaultClient, err := options.OAuthClient("default")
|
||||
if err != nil {
|
||||
|
||||
@@ -19,9 +19,11 @@ package options
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/pflag"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/aliyunidaas"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/github"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/ldap"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/oidc"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"time"
|
||||
)
|
||||
@@ -67,6 +69,9 @@ func (options *AuthenticationOptions) Validate() []error {
|
||||
if len(options.JwtSecret) == 0 {
|
||||
errs = append(errs, fmt.Errorf("jwt secret is empty"))
|
||||
}
|
||||
if err := identityprovider.SetupWithOptions(options.OAuthOptions.IdentityProviders); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ func (o *AuthorizationOptions) AddFlags(fs *pflag.FlagSet, s *AuthorizationOptio
|
||||
fs.StringVar(&o.Mode, "authorization", s.Mode, "Authorization setting, allowed values: AlwaysDeny, AlwaysAllow, RBAC.")
|
||||
}
|
||||
|
||||
func (o AuthorizationOptions) Validate() []error {
|
||||
func (o *AuthorizationOptions) Validate() []error {
|
||||
errs := make([]error, 0)
|
||||
if !sliceutil.HasString([]string{AlwaysAllow, AlwaysDeny, RBAC}, o.Mode) {
|
||||
err := fmt.Errorf("authorization mode %s not support", o.Mode)
|
||||
|
||||
@@ -171,7 +171,7 @@ func (h *handler) Authorize(req *restful.Request, resp *restful.Response) {
|
||||
http.Redirect(resp, req.Request, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *handler) oauthCallBack(req *restful.Request, resp *restful.Response) {
|
||||
func (h *handler) oauthCallback(req *restful.Request, resp *restful.Response) {
|
||||
code := req.QueryParameter("code")
|
||||
provider := req.PathParameter("callback")
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
"otherwise, REQUIRED. The scope of the access token as described by [RFC6479] Section 3.3.").Required(false)).
|
||||
Param(ws.QueryParameter("state", "if the \"state\" parameter was present in the client authorization request."+
|
||||
"The exact value received from the client.").Required(true)).
|
||||
To(handler.oauthCallBack).
|
||||
To(handler.oauthCallback).
|
||||
Returns(http.StatusOK, api.StatusOK, oauth.Token{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ func (p *passwordAuthenticator) Authenticate(username, password string) (authuse
|
||||
if username == constants.AdminUserName {
|
||||
break
|
||||
}
|
||||
if genericProvider, _ := identityprovider.CreateGenericProvider(providerOptions.Type, providerOptions.Provider); genericProvider != nil {
|
||||
if genericProvider, _ := identityprovider.GetGenericProvider(providerOptions.Name); genericProvider != nil {
|
||||
authenticated, err := genericProvider.Authenticate(username, password)
|
||||
if err != nil {
|
||||
if errors.IsUnauthorized(err) {
|
||||
@@ -173,7 +173,6 @@ func preRegistrationUser(idp string, identity identityprovider.Identity) authuse
|
||||
iamv1alpha2.ExtraUID: {identity.GetUserID()},
|
||||
iamv1alpha2.ExtraUsername: {identity.GetUsername()},
|
||||
iamv1alpha2.ExtraEmail: {identity.GetEmail()},
|
||||
iamv1alpha2.ExtraDisplayName: {identity.GetDisplayName()},
|
||||
},
|
||||
Groups: []string{iamv1alpha2.PreRegistrationUserGroup},
|
||||
}
|
||||
@@ -186,7 +185,7 @@ func (o oauth2Authenticator) Authenticate(provider, code string) (authuser.Info,
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
oauthIdentityProvider, err := identityprovider.CreateOAuthProvider(providerOptions.Type, providerOptions.Provider)
|
||||
oauthIdentityProvider, err := identityprovider.GetOAuthProvider(providerOptions.Name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
|
||||
Reference in New Issue
Block a user