feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> * feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> --------- Signed-off-by: ci-bot <ci-bot@kubesphere.io> Co-authored-by: ks-ci-bot <ks-ci-bot@example.com> Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
committed by
GitHub
parent
b5015ec7b9
commit
447a51f08b
@@ -1,22 +1,12 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -30,8 +20,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
@@ -39,17 +28,16 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/request"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/rest"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/im"
|
||||
"kubesphere.io/kubesphere/pkg/server/errors"
|
||||
serverrors "kubesphere.io/kubesphere/pkg/server/errors"
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
)
|
||||
|
||||
const (
|
||||
KindTokenReview = "TokenReview"
|
||||
grantTypePassword = "password"
|
||||
grantTypeRefreshToken = "refresh_token"
|
||||
grantTypeCode = "code"
|
||||
KindTokenReview = "TokenReview"
|
||||
internalServerErrorMessage = "An internal server error occurred while processing the request."
|
||||
)
|
||||
|
||||
type Spec struct {
|
||||
@@ -80,8 +68,8 @@ func (request *TokenReview) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
type discovery struct {
|
||||
// ProviderMetadata https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
type ProviderMetadata struct {
|
||||
// URL using the https scheme with no query or fragment component that the OP
|
||||
// asserts as its Issuer Identifier.
|
||||
Issuer string `json:"issuer"`
|
||||
@@ -121,20 +109,29 @@ type handler struct {
|
||||
passwordAuthenticator auth.PasswordAuthenticator
|
||||
oauthAuthenticator auth.OAuthAuthenticator
|
||||
loginRecorder auth.LoginRecorder
|
||||
clientGetter oauth.ClientGetter
|
||||
}
|
||||
|
||||
func newHandler(im im.IdentityManagementInterface,
|
||||
func NewHandler(im im.IdentityManagementInterface,
|
||||
tokenOperator auth.TokenManagementInterface,
|
||||
passwordAuthenticator auth.PasswordAuthenticator,
|
||||
oauthAuthenticator auth.OAuthAuthenticator,
|
||||
oauth2Authenticator auth.OAuthAuthenticator,
|
||||
loginRecorder auth.LoginRecorder,
|
||||
options *authentication.Options) *handler {
|
||||
return &handler{im: im,
|
||||
options *authentication.Options,
|
||||
oauthOperator oauth.ClientGetter) rest.Handler {
|
||||
handler := &handler{im: im,
|
||||
tokenOperator: tokenOperator,
|
||||
passwordAuthenticator: passwordAuthenticator,
|
||||
oauthAuthenticator: oauthAuthenticator,
|
||||
oauthAuthenticator: oauth2Authenticator,
|
||||
loginRecorder: loginRecorder,
|
||||
options: options}
|
||||
options: options,
|
||||
clientGetter: oauthOperator}
|
||||
return handler
|
||||
}
|
||||
|
||||
func FakeHandler() rest.Handler {
|
||||
handler := &handler{}
|
||||
return handler
|
||||
}
|
||||
|
||||
// tokenReview Implement webhook authentication interface
|
||||
@@ -168,53 +165,51 @@ func (h *handler) tokenReview(req *restful.Request, resp *restful.Response) {
|
||||
},
|
||||
}
|
||||
|
||||
resp.WriteEntity(success)
|
||||
_ = 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",
|
||||
func (h *handler) discovery(_ *restful.Request, response *restful.Response) {
|
||||
result := ProviderMetadata{
|
||||
Issuer: h.options.Issuer.URL,
|
||||
Auth: h.options.Issuer.URL + root + "/authorize",
|
||||
Token: h.options.Issuer.URL + root + "/token",
|
||||
Keys: h.options.Issuer.URL + root + "/keys",
|
||||
UserInfo: h.options.Issuer.URL + root + "/userinfo",
|
||||
Subjects: []string{"public"},
|
||||
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||
GrantTypes: []string{oauth.GrantTypeAuthorizationCode, oauth.GrantTypeRefreshToken},
|
||||
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"},
|
||||
CodeChallengeAlgs: []string{"plain", "S256"},
|
||||
Scopes: []string{oauth.ScopeOpenID, oauth.ScopeEmail, oauth.ScopeProfile},
|
||||
AuthMethods: []string{"client_secret_post"},
|
||||
Claims: []string{
|
||||
"iss", "sub", "aud", "iat", "exp", "email", "locale", "preferred_username",
|
||||
},
|
||||
ResponseTypes: []string{
|
||||
"code",
|
||||
"token",
|
||||
oauth.ResponseTypeCode,
|
||||
oauth.ResponseTypeIDToken,
|
||||
},
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
_ = response.WriteAsJson(result)
|
||||
}
|
||||
|
||||
func (h *handler) keys(req *restful.Request, response *restful.Response) {
|
||||
func (h *handler) keys(_ *restful.Request, response *restful.Response) {
|
||||
jwks := jose.JSONWebKeySet{
|
||||
Keys: []jose.JSONWebKey{*h.tokenOperator.Keys().SigningKeyPub},
|
||||
}
|
||||
response.WriteEntity(jwks)
|
||||
_ = 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
|
||||
var scope, responseType, clientID, redirectURI, state, nonce, prompt 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")
|
||||
|
||||
prompt = req.QueryParameter("prompt")
|
||||
// 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 {
|
||||
@@ -224,25 +219,43 @@ func (h *handler) authorize(req *restful.Request, response *restful.Response) {
|
||||
responseType, _ = req.BodyParameter("response_type")
|
||||
state, _ = req.BodyParameter("state")
|
||||
nonce, _ = req.BodyParameter("nonce")
|
||||
prompt, _ = req.BodyParameter("prompt")
|
||||
}
|
||||
|
||||
oauthClient, err := h.options.OAuthOptions.OAuthClient(clientID)
|
||||
client, err := h.clientGetter.GetOAuthClient(req.Request.Context(), clientID)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidClient(err))
|
||||
if errors.Is(err, oauth.ErrorClientNotFound) {
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidClient("The provided client_id is invalid or does not exist."))
|
||||
return
|
||||
}
|
||||
klog.Errorf("failed to get oauth client: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := oauthClient.ResolveRedirectURL(redirectURI)
|
||||
redirectURL, err := client.ResolveRedirectURL(redirectURI)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest("Redirect URI is not allowed."))
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
// Unless the Redirection URI is invalid, the Authorization Server returns the Client to the Redirection URI
|
||||
// specified in the Authorization Request with the appropriate error and state parameters.
|
||||
// Other parameters SHOULD NOT be returned.
|
||||
// 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)
|
||||
values.Add("error", string(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)
|
||||
}
|
||||
|
||||
// If no openid scope value is present, the request may still be a valid OAuth 2.0 request,
|
||||
@@ -256,205 +269,217 @@ func (h *handler) authorize(req *restful.Request, response *restful.Response) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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))
|
||||
if !client.IsValidScope(scope) {
|
||||
informsError(oauth.NewInvalidScope("The requested scope is invalid or not supported."))
|
||||
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{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: []string{clientID},
|
||||
},
|
||||
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))
|
||||
// Hybrid flow is not supported now
|
||||
if len(responseTypes) > 1 || !oauth.IsValidResponseTypes(responseTypes) {
|
||||
informsError(oauth.NewError(oauth.UnsupportedResponseType, fmt.Sprintf("The provided response_type %s is not supported by the authorization server.", responseType)))
|
||||
return
|
||||
}
|
||||
|
||||
if client.GrantMethod == oauth.GrantMethodDeny {
|
||||
informsError(oauth.NewInvalidGrant("The resource owner or authorization server denied the request."))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, _ := request.UserFrom(req.Request.Context())
|
||||
if authenticated == nil || authenticated.GetName() == user.Anonymous {
|
||||
if prompt == "none" {
|
||||
informsError(oauth.NewError(oauth.LoginRequired, "Not authenticated."))
|
||||
return
|
||||
}
|
||||
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)
|
||||
// TODO redirect to login page with refer
|
||||
http.Redirect(response.ResponseWriter, req.Request, h.options.Issuer.URL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
approved := client.GrantMethod == oauth.GrantMethodAuto
|
||||
if prompt == "none" && !approved {
|
||||
informsError(oauth.NewError(oauth.InteractionRequired, "Consent is required before proceeding with the request."))
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
// TODO oauth.GrantMethodPrompt
|
||||
|
||||
// oauth.GrantMethodAuto
|
||||
switch responseType {
|
||||
case oauth.ResponseTypeCode:
|
||||
h.handleAuthorizationCodeRequest(req, response, authCodeRequest{
|
||||
authenticated: authenticated,
|
||||
clientID: clientID,
|
||||
nonce: nonce,
|
||||
scopes: scopes,
|
||||
redirectURL: redirectURL,
|
||||
state: state,
|
||||
})
|
||||
case oauth.ResponseTypeIDToken:
|
||||
h.handleAuthIDTokenRequest(req, response, &authIDTokenRequest{
|
||||
idTokenRequest: &idTokenRequest{
|
||||
authenticated: authenticated,
|
||||
client: client,
|
||||
nonce: nonce,
|
||||
scopes: scopes,
|
||||
},
|
||||
redirectURL: redirectURL,
|
||||
state: state,
|
||||
})
|
||||
default:
|
||||
informsError(oauth.NewError(oauth.UnsupportedResponseType, "The provided response_type is not supported by the authorization server."))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) oauthCallback(req *restful.Request, response *restful.Response) {
|
||||
provider := req.PathParameter("callback")
|
||||
authenticated, provider, err := h.oauthAuthenticator.Authenticate(req.Request.Context(), provider, req.Request)
|
||||
authenticated, 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
|
||||
}
|
||||
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
// TODO(@hongming) using the really client configuration
|
||||
result, err := h.issueTokenTo(authenticated, nil)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
klog.Errorf("failed to issue token: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
if err = h.loginRecorder.RecordLogin(authenticated.GetName(), iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
|
||||
if err = h.loginRecorder.RecordLogin(req.Request.Context(), authenticated.GetName(), iamv1beta1.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
|
||||
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
_ = response.WriteEntity(result)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// token handles the Token Request to obtain an Access Token, an ID Token, and optionally a Refresh Token.
|
||||
// This is used in the Authorization Code Flow, where the RP (Client) sends a Token Request to the Token Endpoint
|
||||
// (described in Section 3.2 of OAuth 2.0 [RFC6749]) to obtain a Token Response.
|
||||
// Communication with the Token Endpoint is required to utilize TLS for security.
|
||||
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")
|
||||
clientID, _ := req.BodyParameter("client_id")
|
||||
clientSecret, _ := req.BodyParameter("client_secret")
|
||||
grantType, _ := req.BodyParameter("grant_type")
|
||||
|
||||
// All Token Responses containing sensitive information MUST include the following HTTP response header fields and values:
|
||||
// Cache-Control: no-store
|
||||
// Pragma: no-cache
|
||||
response.Header().Set("Cache-Control", "no-store")
|
||||
response.Header().Set("Pragma", "no-cache")
|
||||
|
||||
// Retrieve the OAuth client associated with the provided client_id.
|
||||
client, err := h.clientGetter.GetOAuthClient(req.Request.Context(), clientID)
|
||||
if err != nil {
|
||||
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)
|
||||
if errors.Is(err, oauth.ErrorClientNotFound) {
|
||||
klog.Warningf("The provided client_id %s is invalid or does not exist.", clientID)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidClient("The provided client_id is invalid or does not exist."))
|
||||
return
|
||||
}
|
||||
klog.Errorf("failed to get oauth client: %v", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the client_secret matches the one associated with the retrieved client.
|
||||
if client.Secret != clientSecret {
|
||||
oauthError := oauth.NewInvalidClient(fmt.Errorf("invalid client credential"))
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauthError)
|
||||
klog.Warningf("Invalid client credential for client_id %s", clientID)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.NewError(oauth.UnauthorizedClient, "Invalid client credential."))
|
||||
return
|
||||
}
|
||||
|
||||
grantType, err := req.BodyParameter("grant_type")
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
unsupportedGrantType := oauth.NewError(oauth.UnsupportedGrantType, "The provided grant_type is not supported.")
|
||||
|
||||
switch grantType {
|
||||
case grantTypePassword:
|
||||
username, _ := req.BodyParameter("username")
|
||||
password, _ := req.BodyParameter("password")
|
||||
h.passwordGrant("", username, password, req, response)
|
||||
return
|
||||
case grantTypeRefreshToken:
|
||||
h.refreshTokenGrant(req, response)
|
||||
return
|
||||
case grantTypeCode:
|
||||
h.codeGrant(req, response)
|
||||
return
|
||||
case oauth.GrantTypePassword:
|
||||
if client.Trusted {
|
||||
h.passwordGrant(req, response, client)
|
||||
return
|
||||
}
|
||||
klog.Warningf("The client %s is not trusted.", client.Name)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, unsupportedGrantType)
|
||||
case oauth.GrantTypeRefreshToken:
|
||||
h.refreshTokenGrant(req, response, client)
|
||||
case oauth.GrantTypeCode, oauth.GrantTypeAuthorizationCode:
|
||||
h.codeGrant(req, response, client)
|
||||
default:
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.ErrorUnsupportedGrantType)
|
||||
return
|
||||
klog.Warningf("The provided grant_type %s is not supported.", grantType)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, unsupportedGrantType)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(provider, username string, password string, req *restful.Request, response *restful.Response) {
|
||||
authenticated, provider, err := h.passwordAuthenticator.Authenticate(req.Request.Context(), provider, username, password)
|
||||
// passwordGrant handles the Resource Owner Password Credentials Grant.
|
||||
// For more details, refer to: 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(req *restful.Request, response *restful.Response, client *oauth.Client) {
|
||||
// Extracting parameters from the request body.
|
||||
username, _ := req.BodyParameter("username")
|
||||
password, _ := req.BodyParameter("password")
|
||||
provider, _ := req.BodyParameter("provider")
|
||||
|
||||
// Authenticate the user credentials.
|
||||
authenticated, err := h.passwordAuthenticator.Authenticate(req.Request.Context(), provider, username, password)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case auth.AccountIsNotActiveError:
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
switch {
|
||||
case errors.Is(err, auth.AccountIsNotActiveError):
|
||||
// The Account is suspended.
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("Account suspended."))
|
||||
return
|
||||
case auth.IncorrectPasswordError:
|
||||
case errors.Is(err, auth.IncorrectPasswordError):
|
||||
// Record unsuccessful login attempt.
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
if err := h.loginRecorder.RecordLogin(username, iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, err); err != nil {
|
||||
if err := h.loginRecorder.RecordLogin(req.Request.Context(), username, iamv1beta1.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, err); err != nil {
|
||||
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err)
|
||||
}
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
// Invalid username or password.
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("Invalid username or password."))
|
||||
return
|
||||
case auth.RateLimitExceededError:
|
||||
response.WriteHeaderAndEntity(http.StatusTooManyRequests, oauth.NewInvalidGrant(err))
|
||||
case errors.Is(err, auth.RateLimitExceededError):
|
||||
// Rate limit exceeded.
|
||||
_ = response.WriteHeaderAndEntity(http.StatusTooManyRequests, oauth.NewInvalidGrant("Rate limit exceeded."))
|
||||
return
|
||||
default:
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
// Authentication failed.
|
||||
klog.Errorf("Authentication failed: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
// Issue token to the authenticated user.
|
||||
result, err := h.issueTokenTo(authenticated, client)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
// Failed to issue token.
|
||||
klog.Errorf("Failed to issue token: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
// Record successful login.
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
if err = h.loginRecorder.RecordLogin(authenticated.GetName(), iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
|
||||
if err = h.loginRecorder.RecordLogin(req.Request.Context(), authenticated.GetName(), iamv1beta1.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
|
||||
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
// Respond with the issued token.
|
||||
_ = response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) issueTokenTo(user user.Info) (*oauth.Token, error) {
|
||||
func (h *handler) issueTokenTo(user user.Info, client *oauth.Client) (*oauth.Token, error) {
|
||||
accessTokenMaxAge := h.options.Issuer.AccessTokenMaxAge
|
||||
accessTokenInactivityTimeout := h.options.Issuer.AccessTokenInactivityTimeout
|
||||
if client != nil && client.AccessTokenMaxAgeSeconds > 0 && client.AccessTokenInactivityTimeoutSeconds > 0 {
|
||||
accessTokenMaxAge = time.Duration(client.AccessTokenMaxAgeSeconds) * time.Second
|
||||
accessTokenInactivityTimeout = time.Duration(client.AccessTokenInactivityTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
if !h.options.MultipleLogin {
|
||||
if err := h.tokenOperator.RevokeAllUserTokens(user.GetName()); err != nil {
|
||||
return nil, err
|
||||
@@ -463,7 +488,7 @@ func (h *handler) issueTokenTo(user user.Info) (*oauth.Token, error) {
|
||||
accessToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: user,
|
||||
Claims: token.Claims{TokenType: token.AccessToken},
|
||||
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge,
|
||||
ExpiresIn: accessTokenMaxAge,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -471,7 +496,7 @@ func (h *handler) issueTokenTo(user user.Info) (*oauth.Token, error) {
|
||||
refreshToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: user,
|
||||
Claims: token.Claims{TokenType: token.RefreshToken},
|
||||
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge + h.options.OAuthOptions.AccessTokenInactivityTimeout,
|
||||
ExpiresIn: accessTokenMaxAge + accessTokenInactivityTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -482,156 +507,181 @@ func (h *handler) issueTokenTo(user user.Info) (*oauth.Token, error) {
|
||||
// as specified in OAuth 2.0 Bearer Token Usage [RFC6750]
|
||||
TokenType: "Bearer",
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int(h.options.OAuthOptions.AccessTokenMaxAge.Seconds()),
|
||||
ExpiresIn: int(accessTokenMaxAge.Seconds()),
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Response) {
|
||||
refreshToken, err := req.BodyParameter("refresh_token")
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Response, client *oauth.Client) {
|
||||
refreshToken, _ := req.BodyParameter("refresh_token")
|
||||
verified, err := h.tokenOperator.Verify(refreshToken)
|
||||
if err != nil {
|
||||
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))
|
||||
if err != nil || verified.TokenType != token.RefreshToken {
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("The refresh token is invalid or expired."))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated := verified.User
|
||||
// update token after registration
|
||||
if authenticated.GetName() == iamv1alpha2.PreRegistrationUser &&
|
||||
if authenticated.GetName() == iamv1beta1.PreRegistrationUser &&
|
||||
authenticated.GetExtra() != nil &&
|
||||
len(authenticated.GetExtra()[iamv1alpha2.ExtraIdentityProvider]) > 0 &&
|
||||
len(authenticated.GetExtra()[iamv1alpha2.ExtraUID]) > 0 {
|
||||
len(authenticated.GetExtra()[iamv1beta1.ExtraIdentityProvider]) > 0 &&
|
||||
len(authenticated.GetExtra()[iamv1beta1.ExtraUID]) > 0 {
|
||||
|
||||
idp := authenticated.GetExtra()[iamv1alpha2.ExtraIdentityProvider][0]
|
||||
uid := authenticated.GetExtra()[iamv1alpha2.ExtraUID][0]
|
||||
idp := authenticated.GetExtra()[iamv1beta1.ExtraIdentityProvider][0]
|
||||
uid := authenticated.GetExtra()[iamv1beta1.ExtraUID][0]
|
||||
queryParam := query.New()
|
||||
queryParam.LabelSelector = labels.SelectorFromSet(labels.Set{
|
||||
iamv1alpha2.IdentifyProviderLabel: idp,
|
||||
iamv1alpha2.OriginUIDLabel: uid}).String()
|
||||
result, err := h.im.ListUsers(queryParam)
|
||||
queryParam.LabelSelector = labels.SelectorFromSet(labels.Set{iamv1beta1.IdentifyProviderLabel: idp, iamv1beta1.OriginUIDLabel: uid}).String()
|
||||
users, err := h.im.ListUsers(queryParam)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
klog.Errorf("failed to list users: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
if len(result.Items) != 1 {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(fmt.Errorf("authenticated user does not exist")))
|
||||
if len(users.Items) != 1 {
|
||||
if len(users.Items) > 1 {
|
||||
klog.Errorf("duplicate user IDs associated: %s/%s", idp, uid)
|
||||
}
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("Authenticated user does not exist."))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated = &user.DefaultInfo{Name: result.Items[0].(*iamv1alpha2.User).Name}
|
||||
authenticated = &user.DefaultInfo{Name: users.Items[0].(*iamv1beta1.User).Name}
|
||||
}
|
||||
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
result, err := h.issueTokenTo(authenticated, client)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
klog.Errorf("failed to issue token: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
_ = response.WriteEntity(result)
|
||||
}
|
||||
|
||||
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))
|
||||
func (h *handler) codeGrant(req *restful.Request, response *restful.Response, client *oauth.Client) {
|
||||
code, _ := req.BodyParameter("code")
|
||||
if code == "" {
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest("The authorization code is empty or missing."))
|
||||
return
|
||||
}
|
||||
redirectURI, _ := req.BodyParameter("redirect_uri")
|
||||
if _, err := client.ResolveRedirectURL(redirectURI); err != nil {
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest("Redirect URI is not allowed."))
|
||||
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))
|
||||
if err != nil || authorizeContext.TokenType != token.AuthorizationCode {
|
||||
_ = response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant("The authorization code is invalid or expired."))
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// The client MUST NOT use the authorization code more than once.
|
||||
err = h.tokenOperator.Revoke(code)
|
||||
if err != nil {
|
||||
if err = h.tokenOperator.Revoke(code); err != nil {
|
||||
klog.Warningf("grant: failed to revoke authorization code: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
result, err := h.issueTokenTo(authorizeContext.User)
|
||||
result, err := h.issueTokenTo(authorizeContext.User, client)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
klog.Errorf("failed to issue token: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
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.
|
||||
if !sliceutil.HasString(authorizeContext.Scopes, oauth.ScopeOpenID) {
|
||||
response.WriteEntity(result)
|
||||
_ = response.WriteEntity(result)
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := h.im.DescribeUser(authorizeContext.User.GetName())
|
||||
idTokenRequest, err := h.buildIDTokenIssueRequest(&idTokenRequest{
|
||||
authenticated: authorizeContext.User,
|
||||
client: client,
|
||||
scopes: authorizeContext.Scopes,
|
||||
nonce: authorizeContext.Nonce,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
klog.Errorf("failed to build id token request: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
idTokenRequest := &token.IssueRequest{
|
||||
User: authorizeContext.User,
|
||||
Claims: token.Claims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: authorizeContext.Audience,
|
||||
},
|
||||
Nonce: authorizeContext.Nonce,
|
||||
TokenType: token.IDToken,
|
||||
Name: authorizeContext.User.GetName(),
|
||||
},
|
||||
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))
|
||||
klog.Errorf("failed to issue id token: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
result.IDToken = idToken
|
||||
|
||||
response.WriteEntity(result)
|
||||
_ = response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) buildIDTokenIssueRequest(request *idTokenRequest) (*token.IssueRequest, error) {
|
||||
authenticated, err := h.im.DescribeUser(request.authenticated.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessTokenMaxAge := h.options.Issuer.AccessTokenMaxAge
|
||||
accessTokenInactivityTimeout := h.options.Issuer.AccessTokenInactivityTimeout
|
||||
if request.client != nil && request.client.AccessTokenMaxAgeSeconds > 0 && request.client.AccessTokenInactivityTimeoutSeconds > 0 {
|
||||
accessTokenMaxAge = time.Duration(request.client.AccessTokenMaxAgeSeconds) * time.Second
|
||||
accessTokenInactivityTimeout = time.Duration(request.client.AccessTokenInactivityTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
idTokenRequest := &token.IssueRequest{
|
||||
User: request.authenticated,
|
||||
Claims: token.Claims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: []string{request.client.Name},
|
||||
},
|
||||
Nonce: request.nonce,
|
||||
TokenType: token.IDToken,
|
||||
Name: request.authenticated.GetName(),
|
||||
},
|
||||
ExpiresIn: accessTokenMaxAge + accessTokenInactivityTimeout,
|
||||
}
|
||||
|
||||
if sliceutil.HasString(request.scopes, oauth.ScopeProfile) {
|
||||
idTokenRequest.PreferredUsername = authenticated.Name
|
||||
idTokenRequest.Locale = authenticated.Spec.Lang
|
||||
}
|
||||
|
||||
if sliceutil.HasString(request.scopes, oauth.ScopeEmail) {
|
||||
idTokenRequest.Email = authenticated.Spec.Email
|
||||
}
|
||||
return idTokenRequest, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
api.HandleInternalError(resp, req, apierrors.NewInternalError(err))
|
||||
return
|
||||
}
|
||||
authHeader := strings.TrimSpace(req.Request.Header.Get("Authorization"))
|
||||
if authHeader == "" {
|
||||
_ = resp.WriteAsJson(serverrors.None)
|
||||
return
|
||||
}
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
_ = resp.WriteAsJson(serverrors.None)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := parts[1]
|
||||
if err := h.tokenOperator.Revoke(accessToken); err != nil {
|
||||
reason := fmt.Errorf("failed to revoke access token")
|
||||
klog.Errorf("%s: %s", reason, err)
|
||||
api.HandleInternalError(resp, req, reason)
|
||||
return
|
||||
}
|
||||
|
||||
postLogoutRedirectURI := req.QueryParameter("post_logout_redirect_uri")
|
||||
if postLogoutRedirectURI == "" {
|
||||
resp.WriteAsJson(errors.None)
|
||||
_ = resp.WriteAsJson(serverrors.None)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -656,31 +706,101 @@ func (h *handler) logout(req *restful.Request, resp *restful.Response) {
|
||||
func (h *handler) userinfo(req *restful.Request, response *restful.Response) {
|
||||
authenticated, _ := request.UserFrom(req.Request.Context())
|
||||
if authenticated == nil || authenticated.GetName() == user.Anonymous {
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.ErrorLoginRequired)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.LoginRequired)
|
||||
return
|
||||
}
|
||||
detail, err := h.im.DescribeUser(authenticated.GetName())
|
||||
userDetails, err := h.im.DescribeUser(authenticated.GetName())
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
klog.Errorf("failed to get user details: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
result := token.Claims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: detail.Name,
|
||||
Subject: userDetails.Name,
|
||||
},
|
||||
Name: detail.Name,
|
||||
Email: detail.Spec.Email,
|
||||
Locale: detail.Spec.Lang,
|
||||
PreferredUsername: detail.Name,
|
||||
Name: userDetails.Name,
|
||||
Email: userDetails.Spec.Email,
|
||||
Locale: userDetails.Spec.Lang,
|
||||
PreferredUsername: userDetails.Name,
|
||||
}
|
||||
response.WriteEntity(result)
|
||||
_ = response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) loginByIdentityProvider(req *restful.Request, response *restful.Response) {
|
||||
username, _ := req.BodyParameter("username")
|
||||
password, _ := req.BodyParameter("password")
|
||||
idp := req.PathParameter("identityprovider")
|
||||
|
||||
h.passwordGrant(idp, username, password, req, response)
|
||||
type authCodeRequest struct {
|
||||
authenticated user.Info
|
||||
clientID string
|
||||
nonce string
|
||||
scopes []string
|
||||
redirectURL *url.URL
|
||||
state string
|
||||
}
|
||||
|
||||
func (h *handler) handleAuthorizationCodeRequest(req *restful.Request, response *restful.Response, authCodeRequest authCodeRequest) {
|
||||
code, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: authCodeRequest.authenticated,
|
||||
Claims: token.Claims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: []string{authCodeRequest.clientID},
|
||||
},
|
||||
TokenType: token.AuthorizationCode,
|
||||
Nonce: authCodeRequest.nonce,
|
||||
Scopes: authCodeRequest.scopes,
|
||||
},
|
||||
// A maximum authorization code lifetime of 10 minutes is
|
||||
ExpiresIn: 10 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
klog.Errorf("failed to issue auth code: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
values := authCodeRequest.redirectURL.Query()
|
||||
values.Add("code", code)
|
||||
if authCodeRequest.state != "" {
|
||||
values.Add("state", authCodeRequest.state)
|
||||
}
|
||||
authCodeRequest.redirectURL.RawQuery = values.Encode()
|
||||
http.Redirect(response, req.Request, authCodeRequest.redirectURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
type idTokenRequest struct {
|
||||
authenticated user.Info
|
||||
client *oauth.Client
|
||||
nonce string
|
||||
scopes []string
|
||||
}
|
||||
|
||||
type authIDTokenRequest struct {
|
||||
*idTokenRequest
|
||||
state string
|
||||
redirectURL *url.URL
|
||||
}
|
||||
|
||||
func (h *handler) handleAuthIDTokenRequest(req *restful.Request, response *restful.Response, authIDTokenRequest *authIDTokenRequest) {
|
||||
if authIDTokenRequest.nonce == "" {
|
||||
return
|
||||
}
|
||||
|
||||
idTokenRequest, err := h.buildIDTokenIssueRequest(authIDTokenRequest.idTokenRequest)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to build id token request: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := h.tokenOperator.IssueTo(idTokenRequest)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to issue id token: %s", err)
|
||||
_ = response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(internalServerErrorMessage))
|
||||
}
|
||||
|
||||
values := make(url.Values)
|
||||
values.Add("id_token", idToken)
|
||||
if authIDTokenRequest.state != "" {
|
||||
values.Add("state", authIDTokenRequest.state)
|
||||
}
|
||||
authIDTokenRequest.redirectURL.Fragment = values.Encode()
|
||||
http.Redirect(response, req.Request, authIDTokenRequest.redirectURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/im"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
)
|
||||
|
||||
const contentTypeFormData = "application/x-www-form-urlencoded"
|
||||
const (
|
||||
contentTypeFormData = "application/x-www-form-urlencoded"
|
||||
root = "/oauth"
|
||||
)
|
||||
|
||||
// AddToContainer ks-apiserver includes a built-in OAuth server. Users obtain OAuth access tokens to authenticate themselves to the API.
|
||||
// The OAuth server supports standard authorization code grant and the implicit grant OAuth authorization flows.
|
||||
@@ -39,40 +28,56 @@ const contentTypeFormData = "application/x-www-form-urlencoded"
|
||||
// Most authentication integrations place an authenticating proxy in front of this endpoint, or configure ks-apiserver
|
||||
// to validate credentials against a backing identity provider.
|
||||
// Requests to <ks-apiserver>/oauth/authorize can come from user-agents that cannot display interactive login pages, such as the CLI.
|
||||
func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
tokenOperator auth.TokenManagementInterface,
|
||||
passwordAuthenticator auth.PasswordAuthenticator,
|
||||
oauth2Authenticator auth.OAuthAuthenticator,
|
||||
loginRecorder auth.LoginRecorder,
|
||||
options *authentication.Options) error {
|
||||
func (h *handler) AddToContainer(c *restful.Container) error {
|
||||
wellKnown := &restful.WebService{}
|
||||
wellKnown.Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON)
|
||||
wellKnown.Path("/").Route(wellKnown.GET(".well-known/openid-configuration").
|
||||
To(h.discovery).
|
||||
Doc("OpenID provider configuration information").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Notes("The OpenID Provider's configuration information can be retrieved.").
|
||||
Operation("openid-configuration").
|
||||
Returns(http.StatusOK, api.StatusOK, ProviderMetadata{}))
|
||||
c.Add(wellKnown)
|
||||
|
||||
ws := &restful.WebService{}
|
||||
ws.Path("/oauth").
|
||||
Consumes(restful.MIME_JSON).
|
||||
Produces(restful.MIME_JSON)
|
||||
ws.Path(root).Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON)
|
||||
|
||||
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."))
|
||||
ws.Route(ws.GET("/userinfo").To(handler.userinfo).
|
||||
Doc("UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User."))
|
||||
ws.Route(ws.GET("/keys").
|
||||
To(h.keys).
|
||||
Doc("JSON Web Key Set").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Operation("openid-keys").
|
||||
Notes("This contains the signing key(s) the RP uses to validate signatures from the OP. ").
|
||||
Returns(http.StatusOK, api.StatusOK, jose.JSONWebKeySet{}))
|
||||
ws.Route(ws.GET("/userinfo").
|
||||
To(h.userinfo).
|
||||
Doc("User info endpoint").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Notes("UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User.").
|
||||
Operation("openid-userinfo").
|
||||
Returns(http.StatusOK, api.StatusOK, token.Claims{}))
|
||||
|
||||
// Implement webhook authentication interface
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
|
||||
ws.Route(ws.POST("/authenticate").
|
||||
Doc("TokenReview attempts to authenticate a token to a known user. Note: TokenReview requests may be "+
|
||||
"cached by the webhook token authenticator plugin in the kube-apiserver.").
|
||||
To(h.tokenReview).
|
||||
Deprecate().
|
||||
Doc("Token review").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Notes("Token Review attempts to authenticate a token to a known user. "+
|
||||
"Note: TokenReview requests may be cached by the webhook token authenticator plugin in the kube-apiserver.").
|
||||
Operation("token-review").
|
||||
Reads(TokenReview{}).
|
||||
To(handler.tokenReview).
|
||||
Returns(http.StatusOK, api.StatusOK, TokenReview{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
Returns(http.StatusOK, api.StatusOK, TokenReview{}))
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
|
||||
ws.Route(ws.GET("/authorize").
|
||||
Doc("The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.").
|
||||
To(h.authorize).
|
||||
Doc("Authorization endpoint").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Notes("The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.").
|
||||
Operation("openid-authorize-get").
|
||||
Param(ws.QueryParameter("response_type", "The value MUST be one of \"code\" for requesting an "+
|
||||
"authorization code as described by [RFC6749] Section 4.1.1, \"token\" for requesting an access token (implicit grant)"+
|
||||
" as described by [RFC6749] Section 4.2.2.").Required(true)).
|
||||
@@ -81,33 +86,37 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
"This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider.").Required(true)).
|
||||
Param(ws.QueryParameter("scope", "OpenID Connect requests MUST contain the openid scope value. "+
|
||||
"If the openid scope value is not present, the behavior is entirely unspecified.").Required(false)).
|
||||
Param(ws.QueryParameter("state", "Opaque value used to maintain state between the request and the callback.").Required(false)).
|
||||
To(handler.authorize).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
Param(ws.QueryParameter("state", "Opaque value used to maintain state between the request and the callback.").Required(false)))
|
||||
|
||||
// Authorization Servers MUST support the use of the HTTP GET and POST methods
|
||||
// defined in RFC 2616 [RFC2616] at the Authorization Endpoint.
|
||||
ws.Route(ws.POST("/authorize").
|
||||
Consumes(contentTypeFormData).
|
||||
Doc("The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.").
|
||||
Param(ws.BodyParameter("response_type", "The value MUST be one of \"code\" for requesting an "+
|
||||
To(h.authorize).
|
||||
Doc("Authorization endpoint").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Notes("The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.").
|
||||
Operation("openid-authorize-post").
|
||||
Param(ws.FormParameter("response_type", "The value MUST be one of \"code\" for requesting an "+
|
||||
"authorization code as described by [RFC6749] Section 4.1.1, \"token\" for requesting an access token (implicit grant)"+
|
||||
" as described by [RFC6749] Section 4.2.2.").Required(true)).
|
||||
Param(ws.BodyParameter("client_id", "OAuth 2.0 Client Identifier valid at the Authorization Server.").Required(true)).
|
||||
Param(ws.BodyParameter("redirect_uri", "Redirection URI to which the response will be sent. "+
|
||||
" as described by [RFC6749] Section 4.2.2.")).
|
||||
Param(ws.FormParameter("client_id", "OAuth 2.0 Client Identifier valid at the Authorization Server.").Required(true)).
|
||||
Param(ws.FormParameter("redirect_uri", "Redirection URI to which the response will be sent. "+
|
||||
"This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider.").Required(true)).
|
||||
Param(ws.BodyParameter("scope", "OpenID Connect requests MUST contain the openid scope value. "+
|
||||
Param(ws.FormParameter("scope", "OpenID Connect requests MUST contain the openid scope value. "+
|
||||
"If the openid scope value is not present, the behavior is entirely unspecified.").Required(false)).
|
||||
Param(ws.BodyParameter("state", "Opaque value used to maintain state between the request and the callback.").Required(false)).
|
||||
To(handler.authorize).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
Param(ws.FormParameter("state", "Opaque value used to maintain state between the request and the callback.").Required(false)))
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
|
||||
ws.Route(ws.POST("/token").
|
||||
Consumes(contentTypeFormData).
|
||||
Doc("The resource owner password credentials grant type is suitable in\n"+
|
||||
To(h.token).
|
||||
Doc("Token endpoint").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Notes("The resource owner password credentials grant type is suitable in\n"+
|
||||
"cases where the resource owner has a trust relationship with the\n"+
|
||||
"client, such as the device operating system or a highly privileged application.").
|
||||
Operation("openid-token").
|
||||
Param(ws.FormParameter("grant_type", "OAuth defines four grant types: "+
|
||||
"authorization code, implicit, resource owner password credentials, and client credentials.").
|
||||
Required(true)).
|
||||
@@ -116,14 +125,16 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
Param(ws.FormParameter("username", "The resource owner username.").Required(false)).
|
||||
Param(ws.FormParameter("password", "The resource owner password.").Required(false)).
|
||||
Param(ws.FormParameter("code", "Valid authorization code.").Required(false)).
|
||||
To(handler.token).
|
||||
Returns(http.StatusOK, http.StatusText(http.StatusOK), &oauth.Token{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
Returns(http.StatusOK, api.StatusOK, &oauth.Token{}))
|
||||
|
||||
// Authorization callback URL, where the end of the URL contains the identity provider name.
|
||||
// The provider name is also used to build the callback URL.
|
||||
ws.Route(ws.GET("/callback/{callback}").
|
||||
Doc("OAuth callback API, the path param callback is config by identity provider").
|
||||
To(h.oauthCallback).
|
||||
Doc("OAuth2 callback").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Operation("oauth-callback").
|
||||
Param(ws.PathParameter("callback", "The identity provider name.")).
|
||||
Param(ws.QueryParameter("access_token", "The access token issued by the authorization server.").
|
||||
Required(true)).
|
||||
Param(ws.QueryParameter("token_type", "The type of the token issued as described in [RFC6479] Section 7.1. "+
|
||||
@@ -137,14 +148,16 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
"otherwise, REQUIRED. The scope of the access token as described by [RFC6479] Section 3.3.").Required(false)).
|
||||
Param(ws.QueryParameter("state", "if the \"state\" parameter was present in the client authorization request."+
|
||||
"The exact value received from the client.").Required(true)).
|
||||
To(handler.oauthCallback).
|
||||
Returns(http.StatusOK, api.StatusOK, oauth.Token{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
Returns(http.StatusOK, api.StatusOK, oauth.Token{}))
|
||||
|
||||
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html
|
||||
ws.Route(ws.GET("/logout").
|
||||
Doc("This endpoint takes an ID token and logs the user out of KubeSphere if the "+
|
||||
To(h.logout).
|
||||
Doc("Logout").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagAuthentication}).
|
||||
Notes("This endpoint takes an ID token and logs the user out of KubeSphere if the "+
|
||||
"subject matches the current session.").
|
||||
Operation("logout").
|
||||
Param(ws.QueryParameter("id_token_hint", "ID Token previously issued by the OP "+
|
||||
"to the RP passed to the Logout Endpoint as a hint about the End-User's current authenticated "+
|
||||
"session with the Client. This is used as an indication of the identity of the End-User that "+
|
||||
@@ -154,21 +167,8 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
Param(ws.QueryParameter("state", "Opaque value used by the RP to maintain state between "+
|
||||
"the logout request and the callback to the endpoint specified by the post_logout_redirect_uri parameter.").
|
||||
Required(false)).
|
||||
To(handler.logout).
|
||||
Returns(http.StatusOK, http.StatusText(http.StatusOK), "").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
ws.Route(ws.POST("/login/{identityprovider}").
|
||||
Consumes(contentTypeFormData).
|
||||
Doc("Login by identity provider user").
|
||||
Param(ws.PathParameter("identityprovider", "The identity provider name")).
|
||||
Param(ws.FormParameter("username", "The username of the relevant user in ldap")).
|
||||
Param(ws.FormParameter("password", "The password of the relevant user in ldap")).
|
||||
To(handler.loginByIdentityProvider).
|
||||
Returns(http.StatusOK, http.StatusText(http.StatusOK), oauth.Token{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
Returns(http.StatusOK, http.StatusText(http.StatusOK), ""))
|
||||
|
||||
c.Add(ws)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user