Merge pull request #3776 from wansir/feat-logout
Support RP-Initiated Logout
This commit is contained in:
@@ -25,6 +25,8 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -93,6 +95,10 @@ type endpoint struct {
|
||||
UserInfoURL string `json:"userInfoURL" yaml:"userInfoURL"`
|
||||
// URL of the OP's JSON Web Key Set [JWK](https://openid.net/specs/openid-connect-discovery-1_0.html#JWK) document.
|
||||
JWKSURL string `json:"jwksURL"`
|
||||
// URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP.
|
||||
// This URL MUST use the https scheme and MAY contain port, path, and query parameter components.
|
||||
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata
|
||||
EndSessionURL string `json:"endSessionURL"`
|
||||
}
|
||||
|
||||
type oidcIdentity struct {
|
||||
@@ -157,23 +163,23 @@ func (f *oidcProviderFactory) Create(options oauth.DynamicOptions) (identityprov
|
||||
oidcProvider.Endpoint.TokenURL, _ = providerJSON["token_endpoint"].(string)
|
||||
oidcProvider.Endpoint.UserInfoURL, _ = providerJSON["userinfo_endpoint"].(string)
|
||||
oidcProvider.Endpoint.JWKSURL, _ = providerJSON["jwks_uri"].(string)
|
||||
oidcProvider.Endpoint.EndSessionURL, _ = providerJSON["end_session_endpoint"].(string)
|
||||
oidcProvider.Provider = provider
|
||||
oidcProvider.Verifier = provider.Verifier(&oidc.Config{
|
||||
// TODO: support HS256
|
||||
ClientID: oidcProvider.ClientID,
|
||||
})
|
||||
options["endpoint"] = oauth.DynamicOptions{
|
||||
"authURL": oidcProvider.Endpoint.AuthURL,
|
||||
"tokenURL": oidcProvider.Endpoint.TokenURL,
|
||||
"userInfoURL": oidcProvider.Endpoint.UserInfoURL,
|
||||
"jwksURL": oidcProvider.Endpoint.JWKSURL,
|
||||
"authURL": oidcProvider.Endpoint.AuthURL,
|
||||
"tokenURL": oidcProvider.Endpoint.TokenURL,
|
||||
"userInfoURL": oidcProvider.Endpoint.UserInfoURL,
|
||||
"jwksURL": oidcProvider.Endpoint.JWKSURL,
|
||||
"endSessionURL": oidcProvider.Endpoint.EndSessionURL,
|
||||
}
|
||||
}
|
||||
scopes := []string{oidc.ScopeOpenID}
|
||||
if len(oidcProvider.Scopes) > 0 {
|
||||
if !sliceutil.HasString(oidcProvider.Scopes, oidc.ScopeOpenID) {
|
||||
scopes = append(scopes, oidcProvider.Scopes...)
|
||||
} else {
|
||||
scopes = append(scopes, "openid", "profile", "email")
|
||||
}
|
||||
oidcProvider.Scopes = scopes
|
||||
oidcProvider.OAuth2Config = &oauth2.Config{
|
||||
@@ -270,8 +276,8 @@ func (o *oidcProvider) IdentityExchange(code string) (identityprovider.Identity,
|
||||
if o.PreferredUsernameKey != "" {
|
||||
preferredUsernameKey = o.PreferredUsernameKey
|
||||
}
|
||||
preferredUsername, _ = claims[preferredUsernameKey].(string)
|
||||
|
||||
preferredUsername, _ = claims[preferredUsernameKey].(string)
|
||||
if preferredUsername == "" {
|
||||
preferredUsername, _ = claims["name"].(string)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ var _ = BeforeSuite(func(done Done) {
|
||||
"token_endpoint": fmt.Sprintf("%s/token", oidcServer.URL),
|
||||
"authorization_endpoint": fmt.Sprintf("%s/authorize", oidcServer.URL),
|
||||
"userinfo_endpoint": fmt.Sprintf("%s/userinfo", oidcServer.URL),
|
||||
"end_session_endpoint": fmt.Sprintf("%s/endsession", oidcServer.URL),
|
||||
"jwks_uri": fmt.Sprintf("%s/keys", oidcServer.URL),
|
||||
"response_types_supported": []string{
|
||||
"code",
|
||||
@@ -182,10 +183,11 @@ var _ = Describe("OIDC", func() {
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"insecureSkipVerify": true,
|
||||
"endpoint": oauth.DynamicOptions{
|
||||
"authURL": fmt.Sprintf("%s/authorize", oidcServer.URL),
|
||||
"tokenURL": fmt.Sprintf("%s/token", oidcServer.URL),
|
||||
"userInfoURL": fmt.Sprintf("%s/userinfo", oidcServer.URL),
|
||||
"jwksURL": fmt.Sprintf("%s/keys", oidcServer.URL),
|
||||
"authURL": fmt.Sprintf("%s/authorize", oidcServer.URL),
|
||||
"tokenURL": fmt.Sprintf("%s/token", oidcServer.URL),
|
||||
"userInfoURL": fmt.Sprintf("%s/userinfo", oidcServer.URL),
|
||||
"jwksURL": fmt.Sprintf("%s/keys", oidcServer.URL),
|
||||
"endSessionURL": fmt.Sprintf("%s/endsession", oidcServer.URL),
|
||||
},
|
||||
}
|
||||
Expect(config).Should(Equal(expected))
|
||||
|
||||
@@ -19,6 +19,9 @@ package oauth
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/server/errors"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -320,3 +323,33 @@ func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Resp
|
||||
|
||||
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 {
|
||||
api.HandleInternalError(resp, req, apierrors.NewInternalError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
postLogoutRedirectURI := req.QueryParameter("post_logout_redirect_uri")
|
||||
if postLogoutRedirectURI == "" {
|
||||
resp.WriteAsJson(errors.None)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := url.Parse(postLogoutRedirectURI)
|
||||
if err != nil {
|
||||
api.HandleBadRequest(resp, req, fmt.Errorf("invalid logout redirect URI: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
state := req.QueryParameter("state")
|
||||
if state != "" {
|
||||
redirectURL.Query().Add("state", state)
|
||||
}
|
||||
|
||||
resp.Header().Set("Content-Type", "text/plain")
|
||||
http.Redirect(resp, req.Request, redirectURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
@@ -110,6 +110,23 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
Returns(http.StatusOK, api.StatusOK, oauth.Token{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
// 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 "+
|
||||
"subject matches the current session.").
|
||||
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 "+
|
||||
"the RP is requesting be logged out by the OP.").Required(false)).
|
||||
Param(ws.QueryParameter("post_logout_redirect_uri", "URL to which the RP is requesting "+
|
||||
"that the End-User's User Agent be redirected after a logout has been performed. ").Required(false)).
|
||||
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}))
|
||||
|
||||
c.Add(ws)
|
||||
|
||||
// legacy auth API
|
||||
|
||||
@@ -37,6 +37,8 @@ type TokenManagementInterface interface {
|
||||
Verify(token string) (user.Info, error)
|
||||
// IssueTo issues a token a User, return error if issuing process failed
|
||||
IssueTo(user user.Info) (*oauth.Token, error)
|
||||
// RevokeAllUserTokens revoke all user tokens
|
||||
RevokeAllUserTokens(username string) error
|
||||
}
|
||||
|
||||
type tokenOperator struct {
|
||||
@@ -93,7 +95,7 @@ func (t tokenOperator) IssueTo(user user.Info) (*oauth.Token, error) {
|
||||
}
|
||||
|
||||
if !t.options.MultipleLogin {
|
||||
if err = t.revokeAllUserTokens(user.GetName()); err != nil {
|
||||
if err = t.RevokeAllUserTokens(user.GetName()); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
@@ -113,7 +115,7 @@ func (t tokenOperator) IssueTo(user user.Info) (*oauth.Token, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) revokeAllUserTokens(username string) error {
|
||||
func (t tokenOperator) RevokeAllUserTokens(username string) error {
|
||||
pattern := fmt.Sprintf("kubesphere:user:%s:token:*", username)
|
||||
if keys, err := t.cache.Keys(pattern); err != nil {
|
||||
klog.Error(err)
|
||||
|
||||
Reference in New Issue
Block a user