@@ -61,6 +61,12 @@ type VerifiedResponse struct {
|
||||
Claims
|
||||
}
|
||||
|
||||
// Keys hold encryption and signing keys.
|
||||
type Keys struct {
|
||||
SigningKey *jose.JSONWebKey
|
||||
SigningKeyPub *jose.JSONWebKey
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -68,6 +74,9 @@ type Issuer interface {
|
||||
|
||||
// Verify verifies a token, and return a user info if it's a valid token, otherwise return error
|
||||
Verify(string) (*VerifiedResponse, error)
|
||||
|
||||
// Keys hold encryption and signing keys.
|
||||
Keys() *Keys
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
@@ -80,6 +89,14 @@ type Claims struct {
|
||||
// String value used to associate a Client session with an ID Token, and to mitigate replay attacks.
|
||||
// The value is passed through unmodified from the Authentication Request to the ID Token.
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
// Scopes can be used to request that specific sets of information be made available as Claim Values.
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
// End-User's preferred e-mail address.
|
||||
Email string `json:"email,omitempty"`
|
||||
// End-User's locale, represented as a BCP47 [RFC5646] language tag.
|
||||
Locale string `json:"locale,omitempty"`
|
||||
// Shorthand name by which the End-User wishes to be referred to at the RP,
|
||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||
// Extra contains the additional information
|
||||
Extra map[string][]string `json:"extra,omitempty"`
|
||||
}
|
||||
@@ -90,7 +107,7 @@ type issuer struct {
|
||||
// signing access_token and refresh_token
|
||||
secret []byte
|
||||
// signing id_token
|
||||
signKey *jose.JSONWebKey
|
||||
signKey *Keys
|
||||
// Token verification maximum time difference
|
||||
maximumClockSkew time.Duration
|
||||
}
|
||||
@@ -114,6 +131,18 @@ func (s *issuer) IssueTo(request *IssueRequest) (string, error) {
|
||||
if request.Nonce != "" {
|
||||
claims.Nonce = request.Nonce
|
||||
}
|
||||
if request.Email != "" {
|
||||
claims.Email = request.Email
|
||||
}
|
||||
if request.PreferredUsername != "" {
|
||||
claims.PreferredUsername = request.PreferredUsername
|
||||
}
|
||||
if request.Locale != "" {
|
||||
claims.Locale = request.Locale
|
||||
}
|
||||
if len(request.Scopes) > 0 {
|
||||
claims.Scopes = request.Scopes
|
||||
}
|
||||
if request.ExpiresIn > 0 {
|
||||
claims.ExpiresAt = claims.IssuedAt + int64(request.ExpiresIn.Seconds())
|
||||
}
|
||||
@@ -122,8 +151,8 @@ func (s *issuer) IssueTo(request *IssueRequest) (string, error) {
|
||||
var err error
|
||||
if request.TokenType == IDToken {
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
t.Header[headerKeyID] = s.signKey.KeyID
|
||||
token, err = t.SignedString(s.signKey.Key)
|
||||
t.Header[headerKeyID] = s.signKey.SigningKey.KeyID
|
||||
token, err = t.SignedString(s.signKey.SigningKey.Key)
|
||||
} else {
|
||||
token, err = jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.secret)
|
||||
}
|
||||
@@ -175,13 +204,17 @@ func (s *issuer) Verify(token string) (*VerifiedResponse, error) {
|
||||
return verified, nil
|
||||
}
|
||||
|
||||
func (s *issuer) Keys() *Keys {
|
||||
return s.signKey
|
||||
}
|
||||
|
||||
func (s *issuer) keyFunc(token *jwt.Token) (i interface{}, err error) {
|
||||
alg, _ := token.Header[headerAlgorithm].(string)
|
||||
switch alg {
|
||||
case jwt.SigningMethodHS256.Alg():
|
||||
return s.secret, nil
|
||||
case jwt.SigningMethodRS256.Alg():
|
||||
return s.signKey.Key, nil
|
||||
return s.signKey.SigningKey.Key, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpect signature algorithm %v", token.Header[headerAlgorithm])
|
||||
}
|
||||
@@ -263,11 +296,19 @@ func NewIssuer(options *authentication.Options) (Issuer, error) {
|
||||
name: options.OAuthOptions.Issuer,
|
||||
secret: []byte(options.JwtSecret),
|
||||
maximumClockSkew: options.MaximumClockSkew,
|
||||
signKey: &jose.JSONWebKey{
|
||||
Key: signKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
signKey: &Keys{
|
||||
SigningKey: &jose.JSONWebKey{
|
||||
Key: signKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
},
|
||||
SigningKeyPub: &jose.JSONWebKey{
|
||||
Key: signKey.Public(),
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -87,11 +87,19 @@ func TestNewIssuer(t *testing.T) {
|
||||
name: options.OAuthOptions.Issuer,
|
||||
secret: []byte(options.JwtSecret),
|
||||
maximumClockSkew: options.MaximumClockSkew,
|
||||
signKey: &jose.JSONWebKey{
|
||||
Key: signKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
signKey: &Keys{
|
||||
SigningKey: &jose.JSONWebKey{
|
||||
Key: signKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
},
|
||||
SigningKeyPub: &jose.JSONWebKey{
|
||||
Key: signKey.Public(),
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
@@ -116,8 +124,10 @@ func TestNewIssuerGenerateSignKey(t *testing.T) {
|
||||
|
||||
iss := got.(*issuer)
|
||||
assert.NotNil(t, iss.signKey)
|
||||
assert.NotNil(t, iss.signKey.Key)
|
||||
assert.NotNil(t, iss.signKey.KeyID)
|
||||
assert.NotNil(t, iss.signKey.SigningKey)
|
||||
assert.NotNil(t, iss.signKey.SigningKeyPub)
|
||||
assert.NotNil(t, iss.signKey.SigningKey.KeyID)
|
||||
assert.NotNil(t, iss.signKey.SigningKeyPub.KeyID)
|
||||
}
|
||||
|
||||
func Test_issuer_IssueTo(t *testing.T) {
|
||||
|
||||
@@ -18,6 +18,8 @@ package oauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -82,6 +84,40 @@ func (request *TokenReview) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
type discovery struct {
|
||||
// URL using the https scheme with no query or fragment component that the OP
|
||||
// asserts as its Issuer Identifier.
|
||||
Issuer string `json:"issuer"`
|
||||
// URL of the OP's OAuth 2.0 Authorization Endpoint.
|
||||
Auth string `json:"authorization_endpoint"`
|
||||
// URL of the OP's OAuth 2.0 Token Endpoint.
|
||||
Token string `json:"token_endpoint"`
|
||||
// URL of the OP's UserInfo Endpoint
|
||||
UserInfo string `json:"userinfo_endpoint"`
|
||||
// URL of the OP's JSON Web Key Set [JWK] document.
|
||||
Keys string `json:"jwks_uri"`
|
||||
// JSON array containing a list of the OAuth 2.0 Grant Type values that this OP supports.
|
||||
GrantTypes []string `json:"grant_types_supported"`
|
||||
// JSON array containing a list of the OAuth 2.0 response_type values that this OP supports.
|
||||
ResponseTypes []string `json:"response_types_supported"`
|
||||
// JSON array containing a list of the Subject Identifier types that this OP supports.
|
||||
Subjects []string `json:"subject_types_supported"`
|
||||
// JSON array containing a list of the JWS signing algorithms (alg values) supported by
|
||||
// the OP for the ID Token to encode the Claims in a JWT [JWT].
|
||||
IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"`
|
||||
// JSON array containing a list of Proof Key for Code
|
||||
// Exchange (PKCE) [RFC7636] code challenge methods supported by this authorization server.
|
||||
CodeChallengeAlgs []string `json:"code_challenge_methods_supported"`
|
||||
// JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports.
|
||||
Scopes []string `json:"scopes_supported"`
|
||||
// JSON array containing a list of Client Authentication methods supported by this Token Endpoint.
|
||||
AuthMethods []string `json:"token_endpoint_auth_methods_supported"`
|
||||
// JSON array containing a list of the Claim Names of the Claims that the OpenID Provider
|
||||
// MAY be able to supply values for.
|
||||
Claims []string `json:"claims_supported"`
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
im im.IdentityManagementInterface
|
||||
options *authentication.Options
|
||||
@@ -139,6 +175,39 @@ func (h *handler) tokenReview(req *restful.Request, resp *restful.Response) {
|
||||
resp.WriteEntity(success)
|
||||
}
|
||||
|
||||
func (h *handler) discovery(req *restful.Request, response *restful.Response) {
|
||||
result := discovery{
|
||||
Issuer: h.options.OAuthOptions.Issuer,
|
||||
Auth: h.options.OAuthOptions.Issuer + "/authorize",
|
||||
Token: h.options.OAuthOptions.Issuer + "/token",
|
||||
Keys: h.options.OAuthOptions.Issuer + "/keys",
|
||||
UserInfo: h.options.OAuthOptions.Issuer + "/userinfo",
|
||||
Subjects: []string{"public"},
|
||||
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||
IDTokenAlgs: []string{string(jose.RS256)},
|
||||
CodeChallengeAlgs: []string{"S256", "plain"},
|
||||
Scopes: []string{"openid", "email", "profile", "offline_access"},
|
||||
// TODO(hongming) support client_secret_jwt
|
||||
AuthMethods: []string{"client_secret_basic", "client_secret_post"},
|
||||
Claims: []string{
|
||||
"iss", "sub", "aud", "iat", "exp", "email", "locale", "preferred_username",
|
||||
},
|
||||
ResponseTypes: []string{
|
||||
"code",
|
||||
"token",
|
||||
},
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) keys(req *restful.Request, response *restful.Response) {
|
||||
jwks := jose.JSONWebKeySet{
|
||||
Keys: []jose.JSONWebKey{*h.tokenOperator.Keys().SigningKeyPub},
|
||||
}
|
||||
response.WriteEntity(jwks)
|
||||
}
|
||||
|
||||
// The Authorization Endpoint performs Authentication of the End-User.
|
||||
func (h *handler) authorize(req *restful.Request, response *restful.Response) {
|
||||
var scope, responseType, clientID, redirectURI, state, nonce string
|
||||
@@ -207,7 +276,6 @@ func (h *handler) authorize(req *restful.Request, response *restful.Response) {
|
||||
}
|
||||
redirectURL.RawQuery = values.Encode()
|
||||
http.Redirect(response.ResponseWriter, req.Request, redirectURL.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Other scope values MAY be present.
|
||||
@@ -233,12 +301,14 @@ func (h *handler) authorize(req *restful.Request, response *restful.Response) {
|
||||
},
|
||||
TokenType: token.AuthorizationCode,
|
||||
Nonce: nonce,
|
||||
Scopes: scopes,
|
||||
},
|
||||
// A maximum authorization code lifetime of 10 minutes is
|
||||
ExpiresIn: 10 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
values := redirectURL.Query()
|
||||
values.Add("code", code)
|
||||
@@ -255,6 +325,7 @@ func (h *handler) authorize(req *restful.Request, response *restful.Response) {
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
values := make(url.Values, 0)
|
||||
@@ -277,6 +348,7 @@ func (h *handler) oauthCallback(req *restful.Request, response *restful.Response
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
@@ -382,6 +454,7 @@ func (h *handler) passwordGrant(username string, password string, req *restful.R
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
@@ -392,9 +465,9 @@ func (h *handler) passwordGrant(username string, password string, req *restful.R
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) issueTokenTo(authenticated user.Info) (*oauth.Token, error) {
|
||||
func (h *handler) issueTokenTo(user user.Info) (*oauth.Token, error) {
|
||||
accessToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: authenticated,
|
||||
User: user,
|
||||
Claims: token.Claims{TokenType: token.AccessToken},
|
||||
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge,
|
||||
})
|
||||
@@ -402,7 +475,7 @@ func (h *handler) issueTokenTo(authenticated user.Info) (*oauth.Token, error) {
|
||||
return nil, err
|
||||
}
|
||||
refreshToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: authenticated,
|
||||
User: user,
|
||||
Claims: token.Claims{TokenType: token.RefreshToken},
|
||||
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge + h.options.OAuthOptions.AccessTokenInactivityTimeout,
|
||||
})
|
||||
@@ -469,6 +542,7 @@ func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Resp
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
@@ -486,26 +560,42 @@ func (h *handler) codeGrant(req *restful.Request, response *restful.Response) {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
return
|
||||
}
|
||||
|
||||
if authorizeContext.TokenType != token.AuthorizationCode {
|
||||
err = fmt.Errorf("ivalid token type %v want %v", authorizeContext.TokenType, token.AuthorizationCode)
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
return
|
||||
}
|
||||
|
||||
// The client MUST NOT use the authorization code more than once.
|
||||
err = h.tokenOperator.Revoke(code)
|
||||
if err != nil {
|
||||
klog.Warningf("grant: failed to revoke authorization code: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
// The client MUST NOT use the authorization code more than once.
|
||||
err = h.tokenOperator.Revoke(code)
|
||||
if err != nil {
|
||||
klog.Warningf("grant: failed to revoke authorization code: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
authenticated := authorizeContext.User
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
result, err := h.issueTokenTo(authorizeContext.User)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: authenticated,
|
||||
// If no openid scope value is present, the request may still be a valid OAuth 2.0 request,
|
||||
// but is not an OpenID Connect request.
|
||||
if !sliceutil.HasString(authorizeContext.Scopes, oauth.ScopeOpenID) {
|
||||
response.WriteEntity(result)
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := h.im.DescribeUser(authorizeContext.User.GetName())
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
idTokenRequest := &token.IssueRequest{
|
||||
User: authorizeContext.User,
|
||||
Claims: token.Claims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Audience: authorizeContext.Audience,
|
||||
@@ -514,8 +604,25 @@ func (h *handler) codeGrant(req *restful.Request, response *restful.Response) {
|
||||
TokenType: token.IDToken,
|
||||
},
|
||||
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge + h.options.OAuthOptions.AccessTokenInactivityTimeout,
|
||||
})
|
||||
}
|
||||
|
||||
if sliceutil.HasString(authorizeContext.Scopes, oauth.ScopeProfile) {
|
||||
idTokenRequest.PreferredUsername = authenticated.Name
|
||||
idTokenRequest.Locale = authenticated.Spec.Lang
|
||||
}
|
||||
|
||||
if sliceutil.HasString(authorizeContext.Scopes, oauth.ScopeEmail) {
|
||||
idTokenRequest.Email = authenticated.Spec.Email
|
||||
}
|
||||
|
||||
idToken, err := h.tokenOperator.IssueTo(idTokenRequest)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
result.IDToken = idToken
|
||||
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
|
||||
handler := newHandler(im, tokenOperator, passwordAuthenticator, oauth2Authenticator, loginRecorder, options)
|
||||
|
||||
ws.Route(ws.GET("/.well-known/openid-configuration").To(handler.discovery).
|
||||
Doc("The OpenID Provider's configuration information can be retrieved."))
|
||||
ws.Route(ws.GET("/keys").To(handler.keys).
|
||||
Doc("OP's JSON Web Key Set [JWK] document."))
|
||||
|
||||
// Implement webhook authentication interface
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
|
||||
ws.Route(ws.POST("/authenticate").
|
||||
|
||||
@@ -41,6 +41,8 @@ type TokenManagementInterface interface {
|
||||
Revoke(token string) error
|
||||
// RevokeAllUserTokens revoke all user tokens
|
||||
RevokeAllUserTokens(username string) error
|
||||
// Keys hold encryption and signing keys.
|
||||
Keys() *token.Keys
|
||||
}
|
||||
|
||||
type tokenOperator struct {
|
||||
@@ -117,6 +119,10 @@ func (t *tokenOperator) RevokeAllUserTokens(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tokenOperator) Keys() *token.Keys {
|
||||
return t.issuer.Keys()
|
||||
}
|
||||
|
||||
// tokenCacheValidate verify that the token is in the cache
|
||||
func (t *tokenOperator) tokenCacheValidate(username, token string) error {
|
||||
key := fmt.Sprintf("kubesphere:user:%s:token:%s", username, token)
|
||||
|
||||
Reference in New Issue
Block a user