From 34dfc2048a55ba9228851d7db2cf416b0a3a360b Mon Sep 17 00:00:00 2001 From: hongming Date: Mon, 30 Mar 2020 17:06:19 +0800 Subject: [PATCH] add default oauth client Signed-off-by: hongming --- .../identityprovider/github/github.go | 198 ------------------ .../authentication/oauth/oauth_options.go | 47 ++++- .../oauth/oauth_options_test.go | 104 +++++++++ pkg/apiserver/config/config_test.go | 24 +-- 4 files changed, 150 insertions(+), 223 deletions(-) delete mode 100644 pkg/apiserver/authentication/identityprovider/github/github.go create mode 100644 pkg/apiserver/authentication/oauth/oauth_options_test.go diff --git a/pkg/apiserver/authentication/identityprovider/github/github.go b/pkg/apiserver/authentication/identityprovider/github/github.go deleted file mode 100644 index 580b73f1d..000000000 --- a/pkg/apiserver/authentication/identityprovider/github/github.go +++ /dev/null @@ -1,198 +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 github - -import ( - "context" - "encoding/json" - "golang.org/x/oauth2" - "gopkg.in/yaml.v2" - "io/ioutil" - "k8s.io/apiserver/pkg/authentication/user" - "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" - "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" - "time" -) - -const ( - UserInfoURL = "https://api.github.com/user" -) - -type Github struct { - // 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"` -} - -// 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"` -} - -type GithubIdentity struct { - Login string `json:"login"` - ID string `json:"id"` - NodeID string `json:"node_id"` - AvatarURL string `json:"avatar_url"` - GravatarID string `json:"gravatar_id"` - URL string `json:"url"` - HTMLURL string `json:"html_url"` - FollowersURL string `json:"followers_url"` - FollowingURL string `json:"following_url"` - GistsURL string `json:"gists_url"` - StarredURL string `json:"starred_url"` - SubscriptionsURL string `json:"subscriptions_url"` - OrganizationsURL string `json:"organizations_url"` - ReposURL string `json:"repos_url"` - EventsURL string `json:"events_url"` - ReceivedEventsURL string `json:"received_events_url"` - Type string `json:"type"` - SiteAdmin bool `json:"site_admin"` - Name string `json:"name"` - Company string `json:"company"` - Blog string `json:"blog"` - Location string `json:"location"` - Email string `json:"email"` - Hireable bool `json:"hireable"` - Bio string `json:"bio"` - PublicRepos int `json:"public_repos"` - PublicGists int `json:"public_gists"` - Followers int `json:"followers"` - Following int `json:"following"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - PrivateGists int `json:"private_gists"` - TotalPrivateRepos int `json:"total_private_repos"` - OwnedPrivateRepos int `json:"owned_private_repos"` - DiskUsage int `json:"disk_usage"` - Collaborators int `json:"collaborators"` -} - -func init() { - identityprovider.RegisterOAuthProviderCodec(&codec{t: "github"}) -} - -type codec struct { - t string -} - -func (c codec) Type() string { - return c.t -} - -func (codec) Encode(provider identityprovider.OAuthProvider) (*oauth.DynamicOptions, error) { - data, err := yaml.Marshal(provider) - if err != nil { - return nil, err - } - var options oauth.DynamicOptions - err = yaml.Unmarshal(data, &options) - if err != nil { - return nil, err - } - return &options, nil -} - -func (codec) Decode(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) { - data, err := yaml.Marshal(options) - if err != nil { - return nil, err - } - var provider Github - err = yaml.Unmarshal(data, &provider) - if err != nil { - return nil, err - } - return &provider, nil -} - -func (g GithubIdentity) GetName() string { - return g.Login -} - -func (g GithubIdentity) GetUID() string { - return g.ID -} - -func (g GithubIdentity) GetGroups() []string { - return nil -} - -func (g GithubIdentity) GetExtra() map[string][]string { - return nil -} - -func (g *Github) IdentityExchange(code string) (user.Info, 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, - } - token, err := config.Exchange(context.Background(), code) - - if err != nil { - return nil, err - } - - resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(UserInfoURL) - - if err != nil { - return nil, err - } - - data, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - - if err != nil { - return nil, err - } - - var githubIdentity GithubIdentity - - err = json.Unmarshal(data, &githubIdentity) - - if err != nil { - return nil, err - } - - return githubIdentity, nil -} diff --git a/pkg/apiserver/authentication/oauth/oauth_options.go b/pkg/apiserver/authentication/oauth/oauth_options.go index 554d7b966..bb964dc74 100644 --- a/pkg/apiserver/authentication/oauth/oauth_options.go +++ b/pkg/apiserver/authentication/oauth/oauth_options.go @@ -21,6 +21,7 @@ package oauth import ( "errors" "kubesphere.io/kubesphere/pkg/utils/sliceutil" + "net/url" "time" ) @@ -152,12 +153,33 @@ type Client struct { AccessTokenInactivityTimeout *time.Duration `json:"accessTokenInactivityTimeout,omitempty" yaml:"accessTokenInactivityTimeout,omitempty"` } +var ( + // Allow any redirect URI if the redirectURI is defined in request + AllowAllRedirectURI = "*" + DefaultTokenMaxAge = time.Second * 86400 + DefaultAccessTokenInactivityTimeout = time.Duration(0) + DefaultClients = []Client{{ + Name: "default", + RespondWithChallenges: true, + RedirectURIs: []string{AllowAllRedirectURI}, + GrantMethod: GrantHandlerAuto, + ScopeRestrictions: []string{"full"}, + AccessTokenMaxAge: &DefaultTokenMaxAge, + AccessTokenInactivityTimeout: &DefaultAccessTokenInactivityTimeout, + }} +) + func (o *Options) OAuthClient(name string) (Client, error) { for _, found := range o.Clients { if found.Name == name { return found, nil } } + for _, defaultClient := range DefaultClients { + if defaultClient.Name == name { + return defaultClient, nil + } + } return Client{}, ErrorClientNotFound } func (o *Options) IdentityProviderOptions(name string) (IdentityProviderOptions, error) { @@ -169,16 +191,37 @@ func (o *Options) IdentityProviderOptions(name string) (IdentityProviderOptions, return IdentityProviderOptions{}, ErrorClientNotFound } +func (c Client) anyRedirectAbleURI() []string { + uris := make([]string, 0) + for _, uri := range c.RedirectURIs { + _, err := url.Parse(uri) + if err == nil { + uris = append(uris, uri) + } + } + return uris +} + func (c Client) ResolveRedirectURL(expectURL string) (string, error) { + // RedirectURIs is empty if len(c.RedirectURIs) == 0 { return "", ErrorRedirectURLNotAllowed } + allowAllRedirectURI := sliceutil.HasString(c.RedirectURIs, AllowAllRedirectURI) + redirectAbleURIs := c.anyRedirectAbleURI() + if expectURL == "" { - return c.RedirectURIs[0], nil + // Need to specify at least one RedirectURI + if len(redirectAbleURIs) > 0 { + return redirectAbleURIs[0], nil + } else { + return "", ErrorRedirectURLNotAllowed + } } - if sliceutil.HasString(c.RedirectURIs, expectURL) { + if allowAllRedirectURI || sliceutil.HasString(redirectAbleURIs, expectURL) { return expectURL, nil } + return "", ErrorRedirectURLNotAllowed } diff --git a/pkg/apiserver/authentication/oauth/oauth_options_test.go b/pkg/apiserver/authentication/oauth/oauth_options_test.go new file mode 100644 index 000000000..c7af42075 --- /dev/null +++ b/pkg/apiserver/authentication/oauth/oauth_options_test.go @@ -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 oauth + +import ( + "github.com/google/go-cmp/cmp" + "testing" + "time" +) + +func TestDefaultAuthOptions(t *testing.T) { + oneDay := time.Second * 86400 + zero := time.Duration(0) + expect := Client{ + Name: "default", + RespondWithChallenges: true, + RedirectURIs: []string{AllowAllRedirectURI}, + GrantMethod: GrantHandlerAuto, + ScopeRestrictions: []string{"full"}, + AccessTokenMaxAge: &oneDay, + AccessTokenInactivityTimeout: &zero, + } + + options := NewOptions() + client, err := options.OAuthClient("default") + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expect, client); len(diff) != 0 { + t.Errorf("%T differ (-got, +expected), %s", expect, diff) + } +} + +func TestClientResolveRedirectURL(t *testing.T) { + + options := NewOptions() + defaultClient, err := options.OAuthClient("default") + if err != nil { + t.Fatal(err) + } + tests := []struct { + Name string + client Client + expectError error + expectURL string + }{ + { + Name: "default client test", + client: defaultClient, + expectError: nil, + expectURL: "https://localhost:8080/auth/cb", + }, + { + Name: "custom client test", + client: Client{ + Name: "default", + RespondWithChallenges: true, + RedirectURIs: []string{"https://foo.bar.com/oauth/cb"}, + GrantMethod: GrantHandlerAuto, + ScopeRestrictions: []string{"full"}, + }, + expectError: ErrorRedirectURLNotAllowed, + expectURL: "https://foo.bar.com/oauth/err", + }, + { + Name: "custom client test", + client: Client{ + Name: "default", + RespondWithChallenges: true, + RedirectURIs: []string{AllowAllRedirectURI, "https://foo.bar.com/oauth/cb"}, + GrantMethod: GrantHandlerAuto, + ScopeRestrictions: []string{"full"}, + }, + expectError: nil, + expectURL: "https://foo.bar.com/oauth/err2", + }, + } + + for _, test := range tests { + redirectURL, err := test.client.ResolveRedirectURL(test.expectURL) + if err != test.expectError { + t.Errorf("expected error: %s, got: %s", test.expectError, err) + } + if test.expectError == nil && test.expectURL != redirectURL { + t.Errorf("expected redirect url: %s, got: %s", test.expectURL, redirectURL) + } + } +} diff --git a/pkg/apiserver/config/config_test.go b/pkg/apiserver/config/config_test.go index 42ec60fc2..df31e2ae8 100644 --- a/pkg/apiserver/config/config_test.go +++ b/pkg/apiserver/config/config_test.go @@ -5,8 +5,6 @@ import ( "github.com/google/go-cmp/cmp" "gopkg.in/yaml.v2" "io/ioutil" - "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" - "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/github" "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/simple/client/alerting" @@ -29,19 +27,6 @@ import ( func newTestConfig() (*Config, error) { - githubOAuthProvider, err := identityprovider.ResolveOAuthOptions("github", &github.Github{ - ClientID: "de6ff7bed0304e487b6e", - ClientSecret: "xxxxxx-xxxxx-xxxxx", - Endpoint: github.Endpoint{ - AuthURL: "https://github.com/login/oauth/authorize", - TokenURL: "https://github.com/login/oauth/token", - }, - RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/callbak/github", - Scopes: []string{"user"}, - }) - if err != nil { - return nil, err - } var conf = &Config{ MySQLOptions: &mysql.Options{ Host: "10.68.96.5:3306", @@ -125,13 +110,7 @@ func newTestConfig() (*Config, error) { JwtSecret: "xxxxxx", MultipleLogin: false, OAuthOptions: &oauth.Options{ - IdentityProviders: []oauth.IdentityProviderOptions{{ - Name: "github", - MappingMethod: "auto", - LoginRedirect: true, - Type: "github", - Provider: githubOAuthProvider, - }}, + IdentityProviders: []oauth.IdentityProviderOptions{}, Clients: []oauth.Client{{ Name: "kubesphere-console-client", Secret: "xxxxxx-xxxxxx-xxxxxx", @@ -185,7 +164,6 @@ func TestGet(t *testing.T) { if err != nil { t.Fatal(err) } - if diff := cmp.Diff(conf, conf2); diff != "" { t.Fatal(diff) }