implement identity provider and built-in oauth server

Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-03-26 06:43:20 +08:00
parent 59002cd176
commit 9b9d4021ec
25 changed files with 637 additions and 1163 deletions

View File

@@ -23,7 +23,7 @@ func NewTokenAuthenticator(issuer token2.Issuer) authenticator.Token {
}
func (t *tokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
providedUser, _, err := t.jwtTokenIssuer.Verify(token)
providedUser, err := t.jwtTokenIssuer.Verify(token)
if err != nil {
return nil, false, err
}

View File

@@ -16,15 +16,10 @@
* /
*/
package oauth
package identityprovider
import (
"errors"
"golang.org/x/oauth2"
)
import "k8s.io/apiserver/pkg/authentication/user"
var ConfigNotFound = errors.New("config not found")
type Configuration interface {
Load(clientId string) (*oauth2.Config, error)
type OAuth2Interface interface {
IdentityExchange(code string) (user.Info, error)
}

View File

@@ -0,0 +1,157 @@
/*
*
* 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 (
"context"
"encoding/json"
"golang.org/x/oauth2"
"io/ioutil"
"k8s.io/apiserver/pkg/authentication/user"
"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 (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
}

View File

@@ -0,0 +1,203 @@
/*
*
* 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 (
"errors"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
)
type GrantHandlerType string
type MappingMethod string
type IdentityProviderType string
const (
// GrantHandlerAuto auto-approves client authorization grant requests
GrantHandlerAuto GrantHandlerType = "auto"
// GrantHandlerPrompt prompts the user to approve new client authorization grant requests
GrantHandlerPrompt GrantHandlerType = "prompt"
// GrantHandlerDeny auto-denies client authorization grant requests
GrantHandlerDeny GrantHandlerType = "deny"
// MappingMethodAuto The default value.The user will automatically create and mapping when login successful.
// Fails if a user with that user name is already mapped to another identity.
MappingMethodAuto MappingMethod = "auto"
// MappingMethodLookup Looks up an existing identity, user identity mapping, and user, but does not automatically
// provision users or identities. Using this method requires you to manually provision users.
MappingMethodLookup MappingMethod = "lookup"
// MappingMethodMixed A user entity can be mapped with multiple identifyProvider.
MappingMethodMixed MappingMethod = "mixed"
IdentityProviderTypeGithub = "github"
)
var (
ErrorClientNotFound = errors.New("the OAuth client was not found")
ErrorRedirectURLNotAllowed = errors.New("redirect URL is not allowed")
ErrorIdentityProviderNotFound = errors.New("the identity provider was not found")
)
type Options struct {
// LDAPPasswordIdentityProvider provider is used by default.
IdentityProviders []IdentityProvider `json:"identityProviders,omitempty" yaml:"identityProviders,omitempty"`
// Register additional OAuth clients.
Clients []Client `json:"clients,omitempty" yaml:"clients,omitempty"`
// AccessTokenMaxAgeSeconds control the lifetime of access tokens. The default lifetime is 24 hours.
// 0 means no expiration.
AccessTokenMaxAgeSeconds int `json:"accessTokenMaxAgeSeconds" yaml:"accessTokenMaxAgeSeconds"`
// Inactivity timeout for tokens
// The value represents the maximum amount of time that can occur between
// consecutive uses of the token. Tokens become invalid if they are not
// used within this temporal window. The user will need to acquire a new
// token to regain access once a token times out.
// This value needs to be set only if the default set in configuration is
// not appropriate for this client. Valid values are:
// - 0: Tokens for this client never time out
// - X: Tokens time out if there is no activity for X seconds
// The current minimum allowed value for X is 300 (5 minutes)
AccessTokenInactivityTimeoutSeconds int `json:"accessTokenInactivityTimeoutSeconds" yaml:"accessTokenInactivityTimeoutSeconds"`
}
type IdentityProvider struct {
// The provider name.
Name string `json:"name" yaml:"name"`
// Defines how new identities are mapped to users when they login. Allowed values are:
// - auto: The default value.The user will automatically create and mapping when login successful.
// Fails if a user with that user name is already mapped to another identity.
// - lookup: Looks up an existing identity, user identity mapping, and user, but does not automatically
// provision users or identities. Using this method requires you to manually provision users.
// - mixed: A user entity can be mapped with multiple identifyProvider.
MappingMethod MappingMethod `json:"mappingMethod" yaml:"mappingMethod"`
// When true, unauthenticated token requests from web clients (like the web console)
// are redirected to a login page (with WWW-Authenticate challenge header) backed by this provider.
LoginRedirect bool `json:"loginRedirect" yaml:"loginRedirect"`
// The type of identify provider
Type string `json:"type" yaml:"type"`
// Provider options
Github *Github `json:"github" yaml:"github" `
}
type Token struct {
// AccessToken is the token that authorizes and authenticates
// the requests.
AccessToken string `json:"access_token"`
// TokenType is the type of token.
// The Type method returns either this or "Bearer", the default.
TokenType string `json:"token_type,omitempty"`
// RefreshToken is a token that's used by the application
// (as opposed to the user) to refresh the access token
// if it expires.
RefreshToken string `json:"refresh_token,omitempty"`
// ExpiresIn is the optional expiration second of the access token.
ExpiresIn int `json:"expires_in,omitempty"`
}
type Client struct {
// The name of the OAuth client is used as the client_id parameter when making requests to <master>/oauth/authorize
// and <master>/oauth/token.
Name string
// Secret is the unique secret associated with a client
Secret string `json:"-" yaml:"secret,omitempty"`
// RespondWithChallenges indicates whether the client wants authentication needed responses made
// in the form of challenges instead of redirects
RespondWithChallenges bool `json:"respondWithChallenges,omitempty" yaml:"respondWithChallenges,omitempty"`
// RedirectURIs is the valid redirection URIs associated with a client
RedirectURIs []string `json:"redirectURIs,omitempty" yaml:"redirectURIs,omitempty"`
// GrantMethod determines how to handle grants for this client. If no method is provided, the
// cluster default grant handling method will be used. Valid grant handling methods are:
// - auto: always approves grant requests, useful for trusted clients
// - prompt: prompts the end user for approval of grant requests, useful for third-party clients
// - deny: always denies grant requests, useful for black-listed clients
GrantMethod GrantHandlerType `json:"grantMethod,omitempty" yaml:"grantMethod,omitempty"`
// ScopeRestrictions describes which scopes this client can request. Each requested scope
// is checked against each restriction. If any restriction matches, then the scope is allowed.
// If no restriction matches, then the scope is denied.
ScopeRestrictions []string `json:"scopeRestrictions,omitempty" yaml:"scopeRestrictions,omitempty"`
// AccessTokenMaxAgeSeconds overrides the default access token max age for tokens granted to this client.
AccessTokenMaxAgeSeconds *int `json:"accessTokenMaxAgeSeconds,omitempty" yaml:"accessTokenMaxAgeSeconds,omitempty"`
// AccessTokenInactivityTimeoutSeconds overrides the default token
// inactivity timeout for tokens granted to this client.
AccessTokenInactivityTimeoutSeconds *int `json:"accessTokenInactivityTimeoutSeconds,omitempty" yaml:"accessTokenInactivityTimeoutSeconds,omitempty"`
}
func (o *Options) GetOAuthClient(name string) (Client, error) {
for _, found := range o.Clients {
if found.Name == name {
return found, nil
}
}
return Client{}, ErrorClientNotFound
}
func (o *Options) GetIdentityProvider(name string) (IdentityProvider, error) {
for _, found := range o.IdentityProviders {
if found.Name == name {
return found, nil
}
}
return IdentityProvider{}, ErrorClientNotFound
}
func (c Client) ResolveRedirectURL(expectURL string) (string, error) {
if len(c.RedirectURIs) == 0 {
return "", ErrorRedirectURLNotAllowed
}
if expectURL == "" {
return c.RedirectURIs[0], nil
}
if sliceutil.HasString(c.RedirectURIs, expectURL) {
return expectURL, nil
}
return "", ErrorRedirectURLNotAllowed
}
func (i IdentityProvider) GetOAuth2IdentityProviderInstance() (identityprovider.OAuth2Interface, error) {
switch i.Type {
case IdentityProviderTypeGithub:
if i.Github != nil {
return i.Github, nil
}
}
return nil, ErrorIdentityProviderNotFound
}
func NewOptions() *Options {
return &Options{
IdentityProviders: make([]IdentityProvider, 0),
Clients: make([]Client, 0),
AccessTokenMaxAgeSeconds: 86400,
AccessTokenInactivityTimeoutSeconds: 0,
}
}

View File

@@ -1,37 +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 oauth
import "golang.org/x/oauth2"
type SimpleConfigManager struct {
}
func (s *SimpleConfigManager) Load(clientId string) (*oauth2.Config, error) {
if clientId == "kubesphere-console-client" {
return &oauth2.Config{
ClientID: "8b21fef43889a28f2bd6",
ClientSecret: "xb21fef43889a28f2bd6",
Endpoint: oauth2.Endpoint{AuthURL: "http://ks-apiserver.kubesphere-system.svc/oauth/authorize", TokenURL: "http://ks-apiserver.kubesphere.io/oauth/token"},
RedirectURL: "http://ks-console.kubesphere-system.svc/oauth/token/implicit",
Scopes: nil,
}, nil
}
return nil, ConfigNotFound
}

View File

@@ -0,0 +1,72 @@
/*
*
* 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 options
import (
"fmt"
"github.com/spf13/pflag"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
"time"
)
type AuthenticationOptions struct {
// authenticate rate limit will
AuthenticateRateLimiterMaxTries int `json:"authenticateRateLimiterMaxTries" yaml:"authenticateRateLimiterMaxTries"`
AuthenticateRateLimiterDuration time.Duration `json:"authenticationRateLimiterDuration" yaml:"authenticationRateLimiterDuration"`
// maximum retries when authenticate failed
MaxAuthenticateRetries int `json:"maxAuthenticateRetries" yaml:"maxAuthenticateRetries"`
// allow multiple users login at the same time
MultipleLogin bool `json:"multipleLogin" yaml:"multipleLogin"`
// secret to signed jwt token
JwtSecret string `json:"-" yaml:"jwtSecret"`
OAuthOptions *oauth.Options `json:"oauthOptions" yaml:"oauthOptions"`
}
func NewAuthenticateOptions() *AuthenticationOptions {
return &AuthenticationOptions{
AuthenticateRateLimiterMaxTries: 5,
AuthenticateRateLimiterDuration: time.Minute * 30,
MaxAuthenticateRetries: 0,
OAuthOptions: oauth.NewOptions(),
MultipleLogin: false,
JwtSecret: "",
}
}
func (options *AuthenticationOptions) Validate() []error {
var errs []error
if len(options.JwtSecret) == 0 {
errs = append(errs, fmt.Errorf("jwt secret is empty"))
}
return errs
}
func (options *AuthenticationOptions) AddFlags(fs *pflag.FlagSet, s *AuthenticationOptions) {
fs.IntVar(&options.AuthenticateRateLimiterMaxTries, "authenticate-rate-limiter-max-retries", s.AuthenticateRateLimiterMaxTries, "")
fs.DurationVar(&options.AuthenticateRateLimiterDuration, "authenticate-rate-limiter-duration", s.AuthenticateRateLimiterDuration, "")
fs.IntVar(&options.MaxAuthenticateRetries, "authenticate-max-retries", s.MaxAuthenticateRetries, "")
fs.BoolVar(&options.MultipleLogin, "multiple-login", s.MultipleLogin, "Allow multiple login with the same account, disable means only one user can login at the same time.")
fs.StringVar(&options.JwtSecret, "jwt-secret", s.JwtSecret, "Secret to sign jwt token, must not be empty.")
}

View File

@@ -21,10 +21,10 @@ package token
// Issuer issues token to user, tokens are required to perform mutating requests to resources
type Issuer interface {
// IssueTo issues a token a User, return error if issuing process failed
IssueTo(User) (string, *Claims, error)
IssueTo(user User, expiresInSecond int) (string, error)
// Verify verifies a token, and return a User if it's a valid token, otherwise return error
Verify(string) (User, *Claims, error)
Verify(string) (User, error)
// Revoke a token,
Revoke(token string) error

View File

@@ -21,8 +21,8 @@ package token
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"kubesphere.io/kubesphere/pkg/api/auth"
"kubesphere.io/kubesphere/pkg/api/iam"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"time"
@@ -44,35 +44,35 @@ type Claims struct {
type jwtTokenIssuer struct {
name string
options *auth.AuthenticationOptions
options *authoptions.AuthenticationOptions
cache cache.Interface
keyFunc jwt.Keyfunc
}
func (s *jwtTokenIssuer) Verify(tokenString string) (User, *Claims, error) {
func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) {
if len(tokenString) == 0 {
return nil, nil, errInvalidToken
return nil, errInvalidToken
}
_, err := s.cache.Get(tokenCacheKey(tokenString))
if err != nil {
if err == cache.ErrNoSuchKey {
return nil, nil, errTokenExpired
return nil, errTokenExpired
}
return nil, nil, err
return nil, err
}
clm := &Claims{}
_, err = jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
if err != nil {
return nil, nil, err
return nil, err
}
return &iam.User{Name: clm.Username, UID: clm.UID}, clm, nil
return &iam.User{Name: clm.Username, UID: clm.UID}, nil
}
func (s *jwtTokenIssuer) IssueTo(user User) (string, *Claims, error) {
func (s *jwtTokenIssuer) IssueTo(user User, expiresInSecond int) (string, error) {
clm := &Claims{
Username: user.GetName(),
UID: user.GetUID(),
@@ -83,8 +83,8 @@ func (s *jwtTokenIssuer) IssueTo(user User) (string, *Claims, error) {
},
}
if s.options.TokenExpiration > 0 {
clm.ExpiresAt = clm.IssuedAt + int64(s.options.TokenExpiration.Seconds())
if expiresInSecond > 0 {
clm.ExpiresAt = clm.IssuedAt + int64(expiresInSecond)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, clm)
@@ -92,19 +92,19 @@ func (s *jwtTokenIssuer) IssueTo(user User) (string, *Claims, error) {
tokenString, err := token.SignedString([]byte(s.options.JwtSecret))
if err != nil {
return "", nil, err
return "", err
}
s.cache.Set(tokenCacheKey(tokenString), tokenString, s.options.TokenExpiration)
s.cache.Set(tokenCacheKey(tokenString), tokenString, time.Second*time.Duration(expiresInSecond))
return tokenString, clm, nil
return tokenString, nil
}
func (s *jwtTokenIssuer) Revoke(token string) error {
return s.cache.Del(tokenCacheKey(token))
}
func NewJwtTokenIssuer(issuerName string, options *auth.AuthenticationOptions, cache cache.Interface) Issuer {
func NewJwtTokenIssuer(issuerName string, options *authoptions.AuthenticationOptions, cache cache.Interface) Issuer {
return &jwtTokenIssuer{
name: issuerName,
options: options,

View File

@@ -20,14 +20,14 @@ package token
import (
"github.com/google/go-cmp/cmp"
"kubesphere.io/kubesphere/pkg/api/auth"
"kubesphere.io/kubesphere/pkg/api/iam"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"testing"
)
func TestJwtTokenIssuer(t *testing.T) {
options := auth.NewAuthenticateOptions()
options := authoptions.NewAuthenticateOptions()
options.JwtSecret = "kubesphere"
issuer := NewJwtTokenIssuer(DefaultIssuerName, options, cache.NewSimpleCache())
@@ -54,12 +54,12 @@ func TestJwtTokenIssuer(t *testing.T) {
}
t.Run(testCase.description, func(t *testing.T) {
token, _, err := issuer.IssueTo(user)
token, err := issuer.IssueTo(user, 0)
if err != nil {
t.Fatal(err)
}
got, _, err := issuer.Verify(token)
got, err := issuer.Verify(token)
if err != nil {
t.Fatal(err)
}