refactor authentication (#1950)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/pflag"
|
||||
"time"
|
||||
)
|
||||
@@ -14,10 +15,14 @@ type AuthenticationOptions struct {
|
||||
MaxAuthenticateRetries int
|
||||
|
||||
// token validation duration, will refresh token expiration for each user request
|
||||
// 0 means never expire
|
||||
TokenExpiration time.Duration
|
||||
|
||||
// allow multiple users login at the same time
|
||||
MultipleLogin bool
|
||||
|
||||
// secret to signed jwt token
|
||||
JwtSecret string
|
||||
}
|
||||
|
||||
func NewAuthenticateOptions() *AuthenticationOptions {
|
||||
@@ -27,11 +32,17 @@ func NewAuthenticateOptions() *AuthenticationOptions {
|
||||
MaxAuthenticateRetries: 0,
|
||||
TokenExpiration: 0,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -39,6 +50,7 @@ func (options *AuthenticationOptions) AddFlags(fs *pflag.FlagSet, s *Authenticat
|
||||
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.DurationVar(&options.TokenExpiration, "token-expiration", s.TokenExpiration, "")
|
||||
fs.BoolVar(&options.MultipleLogin, "multiple-login", s.MultipleLogin, "")
|
||||
fs.DurationVar(&options.TokenExpiration, "token-expiration", s.TokenExpiration, "Token expire duration, for example 30m/2h/1d, 0 means token never expire unless server restart.")
|
||||
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.")
|
||||
}
|
||||
|
||||
10
pkg/api/iam/token/issuer.go
Normal file
10
pkg/api/iam/token/issuer.go
Normal file
@@ -0,0 +1,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, error)
|
||||
|
||||
// Verify verifies a token, and return a User if it's a valid token, otherwise return error
|
||||
Verify(string) (User, error)
|
||||
}
|
||||
75
pkg/api/iam/token/jwt.go
Normal file
75
pkg/api/iam/token/jwt.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"kubesphere.io/kubesphere/pkg/api/iam"
|
||||
"kubesphere.io/kubesphere/pkg/server/errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultIssuerName = "kubesphere"
|
||||
|
||||
var errInvalidToken = errors.New("invalid token")
|
||||
|
||||
type claims struct {
|
||||
Username string `json:"username"`
|
||||
UID string `json:"uid"`
|
||||
// Currently, we are not using any field in jwt.StandardClaims
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
type jwtTokenIssuer struct {
|
||||
name string
|
||||
secret []byte
|
||||
keyFunc jwt.Keyfunc
|
||||
}
|
||||
|
||||
func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) {
|
||||
if len(tokenString) == 0 {
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
|
||||
clm := &claims{}
|
||||
|
||||
_, err := jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &iam.User{Username: clm.Username, Email: clm.UID}, nil
|
||||
}
|
||||
|
||||
func (s *jwtTokenIssuer) IssueTo(user User) (string, error) {
|
||||
clm := &claims{
|
||||
Username: user.Name(),
|
||||
UID: user.UID(),
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
IssuedAt: time.Now().Unix(),
|
||||
Issuer: s.name,
|
||||
NotBefore: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, clm)
|
||||
tokenString, err := token.SignedString(s.secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func NewJwtTokenIssuer(issuerName string, secret []byte) Issuer {
|
||||
return &jwtTokenIssuer{
|
||||
name: issuerName,
|
||||
secret: secret,
|
||||
keyFunc: func(token *jwt.Token) (i interface{}, err error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
|
||||
return secret, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("expect token signed with HMAC but got %v", token.Header["alg"])
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
49
pkg/api/iam/token/jwt_test.go
Normal file
49
pkg/api/iam/token/jwt_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"kubesphere.io/kubesphere/pkg/api/iam"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJwtTokenIssuer(t *testing.T) {
|
||||
issuer := NewJwtTokenIssuer(DefaultIssuerName, []byte("kubesphere"))
|
||||
|
||||
testCases := []struct {
|
||||
description string
|
||||
name string
|
||||
email string
|
||||
}{
|
||||
{
|
||||
name: "admin",
|
||||
email: "admin@kubesphere.io",
|
||||
},
|
||||
{
|
||||
name: "bar",
|
||||
email: "bar@kubesphere.io",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
user := &iam.User{
|
||||
Username: testCase.name,
|
||||
Email: testCase.email,
|
||||
}
|
||||
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
token, err := issuer.IssueTo(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := issuer.Verify(token)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(user, got); len(diff) != 0 {
|
||||
t.Errorf("%T differ (-got, +expected), %s", user, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
8
pkg/api/iam/token/user.go
Normal file
8
pkg/api/iam/token/user.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package token
|
||||
|
||||
type User interface {
|
||||
// Name
|
||||
Name() string
|
||||
|
||||
UID() string
|
||||
}
|
||||
@@ -27,6 +27,14 @@ func NewUser() *User {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) Name() string {
|
||||
return u.Username
|
||||
}
|
||||
|
||||
func (u *User) UID() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u *User) Validate() error {
|
||||
if u.Username == "" {
|
||||
return errors.New("username can not be empty")
|
||||
|
||||
@@ -5,20 +5,19 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func HandleInternalError(response *restful.Response, err error) {
|
||||
statusCode := http.StatusInternalServerError
|
||||
|
||||
response.WriteError(statusCode, err)
|
||||
func HandleInternalError(response *restful.Response, req *restful.Request, err error) {
|
||||
response.WriteError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
func HandleBadRequest(response *restful.Response, err error) {
|
||||
|
||||
// HandleBadRequest writes http.StatusBadRequest and log error
|
||||
func HandleBadRequest(response *restful.Response, req *restful.Request, err error) {
|
||||
response.WriteError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
func HandleNotFound(response *restful.Response, err error) {
|
||||
|
||||
func HandleNotFound(response *restful.Response, req *restful.Request, err error) {
|
||||
response.WriteError(http.StatusNotFound, err)
|
||||
}
|
||||
|
||||
func HandleForbidden(response *restful.Response, err error) {
|
||||
|
||||
func HandleForbidden(response *restful.Response, req *restful.Request, err error) {
|
||||
response.WriteError(http.StatusForbidden, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user