Refactor authenticator

Signed-off-by: hongming <hongming@kubesphere.io>
This commit is contained in:
hongming
2021-08-17 11:34:51 +08:00
parent 83df7d1ffd
commit 4b5b1c64bc
41 changed files with 1923 additions and 758 deletions

View File

@@ -20,6 +20,15 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/form3tech-oss/jwt-go"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
"kubesphere.io/kubesphere/pkg/server/errors"
@@ -32,7 +41,6 @@ import (
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/api"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/apiserver/query"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/models/auth"
@@ -41,8 +49,9 @@ import (
const (
KindTokenReview = "TokenReview"
passwordGrantType = "password"
refreshTokenGrantType = "refresh_token"
grantTypePassword = "password"
grantTypeRefreshToken = "refresh_token"
grantTypeCode = "code"
)
type Spec struct {
@@ -75,30 +84,30 @@ func (request *TokenReview) Validate() error {
type handler struct {
im im.IdentityManagementInterface
options *authoptions.AuthenticationOptions
options *authentication.Options
tokenOperator auth.TokenManagementInterface
passwordAuthenticator auth.PasswordAuthenticator
oauth2Authenticator auth.OAuthAuthenticator
oauthAuthenticator auth.OAuthAuthenticator
loginRecorder auth.LoginRecorder
}
func newHandler(im im.IdentityManagementInterface,
tokenOperator auth.TokenManagementInterface,
passwordAuthenticator auth.PasswordAuthenticator,
oauth2Authenticator auth.OAuthAuthenticator,
oauthAuthenticator auth.OAuthAuthenticator,
loginRecorder auth.LoginRecorder,
options *authoptions.AuthenticationOptions) *handler {
options *authentication.Options) *handler {
return &handler{im: im,
tokenOperator: tokenOperator,
passwordAuthenticator: passwordAuthenticator,
oauth2Authenticator: oauth2Authenticator,
oauthAuthenticator: oauthAuthenticator,
loginRecorder: loginRecorder,
options: options}
}
// Implement webhook authentication interface
// tokenReview Implement webhook authentication interface
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
func (h *handler) TokenReview(req *restful.Request, resp *restful.Response) {
func (h *handler) tokenReview(req *restful.Request, resp *restful.Response) {
var tokenReview TokenReview
err := req.ReadEntity(&tokenReview)
@@ -112,12 +121,13 @@ func (h *handler) TokenReview(req *restful.Request, resp *restful.Response) {
return
}
authenticated, err := h.tokenOperator.Verify(tokenReview.Spec.Token)
verified, err := h.tokenOperator.Verify(tokenReview.Spec.Token)
if err != nil {
api.HandleInternalError(resp, req, err)
api.HandleBadRequest(resp, req, err)
return
}
authenticated := verified.User
success := TokenReview{APIVersion: tokenReview.APIVersion,
Kind: KindTokenReview,
Status: &Status{
@@ -129,78 +139,144 @@ func (h *handler) TokenReview(req *restful.Request, resp *restful.Response) {
resp.WriteEntity(success)
}
func (h *handler) Authorize(req *restful.Request, resp *restful.Response) {
authenticated, ok := request.UserFrom(req.Request.Context())
clientId := req.QueryParameter("client_id")
responseType := req.QueryParameter("response_type")
redirectURI := req.QueryParameter("redirect_uri")
// 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
scope = req.QueryParameter("scope")
clientID = req.QueryParameter("client_id")
redirectURI = req.QueryParameter("redirect_uri")
//prompt = req.QueryParameter("prompt")
responseType = req.QueryParameter("response_type")
state = req.QueryParameter("state")
nonce = req.QueryParameter("nonce")
conf, err := h.options.OAuthOptions.OAuthClient(clientId)
// Authorization Servers MUST support the use of the HTTP GET and POST methods
// defined in RFC 2616 [RFC2616] at the Authorization Endpoint.
if req.Request.Method == http.MethodPost {
scope, _ = req.BodyParameter("scope")
clientID, _ = req.BodyParameter("client_id")
redirectURI, _ = req.BodyParameter("redirect_uri")
responseType, _ = req.BodyParameter("response_type")
state, _ = req.BodyParameter("state")
nonce, _ = req.BodyParameter("nonce")
}
oauthClient, err := h.options.OAuthOptions.OAuthClient(clientID)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
api.HandleError(resp, req, err)
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidClient(err))
return
}
if responseType != "token" {
err := apierrors.NewBadRequest(fmt.Sprintf("Response type %s is not supported", responseType))
api.HandleError(resp, req, err)
return
}
if !ok {
err := apierrors.NewUnauthorized("Unauthorized")
api.HandleError(resp, req, err)
return
}
token, err := h.tokenOperator.IssueTo(authenticated)
redirectURL, err := oauthClient.ResolveRedirectURL(redirectURI)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
api.HandleError(resp, req, err)
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
return
}
redirectURL, err := conf.ResolveRedirectURL(redirectURI)
authenticated, _ := request.UserFrom(req.Request.Context())
if authenticated == nil || authenticated.GetName() == user.Anonymous {
response.Header().Add("WWW-Authenticate", "Basic")
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.ErrorLoginRequired)
return
}
// 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.
var scopes []string
if scope != "" {
scopes = strings.Split(scope, " ")
}
var responseTypes []string
if responseType != "" {
responseTypes = strings.Split(responseType, " ")
}
// If the resource owner denies the access request or if the request
// fails for reasons other than a missing or invalid redirection URI,
// the authorization server informs the client by adding the following
// parameters to the query component of the redirection URI using the
// "application/x-www-form-urlencoded" format
informsError := func(err oauth.Error) {
values := make(url.Values, 0)
values.Add("error", err.Type)
if err.Description != "" {
values.Add("error_description", err.Description)
}
if state != "" {
values.Add("state", state)
}
redirectURL.RawQuery = values.Encode()
http.Redirect(response.ResponseWriter, req.Request, redirectURL.String(), http.StatusFound)
return
}
// Other scope values MAY be present.
// Scope values used that are not understood by an implementation SHOULD be ignored.
if !oauth.IsValidScopes(scopes) {
klog.Warningf("Some requested scopes were invalid: %v", scopes)
}
if !oauth.IsValidResponseTypes(responseTypes) {
err := fmt.Errorf("Some requested response types were invalid")
informsError(oauth.NewInvalidRequest(err))
return
}
// TODO(hongming) support Hybrid Flow
// Authorization Code Flow
if responseType == oauth.ResponseCode {
code, err := h.tokenOperator.IssueTo(&token.IssueRequest{
User: authenticated,
Claims: token.Claims{
StandardClaims: jwt.StandardClaims{
Audience: []string{clientID},
},
TokenType: token.AuthorizationCode,
Nonce: nonce,
},
// A maximum authorization code lifetime of 10 minutes is
ExpiresIn: 10 * time.Minute,
})
if err != nil {
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
}
values := redirectURL.Query()
values.Add("code", code)
redirectURL.RawQuery = values.Encode()
http.Redirect(response, req.Request, redirectURL.String(), http.StatusFound)
}
// Implicit Flow
if responseType != oauth.ResponseToken {
informsError(oauth.ErrorUnsupportedResponseType)
return
}
result, err := h.issueTokenTo(authenticated)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
api.HandleError(resp, req, err)
return
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
}
redirectURL = fmt.Sprintf("%s#access_token=%s&token_type=Bearer", redirectURL, token.AccessToken)
if token.ExpiresIn > 0 {
redirectURL = fmt.Sprintf("%s&expires_in=%v", redirectURL, token.ExpiresIn)
}
resp.Header().Set("Content-Type", "text/plain")
http.Redirect(resp, req.Request, redirectURL, http.StatusFound)
values := make(url.Values, 0)
values.Add("access_token", result.AccessToken)
values.Add("refresh_token", result.RefreshToken)
values.Add("token_type", result.TokenType)
values.Add("expires_in", fmt.Sprint(result.ExpiresIn))
redirectURL.Fragment = values.Encode()
http.Redirect(response, req.Request, redirectURL.String(), http.StatusFound)
}
func (h *handler) oauthCallback(req *restful.Request, resp *restful.Response) {
func (h *handler) oauthCallback(req *restful.Request, response *restful.Response) {
provider := req.PathParameter("callback")
// OAuth2 callback, see also https://tools.ietf.org/html/rfc6749#section-4.1.2
code := req.QueryParameter("code")
// CAS callback, see also https://apereo.github.io/cas/6.3.x/protocol/CAS-Protocol-V2-Specification.html#25-servicevalidate-cas-20
if code == "" {
code = req.QueryParameter("ticket")
}
if code == "" {
err := apierrors.NewUnauthorized("Unauthorized: missing code")
api.HandleError(resp, req, err)
authenticated, provider, err := h.oauthAuthenticator.Authenticate(req.Request.Context(), provider, req.Request)
if err != nil {
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
return
}
authenticated, provider, err := h.oauth2Authenticator.Authenticate(provider, code)
result, err := h.issueTokenTo(authenticated)
if err != nil {
api.HandleUnauthorized(resp, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
return
}
result, err := h.tokenOperator.IssueTo(authenticated)
if err != nil {
api.HandleInternalError(resp, req, apierrors.NewInternalError(err))
return
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
}
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
@@ -208,10 +284,10 @@ func (h *handler) oauthCallback(req *restful.Request, resp *restful.Response) {
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
}
resp.WriteEntity(result)
response.WriteEntity(result)
}
func (h *handler) Login(request *restful.Request, response *restful.Response) {
func (h *handler) login(request *restful.Request, response *restful.Response) {
var loginRequest LoginRequest
err := request.ReadEntity(&loginRequest)
if err != nil {
@@ -221,29 +297,70 @@ func (h *handler) Login(request *restful.Request, response *restful.Response) {
h.passwordGrant(loginRequest.Username, loginRequest.Password, request, response)
}
func (h *handler) Token(req *restful.Request, response *restful.Response) {
grantType, err := req.BodyParameter("grant_type")
// To obtain an Access Token, an ID Token, and optionally a Refresh Token,
// the RP (Client) sends a Token Request to the Token Endpoint to obtain a Token Response,
// as described in Section 3.2 of OAuth 2.0 [RFC6749], when using the Authorization Code Flow.
// Communication with the Token Endpoint MUST utilize TLS.
func (h *handler) token(req *restful.Request, response *restful.Response) {
// TODO(hongming) support basic auth
// https://datatracker.ietf.org/doc/html/rfc6749#section-2.3
clientID, err := req.BodyParameter("client_id")
if err != nil {
api.HandleBadRequest(response, req, err)
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.NewInvalidClient(err))
return
}
clientSecret, err := req.BodyParameter("client_secret")
if err != nil {
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.NewInvalidClient(err))
return
}
client, err := h.options.OAuthOptions.OAuthClient(clientID)
if err != nil {
oauthError := oauth.NewInvalidClient(err)
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauthError)
return
}
if client.Secret != clientSecret {
oauthError := oauth.NewInvalidClient(fmt.Errorf("invalid client credential"))
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauthError)
return
}
grantType, err := req.BodyParameter("grant_type")
if err != nil {
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
return
}
switch grantType {
case passwordGrantType:
case grantTypePassword:
username, _ := req.BodyParameter("username")
password, _ := req.BodyParameter("password")
h.passwordGrant(username, password, req, response)
break
case refreshTokenGrantType:
return
case grantTypeRefreshToken:
h.refreshTokenGrant(req, response)
break
return
case grantTypeCode:
h.codeGrant(req, response)
return
default:
err := apierrors.NewBadRequest(fmt.Sprintf("Grant type %s is not supported", grantType))
api.HandleBadRequest(response, req, err)
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.ErrorUnsupportedGrantType)
return
}
}
// passwordGrant handle Resource Owner Password Credentials Grant
// for more details: https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
// The resource owner password credentials grant type is suitable in
// cases where the resource owner has a trust relationship with the client,
// such as the device operating system or a highly privileged application.
// The authorization server should take special care when enabling this
// grant type and only allow it when other flows are not viable.
func (h *handler) passwordGrant(username string, password string, req *restful.Request, response *restful.Response) {
authenticated, provider, err := h.passwordAuthenticator.Authenticate(username, password)
authenticated, provider, err := h.passwordAuthenticator.Authenticate(req.Request.Context(), username, password)
if err != nil {
switch err {
case auth.IncorrectPasswordError:
@@ -251,45 +368,79 @@ func (h *handler) passwordGrant(username string, password string, req *restful.R
if err := h.loginRecorder.RecordLogin(username, iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, err); err != nil {
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err)
}
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
return
case auth.RateLimitExceededError:
api.HandleTooManyRequests(response, req, apierrors.NewTooManyRequestsError(fmt.Sprintf("Unauthorized: %s", err)))
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
return
default:
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
return
}
}
result, err := h.tokenOperator.IssueTo(authenticated)
result, err := h.issueTokenTo(authenticated)
if err != nil {
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
return
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
}
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
if err = h.loginRecorder.RecordLogin(authenticated.GetName(), iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
klog.Errorf("Failed to record successful login for user %s, error: %v", username, err)
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
}
response.WriteEntity(result)
}
func (h *handler) issueTokenTo(authenticated user.Info) (*oauth.Token, error) {
accessToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
User: authenticated,
Claims: token.Claims{TokenType: token.AccessToken},
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge,
})
if err != nil {
return nil, err
}
refreshToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
User: authenticated,
Claims: token.Claims{TokenType: token.RefreshToken},
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge + h.options.OAuthOptions.AccessTokenInactivityTimeout,
})
if err != nil {
return nil, err
}
result := oauth.Token{
AccessToken: accessToken,
// The OAuth 2.0 token_type response parameter value MUST be Bearer,
// as specified in OAuth 2.0 Bearer Token Usage [RFC6750]
TokenType: "Bearer",
RefreshToken: refreshToken,
ExpiresIn: int(h.options.OAuthOptions.AccessTokenMaxAge.Seconds()),
}
return &result, nil
}
func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Response) {
refreshToken, err := req.BodyParameter("refresh_token")
if err != nil {
api.HandleBadRequest(response, req, apierrors.NewBadRequest(err.Error()))
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
return
}
authenticated, err := h.tokenOperator.Verify(refreshToken)
verified, err := h.tokenOperator.Verify(refreshToken)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
return
}
if verified.TokenType != token.RefreshToken {
err = fmt.Errorf("ivalid token type %v want %v", verified.TokenType, token.RefreshToken)
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
return
}
authenticated := verified.User
// update token after registration
if authenticated.GetName() == iamv1alpha2.PreRegistrationUser &&
authenticated.GetExtra() != nil &&
@@ -304,28 +455,71 @@ func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Resp
iamv1alpha2.OriginUIDLabel: uid}).String()
result, err := h.im.ListUsers(queryParam)
if err != nil {
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
return
}
if len(result.Items) != 1 {
err := apierrors.NewUnauthorized("authenticated user does not exist")
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(fmt.Errorf("authenticated user does not exist")))
return
}
authenticated = &user.DefaultInfo{Name: result.Items[0].(*iamv1alpha2.User).Name}
}
result, err := h.tokenOperator.IssueTo(authenticated)
result, err := h.issueTokenTo(authenticated)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
return
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
}
response.WriteEntity(result)
}
func (h *handler) Logout(req *restful.Request, resp *restful.Response) {
func (h *handler) codeGrant(req *restful.Request, response *restful.Response) {
code, err := req.BodyParameter("code")
if err != nil {
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
return
}
authorizeContext, err := h.tokenOperator.Verify(code)
if err != nil {
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)
}
authenticated := authorizeContext.User
result, err := h.issueTokenTo(authenticated)
if err != nil {
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
}
idToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
User: authenticated,
Claims: token.Claims{
StandardClaims: jwt.StandardClaims{
Audience: authorizeContext.Audience,
},
Nonce: authorizeContext.Nonce,
TokenType: token.IDToken,
},
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge + h.options.OAuthOptions.AccessTokenInactivityTimeout,
})
result.IDToken = idToken
response.WriteEntity(result)
}
func (h *handler) logout(req *restful.Request, resp *restful.Response) {
authenticated, ok := request.UserFrom(req.Request.Context())
if ok {
if err := h.tokenOperator.RevokeAllUserTokens(authenticated.GetName()); err != nil {