diff --git a/pkg/apiserver/authentication/identityprovider/oidc/oidc.go b/pkg/apiserver/authentication/identityprovider/oidc/oidc.go index 177397391..2a7efd3cd 100644 --- a/pkg/apiserver/authentication/identityprovider/oidc/oidc.go +++ b/pkg/apiserver/authentication/identityprovider/oidc/oidc.go @@ -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) } diff --git a/pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go b/pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go index 6e6c00774..89917eeb6 100644 --- a/pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go +++ b/pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go @@ -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)) diff --git a/pkg/kapis/oauth/handler.go b/pkg/kapis/oauth/handler.go index 3eb855387..ac3b37d13 100644 --- a/pkg/kapis/oauth/handler.go +++ b/pkg/kapis/oauth/handler.go @@ -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) +} diff --git a/pkg/kapis/oauth/register.go b/pkg/kapis/oauth/register.go index 0e0db0599..9a49cf949 100644 --- a/pkg/kapis/oauth/register.go +++ b/pkg/kapis/oauth/register.go @@ -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 diff --git a/pkg/models/auth/token.go b/pkg/models/auth/token.go index 210b6b2fe..ec7c8b462 100644 --- a/pkg/models/auth/token.go +++ b/pkg/models/auth/token.go @@ -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)