diff --git a/go.mod b/go.mod index 1f0a34390d573885b7cb62601a570daf87284773..029213c0f89943d8477ce8d7b102451a2f837fa6 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d google.golang.org/grpc v1.30.0 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect + gopkg.in/cas.v2 v2.2.0 gopkg.in/square/go-jose.v2 v2.4.0 gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect gopkg.in/src-d/go-git.v4 v4.11.0 @@ -683,6 +684,7 @@ replace ( gopkg.in/alecthomas/kingpin.v2 => gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/alexcesaro/quotedprintable.v3 => gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc gopkg.in/asn1-ber.v1 => gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d + gopkg.in/cas.v2 => gopkg.in/cas.v2 v2.2.0 gopkg.in/check.v1 => gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 gopkg.in/cheggaaa/pb.v1 => gopkg.in/cheggaaa/pb.v1 v1.0.25 gopkg.in/errgo.v2 => gopkg.in/errgo.v2 v2.1.0 @@ -730,7 +732,6 @@ replace ( k8s.io/klog/v2 => k8s.io/klog/v2 v2.0.0 k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 k8s.io/kubectl => k8s.io/kubectl v0.18.6 - k8s.io/kubernetes => k8s.io/kubernetes v1.14.0 k8s.io/metrics => k8s.io/metrics v0.18.6 k8s.io/utils => k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 kubesphere.io/im => kubesphere.io/im v0.1.0 diff --git a/go.sum b/go.sum index 17a32b982f226d962e06741c72dc5cdb920bf498..ba15f7f70f628f9a07355091a794adb88a2c1630 100644 --- a/go.sum +++ b/go.sum @@ -778,6 +778,8 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/cas.v2 v2.2.0 h1:P9mMBcXS1IH04uNio9M2VVJwrovGDf3D9trxXPXRoE8= +gopkg.in/cas.v2 v2.2.0/go.mod h1:mlmjh4qM/Jm3eSDD0QVr5GaaSW3nOonSUSWkLLvNYnI= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= diff --git a/pkg/apiserver/authentication/identityprovider/cas/cas.go b/pkg/apiserver/authentication/identityprovider/cas/cas.go new file mode 100644 index 0000000000000000000000000000000000000000..8005af938cc81e999ecd16e5b425ba7888d72e71 --- /dev/null +++ b/pkg/apiserver/authentication/identityprovider/cas/cas.go @@ -0,0 +1,96 @@ +/* +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. +*/ + +package cas + +import ( + "crypto/tls" + "fmt" + "github.com/mitchellh/mapstructure" + gocas "gopkg.in/cas.v2" + "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" + "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" + "net/http" + "net/url" +) + +func init() { + identityprovider.RegisterOAuthProvider(&casProviderFactory{}) +} + +type cas struct { + RedirectURL string `json:"redirectURL" yaml:"redirectURL"` + CASServerURL string `json:"casServerURL" yaml:"casServerURL"` + InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"` + client *gocas.RestClient +} + +type casProviderFactory struct { +} + +type casIdentity struct { + User string `json:"user"` +} + +func (c casIdentity) GetUserID() string { + return c.User +} + +func (c casIdentity) GetUsername() string { + return c.User +} + +func (c casIdentity) GetEmail() string { + return "" +} + +func (f casProviderFactory) Type() string { + return "CASIdentityProvider" +} + +func (f casProviderFactory) Create(options oauth.DynamicOptions) (identityprovider.OAuthProvider, error) { + var cas cas + if err := mapstructure.Decode(options, &cas); err != nil { + return nil, err + } + casURL, err := url.Parse(cas.CASServerURL) + if err != nil { + return nil, err + } + redirectURL, err := url.Parse(cas.RedirectURL) + if err != nil { + return nil, err + } + cas.client = gocas.NewRestClient(&gocas.RestOptions{ + CasURL: casURL, + ServiceURL: redirectURL, + Client: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: cas.InsecureSkipVerify}, + }, + }, + URLScheme: nil, + }) + return &cas, nil +} + +func (c cas) IdentityExchange(ticket string) (identityprovider.Identity, error) { + resp, err := c.client.ValidateServiceTicket(gocas.ServiceTicket(ticket)) + if err != nil { + return nil, fmt.Errorf("cas validate service ticket failed: %v", err) + } + return &casIdentity{User: resp.User}, nil +} diff --git a/pkg/apiserver/authentication/options/authenticate_options.go b/pkg/apiserver/authentication/options/authenticate_options.go index 06c300dddae14ac9b50b5d7628e573aea5b3510a..c6d8bf67417a2564f201d0f035e1750918792c3c 100644 --- a/pkg/apiserver/authentication/options/authenticate_options.go +++ b/pkg/apiserver/authentication/options/authenticate_options.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/pflag" "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/aliyunidaas" + _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/cas" _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/github" _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/ldap" _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/oidc" diff --git a/pkg/kapis/oauth/handler.go b/pkg/kapis/oauth/handler.go index 978d74d8427862c52242ed5fead79851a454670c..84c2dbfa7d182ea4b9f6f0a9d77dd907cf473d62 100644 --- a/pkg/kapis/oauth/handler.go +++ b/pkg/kapis/oauth/handler.go @@ -172,9 +172,13 @@ func (h *handler) Authorize(req *restful.Request, resp *restful.Response) { } func (h *handler) oauthCallback(req *restful.Request, resp *restful.Response) { - code := req.QueryParameter("code") 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) diff --git a/vendor/gopkg.in/cas.v2/.gitignore b/vendor/gopkg.in/cas.v2/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..9b9a5a3c22df821e0f91152c14705e362bfd9897 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/.gitignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test +.idea + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode diff --git a/vendor/gopkg.in/cas.v2/LICENSE b/vendor/gopkg.in/cas.v2/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..1d425077a7939bdd48c9f22d6c9db10eeef3fea6 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Geoff Garside + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/gopkg.in/cas.v2/README.md b/vendor/gopkg.in/cas.v2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b88ac5377d6810aa6b35484c19a9c642db6bc4c8 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/README.md @@ -0,0 +1,18 @@ +# CAS Client library + +CAS provides a http package compatible client implementation for use with +securing http frontends in golang. + + import "gopkg.in/cas.v2" + +## Unmaintained + +As is probably obvious I've not had any time to properly put into this project in a while, and no longer work with any projects using CAS for Authentication. Under the normal Golang suggested process I'd recommend anyone else interested in maintaining the project to fork it to a new import path. As I somewhat arrogantly took the gopkg.in/cas import path, if someone is interested in taking over the project under that path please raise an issue. That said in the current world of Golang modules and versioning style I'm not sure gopkg.in is still that relevant. + +Apologies for this project having become so stagnant. + +## Examples and Documentation + +Documentation is available at: http://godoc.org/gopkg.in/cas.v2 +Examples are included in the documentation but are also available in the +`_examples` directory. diff --git a/vendor/gopkg.in/cas.v2/client.go b/vendor/gopkg.in/cas.v2/client.go new file mode 100644 index 0000000000000000000000000000000000000000..deabe1444094ed087e2f25f5fbe15a486efd5f62 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/client.go @@ -0,0 +1,373 @@ +package cas + +import ( + "crypto/rand" + "fmt" + "net/http" + "net/url" + + "github.com/golang/glog" +) + +// Client configuration options +type Options struct { + URL *url.URL // URL to the CAS service + Store TicketStore // Custom TicketStore, if nil a MemoryStore will be used + Client *http.Client // Custom http client to allow options for http connections + SendService bool // Custom sendService to determine whether you need to send service param + URLScheme URLScheme // Custom url scheme, can be used to modify the request urls for the client + Cookie *http.Cookie // http.Cookie options, uses Path, Domain, MaxAge, HttpOnly, & Secure + SessionStore SessionStore +} + +// Client implements the main protocol +type Client struct { + tickets TicketStore + client *http.Client + urlScheme URLScheme + cookie *http.Cookie + + sessions SessionStore + sendService bool + + stValidator *ServiceTicketValidator +} + +// NewClient creates a Client with the provided Options. +func NewClient(options *Options) *Client { + if glog.V(2) { + glog.Infof("cas: new client with options %v", options) + } + + var tickets TicketStore + if options.Store != nil { + tickets = options.Store + } else { + tickets = &MemoryStore{} + } + + var sessions SessionStore + if options.SessionStore != nil { + sessions = options.SessionStore + } else { + sessions = NewMemorySessionStore() + } + + var urlScheme URLScheme + if options.URLScheme != nil { + urlScheme = options.URLScheme + } else { + urlScheme = NewDefaultURLScheme(options.URL) + } + + var client *http.Client + if options.Client != nil { + client = options.Client + } else { + client = &http.Client{} + } + + var cookie *http.Cookie + if options.Cookie != nil { + cookie = options.Cookie + } else { + cookie = &http.Cookie{ + MaxAge: 86400, + HttpOnly: false, + Secure: false, + } + } + + return &Client{ + tickets: tickets, + client: client, + urlScheme: urlScheme, + cookie: cookie, + sessions: sessions, + sendService: options.SendService, + stValidator: NewServiceTicketValidator(client, options.URL), + } +} + +// Handle wraps a http.Handler to provide CAS authentication for the handler. +func (c *Client) Handle(h http.Handler) http.Handler { + return &clientHandler{ + c: c, + h: h, + } +} + +// HandleFunc wraps a function to provide CAS authentication for the handler function. +func (c *Client) HandleFunc(h func(http.ResponseWriter, *http.Request)) http.Handler { + return c.Handle(http.HandlerFunc(h)) +} + +// requestURL determines an absolute URL from the http.Request. +func requestURL(r *http.Request) (*url.URL, error) { + u, err := url.Parse(r.URL.String()) + if err != nil { + return nil, err + } + + u.Host = r.Host + if host := r.Header.Get("X-Forwarded-Host"); host != "" { + u.Host = host + } + + u.Scheme = "http" + if scheme := r.Header.Get("X-Forwarded-Proto"); scheme != "" { + u.Scheme = scheme + } else if r.TLS != nil { + u.Scheme = "https" + } + + return u, nil +} + +// LoginUrlForRequest determines the CAS login URL for the http.Request. +func (c *Client) LoginUrlForRequest(r *http.Request) (string, error) { + u, err := c.urlScheme.Login() + if err != nil { + return "", err + } + + service, err := requestURL(r) + if err != nil { + return "", err + } + + q := u.Query() + q.Add("service", sanitisedURLString(service)) + u.RawQuery = q.Encode() + + return u.String(), nil +} + +// LogoutUrlForRequest determines the CAS logout URL for the http.Request. +func (c *Client) LogoutUrlForRequest(r *http.Request) (string, error) { + u, err := c.urlScheme.Logout() + if err != nil { + return "", err + } + + if c.sendService { + service, err := requestURL(r) + if err != nil { + return "", err + } + + q := u.Query() + q.Add("service", sanitisedURLString(service)) + u.RawQuery = q.Encode() + } + + return u.String(), nil +} + +// ServiceValidateUrlForRequest determines the CAS serviceValidate URL for the ticket and http.Request. +func (c *Client) ServiceValidateUrlForRequest(ticket string, r *http.Request) (string, error) { + service, err := requestURL(r) + if err != nil { + return "", err + } + return c.stValidator.ServiceValidateUrl(service, ticket) +} + +// ValidateUrlForRequest determines the CAS validate URL for the ticket and http.Request. +func (c *Client) ValidateUrlForRequest(ticket string, r *http.Request) (string, error) { + service, err := requestURL(r) + if err != nil { + return "", err + } + return c.stValidator.ValidateUrl(service, ticket) +} + +// RedirectToLogout replies to the request with a redirect URL to log out of CAS. +func (c *Client) RedirectToLogout(w http.ResponseWriter, r *http.Request) { + u, err := c.LogoutUrlForRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if glog.V(2) { + glog.Infof("Logging out, redirecting client to %v with status %v", + u, http.StatusFound) + } + + c.clearSession(w, r) + http.Redirect(w, r, u, http.StatusFound) +} + +// RedirectToLogout replies to the request with a redirect URL to authenticate with CAS. +func (c *Client) RedirectToLogin(w http.ResponseWriter, r *http.Request) { + u, err := c.LoginUrlForRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if glog.V(2) { + glog.Infof("Redirecting client to %v with status %v", u, http.StatusFound) + } + + http.Redirect(w, r, u, http.StatusFound) +} + +// validateTicket performs CAS ticket validation with the given ticket and service. +func (c *Client) validateTicket(ticket string, service *http.Request) error { + serviceUrl, err := requestURL(service) + if err != nil { + return err + } + + success, err := c.stValidator.ValidateTicket(serviceUrl, ticket) + if err != nil { + return err + } + + if err := c.tickets.Write(ticket, success); err != nil { + return err + } + + return nil +} + +// getSession finds or creates a session for the request. +// +// A cookie is set on the response if one is not provided with the request. +// Validates the ticket if the URL parameter is provided. +func (c *Client) getSession(w http.ResponseWriter, r *http.Request) { + cookie := c.getCookie(w, r) + + if s, ok := c.sessions.Get(cookie.Value); ok { + if t, err := c.tickets.Read(s); err == nil { + if glog.V(1) { + glog.Infof("Re-used ticket %s for %s", s, t.User) + } + + setAuthenticationResponse(r, t) + return + } else { + if glog.V(2) { + glog.Infof("Ticket %v not in %T: %v", s, c.tickets, err) + } + + if glog.V(1) { + glog.Infof("Clearing ticket %s, no longer exists in ticket store", s) + } + + clearCookie(w, cookie) + } + } + + if ticket := r.URL.Query().Get("ticket"); ticket != "" { + if err := c.validateTicket(ticket, r); err != nil { + if glog.V(2) { + glog.Infof("Error validating ticket: %v", err) + } + return // allow ServeHTTP() + } + + c.setSession(cookie.Value, ticket) + + if t, err := c.tickets.Read(ticket); err == nil { + if glog.V(1) { + glog.Infof("Validated ticket %s for %s", ticket, t.User) + } + + setAuthenticationResponse(r, t) + return + } else { + if glog.V(2) { + glog.Infof("Ticket %v not in %T: %v", ticket, c.tickets, err) + } + + if glog.V(1) { + glog.Infof("Clearing ticket %s, no longer exists in ticket store", ticket) + } + + clearCookie(w, cookie) + } + } +} + +// getCookie finds or creates the session cookie on the response. +func (c *Client) getCookie(w http.ResponseWriter, r *http.Request) *http.Cookie { + cookie, err := r.Cookie(sessionCookieName) + if err != nil { + // NOTE: Intentionally not enabling HttpOnly so the cookie can + // still be used by Ajax requests. + cookie = &http.Cookie{ + Name: sessionCookieName, + Value: newSessionID(), + Path: c.cookie.Path, + Domain: c.cookie.Domain, + MaxAge: c.cookie.MaxAge, + HttpOnly: c.cookie.HttpOnly, + Secure: c.cookie.Secure, + } + + if glog.V(2) { + glog.Infof("Setting %v cookie with value: %v", cookie.Name, cookie.Value) + } + + r.AddCookie(cookie) // so we can find it later if required + http.SetCookie(w, cookie) + } + + return cookie +} + +// newSessionId generates a new opaque session identifier for use in the cookie. +func newSessionID() string { + const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + // generate 64 character string + bytes := make([]byte, 64) + rand.Read(bytes) + + for k, v := range bytes { + bytes[k] = alphabet[v%byte(len(alphabet))] + } + + return string(bytes) +} + +// clearCookie invalidates and removes the cookie from the client. +func clearCookie(w http.ResponseWriter, c *http.Cookie) { + c.MaxAge = -1 + http.SetCookie(w, c) +} + +// setSession stores the session id to ticket mapping in the Client. +func (c *Client) setSession(id string, ticket string) { + if glog.V(2) { + glog.Infof("Recording session, %v -> %v", id, ticket) + } + + c.sessions.Set(id, ticket) +} + +// clearSession removes the session from the client and clears the cookie. +func (c *Client) clearSession(w http.ResponseWriter, r *http.Request) { + cookie := c.getCookie(w, r) + + if s, ok := c.sessions.Get(cookie.Value); ok { + if err := c.tickets.Delete(s); err != nil { + fmt.Printf("Failed to remove %v from %T: %v\n", cookie.Value, c.tickets, err) + if glog.V(2) { + glog.Errorf("Failed to remove %v from %T: %v", cookie.Value, c.tickets, err) + } + } + + c.deleteSession(s) + } + + clearCookie(w, cookie) +} + +// deleteSession removes the session from the client +func (c *Client) deleteSession(id string) { + c.sessions.Delete(id) +} diff --git a/vendor/gopkg.in/cas.v2/doc.go b/vendor/gopkg.in/cas.v2/doc.go new file mode 100644 index 0000000000000000000000000000000000000000..ce61d7ca0895ffcd9c410f8c8cb67ae2e1ed027c --- /dev/null +++ b/vendor/gopkg.in/cas.v2/doc.go @@ -0,0 +1,10 @@ +/* +Package cas implements a CAS client. + +CAS is a protocol which provides authentication and authorisation for securing +typically HTTP based services. + +References: + [PROTOCOL]: http://jasig.github.io/cas/4.0.x/protocol/CAS-Protocol.html +*/ +package cas diff --git a/vendor/gopkg.in/cas.v2/go.mod b/vendor/gopkg.in/cas.v2/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..d0dfe8a57833b875c0646f5866efd670798da909 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/go.mod @@ -0,0 +1,9 @@ +module gopkg.in/cas.v2 + +go 1.12 + +require ( + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + github.com/stretchr/testify v1.4.0 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/vendor/gopkg.in/cas.v2/go.sum b/vendor/gopkg.in/cas.v2/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..1ab9f6091301db86ead98bed3ab89ca20d3f91e8 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/gopkg.in/cas.v2/handler.go b/vendor/gopkg.in/cas.v2/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..0549d26bee751502f52040cff16a9a89ac406725 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/handler.go @@ -0,0 +1,78 @@ +package cas + +import ( + "fmt" + "net/http" + + "github.com/golang/glog" +) + +const ( + sessionCookieName = "_cas_session" +) + +// clientHandler handles CAS Protocol HTTP requests +type clientHandler struct { + c *Client + h http.Handler +} + +// ServeHTTP handles HTTP requests, processes CAS requests +// and passes requests up to its child http.Handler. +func (ch *clientHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if glog.V(2) { + glog.Infof("cas: handling %v request for %v", r.Method, r.URL) + } + + setClient(r, ch.c) + + if isSingleLogoutRequest(r) { + ch.performSingleLogout(w, r) + return + } + + ch.c.getSession(w, r) + ch.h.ServeHTTP(w, r) + return +} + +// isSingleLogoutRequest determines if the http.Request is a CAS Single Logout Request. +// +// The rules for a SLO request are, HTTP POST urlencoded form with a logoutRequest parameter. +func isSingleLogoutRequest(r *http.Request) bool { + if r.Method != "POST" { + return false + } + + contentType := r.Header.Get("Content-Type") + if contentType != "application/x-www-form-urlencoded" { + return false + } + + if v := r.FormValue("logoutRequest"); v == "" { + return false + } + + return true +} + +// performSingleLogout processes a single logout request +func (ch *clientHandler) performSingleLogout(w http.ResponseWriter, r *http.Request) { + rawXML := r.FormValue("logoutRequest") + logoutRequest, err := parseLogoutRequest([]byte(rawXML)) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := ch.c.tickets.Delete(logoutRequest.SessionIndex); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + ch.c.deleteSession(logoutRequest.SessionIndex) + + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "OK") +} diff --git a/vendor/gopkg.in/cas.v2/http_helpers.go b/vendor/gopkg.in/cas.v2/http_helpers.go new file mode 100644 index 0000000000000000000000000000000000000000..dbd373801feebee134a6e89d4c72577281da9f1b --- /dev/null +++ b/vendor/gopkg.in/cas.v2/http_helpers.go @@ -0,0 +1,152 @@ +package cas + +import ( + "context" + "net/http" + "time" +) + +type key int + +const ( // emulating enums is actually pretty ugly in go. + clientKey key = iota + authenticationResponseKey +) + +// setClient associates a Client with a http.Request. +func setClient(r *http.Request, c *Client) { + ctx := context.WithValue(r.Context(), clientKey, c) + r2 := r.WithContext(ctx) + *r = *r2 +} + +// getClient retrieves the Client associated with the http.Request. +func getClient(r *http.Request) *Client { + if c := r.Context().Value(clientKey); c != nil { + return c.(*Client) + } else { + return nil // explicitly pass along the nil to caller -- conforms to previous impl + } +} + +// RedirectToLogin allows CAS protected handlers to redirect a request +// to the CAS login page. +func RedirectToLogin(w http.ResponseWriter, r *http.Request) { + c := getClient(r) + if c == nil { + err := "cas: redirect to cas failed as no client associated with request" + http.Error(w, err, http.StatusInternalServerError) + return + } + + c.RedirectToLogin(w, r) +} + +// RedirectToLogout allows CAS protected handlers to redirect a request +// to the CAS logout page. +func RedirectToLogout(w http.ResponseWriter, r *http.Request) { + c := getClient(r) + if c == nil { + err := "cas: redirect to cas failed as no client associated with request" + http.Error(w, err, http.StatusInternalServerError) + return + } + + c.RedirectToLogout(w, r) +} + +// setAuthenticationResponse associates an AuthenticationResponse with +// a http.Request. +func setAuthenticationResponse(r *http.Request, a *AuthenticationResponse) { + ctx := context.WithValue(r.Context(), authenticationResponseKey, a) + r2 := r.WithContext(ctx) + *r = *r2 +} + +// getAuthenticationResponse retrieves the AuthenticationResponse associated +// with a http.Request. +func getAuthenticationResponse(r *http.Request) *AuthenticationResponse { + if a := r.Context().Value(authenticationResponseKey); a != nil { + return a.(*AuthenticationResponse) + } else { + return nil // explicitly pass along the nil to caller -- conforms to previous impl + } +} + +// IsAuthenticated indicates whether the request has been authenticated with CAS. +func IsAuthenticated(r *http.Request) bool { + if a := getAuthenticationResponse(r); a != nil { + return true + } + + return false +} + +// Username returns the authenticated users username +func Username(r *http.Request) string { + if a := getAuthenticationResponse(r); a != nil { + return a.User + } + + return "" +} + +// Attributes returns the authenticated users attributes. +func Attributes(r *http.Request) UserAttributes { + if a := getAuthenticationResponse(r); a != nil { + return a.Attributes + } + + return nil +} + +// AuthenticationDate returns the date and time that authentication was performed. +// +// This may return time.IsZero if Authentication Date information is not included +// in the CAS service validation response. This will be the case for CAS 2.0 +// protocol servers. +func AuthenticationDate(r *http.Request) time.Time { + var t time.Time + if a := getAuthenticationResponse(r); a != nil { + t = a.AuthenticationDate + } + + return t +} + +// IsNewLogin indicates whether the CAS service ticket was granted following a +// new authentication. +// +// This may incorrectly return false if Is New Login information is not included +// in the CAS service validation response. This will be the case for CAS 2.0 +// protocol servers. +func IsNewLogin(r *http.Request) bool { + if a := getAuthenticationResponse(r); a != nil { + return a.IsNewLogin + } + + return false +} + +// IsRememberedLogin indicates whether the CAS service ticket was granted by the +// presence of a long term authentication token. +// +// This may incorrectly return false if Remembered Login information is not included +// in the CAS service validation response. This will be the case for CAS 2.0 +// protocol servers. +func IsRememberedLogin(r *http.Request) bool { + if a := getAuthenticationResponse(r); a != nil { + return a.IsRememberedLogin + } + + return false +} + +// MemberOf returns the list of groups which the user belongs to. +func MemberOf(r *http.Request) []string { + if a := getAuthenticationResponse(r); a != nil { + return a.MemberOf + } + + return nil +} diff --git a/vendor/gopkg.in/cas.v2/logout_request.go b/vendor/gopkg.in/cas.v2/logout_request.go new file mode 100644 index 0000000000000000000000000000000000000000..7810cad5dda10e587e1b11e9112bbb0fc239d2a7 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/logout_request.go @@ -0,0 +1,77 @@ +package cas + +import ( + "crypto/rand" + "encoding/xml" + "strings" + "time" +) + +// Represents the XML CAS Single Log Out Request data +type logoutRequest struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol LogoutRequest"` + Version string `xml:"Version,attr"` + IssueInstant time.Time `xml:"-"` + RawIssueInstant string `xml:"IssueInstant,attr"` + ID string `xml:"ID,attr"` + NameID string `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"` + SessionIndex string `xml:"SessionIndex"` +} + +func parseLogoutRequest(data []byte) (*logoutRequest, error) { + l := &logoutRequest{} + if err := xml.Unmarshal(data, &l); err != nil { + return nil, err + } + + t, err := parseDate(l.RawIssueInstant) + if err != nil { + return nil, err + } + + l.IssueInstant = t + l.NameID = strings.TrimSpace(l.NameID) + l.SessionIndex = strings.TrimSpace(l.SessionIndex) + + return l, nil +} + +func parseDate(raw string) (time.Time, error) { + t, err := time.Parse(time.RFC1123Z, raw) + if err != nil { + // if RFC1123Z does not match, we will try iso8601 + t, err = time.Parse("2006-01-02T15:04:05Z0700", raw) + if err != nil { + return t, err + } + } + return t, nil +} + +func newLogoutRequestID() string { + const alphabet = "abcdef0123456789" + + // generate 64 character string + bytes := make([]byte, 64) + rand.Read(bytes) + + for k, v := range bytes { + bytes[k] = alphabet[v%byte(len(alphabet))] + } + + return string(bytes) +} + +func xmlLogoutRequest(ticket string) ([]byte, error) { + l := &logoutRequest{ + Version: "2.0", + IssueInstant: time.Now().UTC(), + ID: newLogoutRequestID(), + NameID: "@NOT_USED@", + SessionIndex: ticket, + } + + l.RawIssueInstant = l.IssueInstant.Format(time.RFC1123Z) + + return xml.MarshalIndent(l, "", " ") +} diff --git a/vendor/gopkg.in/cas.v2/memory_store.go b/vendor/gopkg.in/cas.v2/memory_store.go new file mode 100644 index 0000000000000000000000000000000000000000..21b90787ac503245389472f45b16e86ea90f498e --- /dev/null +++ b/vendor/gopkg.in/cas.v2/memory_store.go @@ -0,0 +1,60 @@ +package cas + +import ( + "sync" +) + +// MemoryStore implements the TicketStore interface storing ticket data in memory. +type MemoryStore struct { + mu sync.RWMutex + store map[string]*AuthenticationResponse +} + +// Read returns the AuthenticationResponse for a ticket +func (s *MemoryStore) Read(id string) (*AuthenticationResponse, error) { + s.mu.RLock() + + if s.store == nil { + s.mu.RUnlock() + return nil, ErrInvalidTicket + } + + t, ok := s.store[id] + s.mu.RUnlock() + + if !ok { + return nil, ErrInvalidTicket + } + + return t, nil +} + +// Write stores the AuthenticationResponse for a ticket +func (s *MemoryStore) Write(id string, ticket *AuthenticationResponse) error { + s.mu.Lock() + + if s.store == nil { + s.store = make(map[string]*AuthenticationResponse) + } + + s.store[id] = ticket + + s.mu.Unlock() + return nil +} + +// Delete removes the AuthenticationResponse for a ticket +func (s *MemoryStore) Delete(id string) error { + s.mu.Lock() + delete(s.store, id) + s.mu.Unlock() + return nil +} + +// Clear removes all ticket data +func (s *MemoryStore) Clear() error { + s.mu.Lock() + s.store = nil + s.mu.Unlock() + return nil +} diff --git a/vendor/gopkg.in/cas.v2/middleware.go b/vendor/gopkg.in/cas.v2/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1419e4488410d0128c72cb3209d908eb74ef8f22 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/middleware.go @@ -0,0 +1,30 @@ +package cas + +import ( + "net/http" + + "github.com/golang/glog" +) + +// Handler returns a standard http.HandlerFunc, which will check the authenticated status (redirect user go login if needed) +// If the user pass the authenticated check, it will call the h's ServeHTTP method +func (c *Client) Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if glog.V(2) { + glog.Infof("cas: handling %v request for %v", r.Method, r.URL) + } + + setClient(r, c) + + if !IsAuthenticated(r) { + RedirectToLogin(w, r) + return + } + + if r.URL.Path == "/logout" { + RedirectToLogout(w, r) + return + } + h.ServeHTTP(w, r) + }) +} diff --git a/vendor/gopkg.in/cas.v2/rest_client.go b/vendor/gopkg.in/cas.v2/rest_client.go new file mode 100644 index 0000000000000000000000000000000000000000..198dbf2c25d371c69615e7e993b491ab20cf5ce8 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/rest_client.go @@ -0,0 +1,178 @@ +package cas + +import ( + "net/url" + "net/http" + "github.com/golang/glog" + "fmt" + "path" + "io/ioutil" +) + +// https://apereo.github.io/cas/4.2.x/protocol/REST-Protocol.html + +// TicketGrantingTicket represents a SSO session for a user, also known as TGT +type TicketGrantingTicket string + +// ServiceTicket stands for the access granted by the CAS server to an application for a specific user, also known as ST +type ServiceTicket string + + +// RestOptions provide options for the RestClient +type RestOptions struct { + CasURL *url.URL + ServiceURL *url.URL + Client *http.Client + URLScheme URLScheme +} + +// RestClient uses the rest protocol provided by cas +type RestClient struct { + urlScheme URLScheme + serviceURL *url.URL + client *http.Client + stValidator *ServiceTicketValidator +} + +// NewRestClient creates a new client for the cas rest protocol with the provided options +func NewRestClient(options *RestOptions) *RestClient { + if glog.V(2) { + glog.Infof("cas: new rest client with options %v", options) + } + + var client *http.Client + if options.Client != nil { + client = options.Client + } else { + client = &http.Client{} + } + + var urlScheme URLScheme + if options.URLScheme != nil { + urlScheme = options.URLScheme + } else { + urlScheme = NewDefaultURLScheme(options.CasURL) + } + + return &RestClient{ + urlScheme: urlScheme, + serviceURL: options.ServiceURL, + client: client, + stValidator: NewServiceTicketValidator(client, options.CasURL), + } +} + +// Handle wraps a http.Handler to provide CAS Rest authentication for the handler. +func (c *RestClient) Handle(h http.Handler) http.Handler { + return &restClientHandler{ + c: c, + h: h, + } +} + +// HandleFunc wraps a function to provide CAS Rest authentication for the handler function. +func (c *RestClient) HandleFunc(h func(http.ResponseWriter, *http.Request)) http.Handler { + return c.Handle(http.HandlerFunc(h)) +} + +// RequestGrantingTicket returns a new TGT, if the username and password authentication was successful +func (c *RestClient) RequestGrantingTicket(username string, password string) (TicketGrantingTicket, error) { + // request: + // POST /cas/v1/tickets HTTP/1.0 + // username=battags&password=password&additionalParam1=paramvalue + + endpoint, err := c.urlScheme.RestGrantingTicket() + if err != nil { + return "", err + } + + values := url.Values{} + values.Set("username", username) + values.Set("password", password) + + resp, err := c.client.PostForm(endpoint.String(), values) + if err != nil { + return "", err + } + + // response: + // 201 Created + // Location: http://www.whatever.com/cas/v1/tickets/{TGT id} + + if resp.StatusCode != 201 { + return "", fmt.Errorf("ticket endoint returned status code %v", resp.StatusCode) + } + + tgt := path.Base(resp.Header.Get("Location")) + if tgt == "" { + return "", fmt.Errorf("does not return a valid location header") + } + + return TicketGrantingTicket(tgt), nil +} + +// RequestServiceTicket requests a service ticket with the TGT for the configured service url +func (c *RestClient) RequestServiceTicket(tgt TicketGrantingTicket) (ServiceTicket, error) { + // request: + // POST /cas/v1/tickets/{TGT id} HTTP/1.0 + // service={form encoded parameter for the service url} + endpoint, err := c.urlScheme.RestServiceTicket(string(tgt)) + if err != nil { + return "", err + } + + values := url.Values{} + values.Set("service", c.serviceURL.String()) + + resp, err := c.client.PostForm(endpoint.String(), values) + if err != nil { + return "", err + } + + // response: + // 200 OK + // ST-1-FFDFHDSJKHSDFJKSDHFJKRUEYREWUIFSD2132 + + if resp.StatusCode != 200 { + return "", fmt.Errorf("service ticket endoint returned status code %v", resp.StatusCode) + } + + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return ServiceTicket(data), nil +} + +// ValidateServiceTicket validates the service ticket and returns an AuthenticationResponse +func (c *RestClient) ValidateServiceTicket(st ServiceTicket) (*AuthenticationResponse, error) { + return c.stValidator.ValidateTicket(c.serviceURL, string(st)) +} + +// Logout destroys the given granting ticket +func (c *RestClient) Logout(tgt TicketGrantingTicket) error { + // DELETE /cas/v1/tickets/TGT-fdsjfsdfjkalfewrihfdhfaie HTTP/1.0 + endpoint, err := c.urlScheme.RestLogout(string(tgt)) + if err != nil { + return err + } + + req, err := http.NewRequest("DELETE", endpoint.String(), nil) + if err != nil { + return err + } + + resp, err := c.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != 200 && resp.StatusCode != 204 { + return fmt.Errorf("could not destroy granting ticket %v, server returned %v", tgt, resp.StatusCode) + } + + return nil +} \ No newline at end of file diff --git a/vendor/gopkg.in/cas.v2/rest_handler.go b/vendor/gopkg.in/cas.v2/rest_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..cf89892d75826c59cb70a7a7991a4acb653a0723 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/rest_handler.go @@ -0,0 +1,59 @@ +package cas + +import ( + "net/http" + + "github.com/golang/glog" +) + +// restClientHandler handles CAS REST Protocol over HTTP Basic Authentication +type restClientHandler struct { + c *RestClient + h http.Handler +} + +// ServeHTTP handles HTTP requests, processes HTTP Basic Authentication over CAS Rest api +// and passes requests up to its child http.Handler. +func (ch *restClientHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if glog.V(2) { + glog.Infof("cas: handling %v request for %v", r.Method, r.URL) + } + + username, password, ok := r.BasicAuth() + if !ok { + w.Header().Set("WWW-Authenticate", "Basic realm=\"CAS Protected Area\"") + w.WriteHeader(401) + return + } + + // TODO we should implement a short cache to avoid hitting cas server on every request + // the cache could use the authorization header as key and the authenticationResponse as value + + success, err := ch.authenticate(username, password) + if err != nil { + if glog.V(1) { + glog.Infof("cas: rest authentication failed %v", err) + } + w.Header().Set("WWW-Authenticate", "Basic realm=\"CAS Protected Area\"") + w.WriteHeader(401) + return + } + + setAuthenticationResponse(r, success) + ch.h.ServeHTTP(w, r) + return +} + +func (ch *restClientHandler) authenticate(username string, password string) (*AuthenticationResponse, error) { + tgt, err := ch.c.RequestGrantingTicket(username, password) + if err != nil { + return nil, err + } + + st, err := ch.c.RequestServiceTicket(tgt) + if err != nil { + return nil, err + } + + return ch.c.ValidateServiceTicket(st) +} diff --git a/vendor/gopkg.in/cas.v2/sanitise.go b/vendor/gopkg.in/cas.v2/sanitise.go new file mode 100644 index 0000000000000000000000000000000000000000..a2cc17cbaaf31d6170957366b3ca23b40683b84d --- /dev/null +++ b/vendor/gopkg.in/cas.v2/sanitise.go @@ -0,0 +1,28 @@ +package cas + +import ( + "net/url" +) + +var ( + urlCleanParameters = []string{"gateway", "renew", "service", "ticket"} +) + +// sanitisedURL cleans a URL of CAS specific parameters +func sanitisedURL(unclean *url.URL) *url.URL { + // Shouldn't be any errors parsing an existing *url.URL + u, _ := url.Parse(unclean.String()) + q := u.Query() + + for _, param := range urlCleanParameters { + q.Del(param) + } + + u.RawQuery = q.Encode() + return u +} + +// sanitisedURLString cleans a URL and returns its string value +func sanitisedURLString(unclean *url.URL) string { + return sanitisedURL(unclean).String() +} diff --git a/vendor/gopkg.in/cas.v2/service_response.go b/vendor/gopkg.in/cas.v2/service_response.go new file mode 100644 index 0000000000000000000000000000000000000000..e3a76e2ef681bc3d8f19e745980594c4aab67258 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/service_response.go @@ -0,0 +1,177 @@ +package cas + +import ( + "encoding/xml" + "fmt" + "reflect" + "strings" + "time" + + "github.com/golang/glog" + "gopkg.in/yaml.v2" +) + +// AuthenticationError Code values +const ( + INVALID_REQUEST = "INVALID_REQUEST" + INVALID_TICKET_SPEC = "INVALID_TICKET_SPEC" + UNAUTHORIZED_SERVICE = "UNAUTHORIZED_SERVICE" + UNAUTHORIZED_SERVICE_PROXY = "UNAUTHORIZED_SERVICE_PROXY" + INVALID_PROXY_CALLBACK = "INVALID_PROXY_CALLBACK" + INVALID_TICKET = "INVALID_TICKET" + INVALID_SERVICE = "INVALID_SERVICE" + INTERNAL_ERROR = "INTERNAL_ERROR" +) + +// AuthenticationError represents a CAS AuthenticationFailure response +type AuthenticationError struct { + Code string + Message string +} + +// AuthenticationError provides a differentiator for casting. +func (e AuthenticationError) AuthenticationError() bool { + return true +} + +// Error returns the AuthenticationError as a string +func (e AuthenticationError) Error() string { + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +// AuthenticationResponse captures authenticated user information +type AuthenticationResponse struct { + User string // Users login name + ProxyGrantingTicket string // Proxy Granting Ticket + Proxies []string // List of proxies + AuthenticationDate time.Time // Time at which authentication was performed + IsNewLogin bool // Whether new authentication was used to grant the service ticket + IsRememberedLogin bool // Whether a long term token was used to grant the service ticket + MemberOf []string // List of groups which the user is a member of + Attributes UserAttributes // Additional information about the user +} + +// UserAttributes represents additional data about the user +type UserAttributes map[string][]string + +// Get retrieves an attribute by name. +// +// Attributes are stored in arrays. Get will only return the first element. +func (a UserAttributes) Get(name string) string { + if v, ok := a[name]; ok { + return v[0] + } + + return "" +} + +// Add appends a new attribute. +func (a UserAttributes) Add(name, value string) { + a[name] = append(a[name], value) +} + +// ParseServiceResponse returns a successful response or an error +func ParseServiceResponse(data []byte) (*AuthenticationResponse, error) { + var x xmlServiceResponse + + if err := xml.Unmarshal(data, &x); err != nil { + return nil, err + } + + if x.Failure != nil { + msg := strings.TrimSpace(x.Failure.Message) + err := &AuthenticationError{Code: x.Failure.Code, Message: msg} + return nil, err + } + + r := &AuthenticationResponse{ + User: x.Success.User, + ProxyGrantingTicket: x.Success.ProxyGrantingTicket, + Attributes: make(UserAttributes), + } + + if p := x.Success.Proxies; p != nil { + r.Proxies = p.Proxies + } + + if a := x.Success.Attributes; a != nil { + r.AuthenticationDate = a.AuthenticationDate + r.IsRememberedLogin = a.LongTermAuthenticationRequestTokenUsed + r.IsNewLogin = a.IsFromNewLogin + r.MemberOf = a.MemberOf + + if a.UserAttributes != nil { + for _, ua := range a.UserAttributes.Attributes { + if ua.Name == "" { + continue + } + + r.Attributes.Add(ua.Name, strings.TrimSpace(ua.Value)) + } + + for _, ea := range a.UserAttributes.AnyAttributes { + r.Attributes.Add(ea.XMLName.Local, strings.TrimSpace(ea.Value)) + } + } + + if a.ExtraAttributes != nil { + for _, ea := range a.ExtraAttributes { + r.Attributes.Add(ea.XMLName.Local, strings.TrimSpace(ea.Value)) + } + } + } + + for _, ea := range x.Success.ExtraAttributes { + addRubycasAttribute(r.Attributes, ea.XMLName.Local, strings.TrimSpace(ea.Value)) + } + + return r, nil +} + +// addRubycasAttribute handles RubyCAS style additional attributes. +func addRubycasAttribute(attributes UserAttributes, key, value string) { + if !strings.HasPrefix(value, "---") { + attributes.Add(key, value) + return + } + + if value == "--- true" { + attributes.Add(key, "true") + return + } + + if value == "--- false" { + attributes.Add(key, "false") + return + } + + var decoded interface{} + if err := yaml.Unmarshal([]byte(value), &decoded); err != nil { + attributes.Add(key, err.Error()) + return + } + + switch reflect.TypeOf(decoded).Kind() { + case reflect.Slice: + s := reflect.ValueOf(decoded) + + for i := 0; i < s.Len(); i++ { + e := s.Index(i).Interface() + + switch reflect.TypeOf(e).Kind() { + case reflect.String: + attributes.Add(key, e.(string)) + } + } + case reflect.String: + s := reflect.ValueOf(decoded).Interface() + attributes.Add(key, s.(string)) + default: + if glog.V(2) { + kind := reflect.TypeOf(decoded).Kind() + glog.Warningf("cas: service response: unable to parse %v value: %#v (kind: %v)", key, decoded, kind) + } + } + + return +} diff --git a/vendor/gopkg.in/cas.v2/service_validate.go b/vendor/gopkg.in/cas.v2/service_validate.go new file mode 100644 index 0000000000000000000000000000000000000000..07950a81ec61be265946440e34db2851a05004c1 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/service_validate.go @@ -0,0 +1,182 @@ +package cas + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path" + + "github.com/golang/glog" +) + +func NewServiceTicketValidator(client *http.Client, casUrl *url.URL) *ServiceTicketValidator { + return &ServiceTicketValidator{ + client: client, + casUrl: casUrl, + } +} + +// ServiceTicketValidator is responsible for the validation of a service ticket +type ServiceTicketValidator struct { + client *http.Client + casUrl *url.URL +} + +// ValidateTicket validates the service ticket for the given server. The method will try to use the service validate +// endpoint of the cas >= 2 protocol, if the service validate endpoint not available, the function will use the cas 1 +// validate endpoint. +func (validator *ServiceTicketValidator) ValidateTicket(serviceUrl *url.URL, ticket string) (*AuthenticationResponse, error) { + if glog.V(2) { + glog.Infof("Validating ticket %v for service %v", ticket, serviceUrl) + } + + u, err := validator.ServiceValidateUrl(serviceUrl, ticket) + if err != nil { + return nil, err + } + + r, err := http.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + r.Header.Add("User-Agent", "Golang CAS client gopkg.in/cas") + + if glog.V(2) { + glog.Infof("Attempting ticket validation with %v", r.URL) + } + + resp, err := validator.client.Do(r) + if err != nil { + return nil, err + } + + if glog.V(2) { + glog.Infof("Request %v %v returned %v", + r.Method, r.URL, + resp.Status) + } + + if resp.StatusCode == http.StatusNotFound { + return validator.validateTicketCas1(serviceUrl, ticket) + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("cas: validate ticket: %v", string(body)) + } + + if glog.V(2) { + glog.Infof("Received authentication response\n%v", string(body)) + } + + success, err := ParseServiceResponse(body) + if err != nil { + return nil, err + } + + if glog.V(2) { + glog.Infof("Parsed ServiceResponse: %#v", success) + } + + return success, nil +} + +// ServiceValidateUrl creates the service validation url for the cas >= 2 protocol. +// TODO the function is only exposed, because of the clients ServiceValidateUrl function +func (validator *ServiceTicketValidator) ServiceValidateUrl(serviceUrl *url.URL, ticket string) (string, error) { + u, err := validator.casUrl.Parse(path.Join(validator.casUrl.Path, "serviceValidate")) + if err != nil { + return "", err + } + + q := u.Query() + q.Add("service", sanitisedURLString(serviceUrl)) + q.Add("ticket", ticket) + u.RawQuery = q.Encode() + + return u.String(), nil +} + +func (validator *ServiceTicketValidator) validateTicketCas1(serviceUrl *url.URL, ticket string) (*AuthenticationResponse, error) { + u, err := validator.ValidateUrl(serviceUrl, ticket) + if err != nil { + return nil, err + } + + r, err := http.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + r.Header.Add("User-Agent", "Golang CAS client gopkg.in/cas") + + if glog.V(2) { + glog.Infof("Attempting ticket validation with %v", r.URL) + } + + resp, err := validator.client.Do(r) + if err != nil { + return nil, err + } + + if glog.V(2) { + glog.Infof("Request %v %v returned %v", + r.Method, r.URL, + resp.Status) + } + + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + + if err != nil { + return nil, err + } + + body := string(data) + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("cas: validate ticket: %v", body) + } + + if glog.V(2) { + glog.Infof("Received authentication response\n%v", body) + } + + if body == "no\n\n" { + return nil, nil // not logged in + } + + success := &AuthenticationResponse{ + User: body[4 : len(body)-1], + } + + if glog.V(2) { + glog.Infof("Parsed ServiceResponse: %#v", success) + } + + return success, nil +} + +// ValidateUrl creates the validation url for the cas >= 1 protocol. +// TODO the function is only exposed, because of the clients ValidateUrl function +func (validator *ServiceTicketValidator) ValidateUrl(serviceUrl *url.URL, ticket string) (string, error) { + u, err := validator.casUrl.Parse(path.Join(validator.casUrl.Path, "validate")) + if err != nil { + return "", err + } + + q := u.Query() + q.Add("service", sanitisedURLString(serviceUrl)) + q.Add("ticket", ticket) + u.RawQuery = q.Encode() + + return u.String(), nil +} diff --git a/vendor/gopkg.in/cas.v2/session_store.go b/vendor/gopkg.in/cas.v2/session_store.go new file mode 100644 index 0000000000000000000000000000000000000000..32901fbe690f6c80e0633a8969d1319537c811ce --- /dev/null +++ b/vendor/gopkg.in/cas.v2/session_store.go @@ -0,0 +1,52 @@ +package cas + +import "sync" + +// SessionStore store the session's ticket +// SessionID is retrived from cookies +type SessionStore interface { + // Get the ticket with the session id + Get(sessionID string) (string, bool) + + // Set the session with a ticket + Set(sessionID, ticket string) error + + // Delete the session + Delete(sessionID string) error +} + +// NewMemorySessionStore create a default SessionStore that uses memory +func NewMemorySessionStore() SessionStore { + return &memorySessionStore{ + sessions: make(map[string]string), + } +} + +type memorySessionStore struct { + mu sync.RWMutex + sessions map[string]string +} + +func (m *memorySessionStore) Get(sessionID string) (string, bool) { + m.mu.RLock() + ticket, ok := m.sessions[sessionID] + m.mu.RUnlock() + + return ticket, ok +} + +func (m *memorySessionStore) Set(sessionID, ticket string) error { + m.mu.Lock() + m.sessions[sessionID] = ticket + m.mu.Unlock() + + return nil +} + +func (m *memorySessionStore) Delete(sessionID string) error { + m.mu.Lock() + delete(m.sessions, sessionID) + m.mu.Unlock() + + return nil +} diff --git a/vendor/gopkg.in/cas.v2/ticket_store.go b/vendor/gopkg.in/cas.v2/ticket_store.go new file mode 100644 index 0000000000000000000000000000000000000000..42ae3cfb3e0a4bb554d3a209eb47ff2c29b35143 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/ticket_store.go @@ -0,0 +1,27 @@ +package cas + +import ( + "errors" +) + +// TicketStore errors +var ( + // Given Ticket is not associated with an AuthenticationResponse + ErrInvalidTicket = errors.New("cas: ticket store: invalid ticket") +) + +// TicketStore provides an interface for storing and retrieving service +// ticket data. +type TicketStore interface { + // Read returns the AuthenticationResponse data associated with a ticket identifier. + Read(id string) (*AuthenticationResponse, error) + + // Write stores the AuthenticationResponse data received from a ticket validation. + Write(id string, ticket *AuthenticationResponse) error + + // Delete removes the AuthenticationResponse data associated with a ticket identifier. + Delete(id string) error + + // Clear removes all of the AuthenticationResponse data from the store. + Clear() error +} diff --git a/vendor/gopkg.in/cas.v2/url_scheme.go b/vendor/gopkg.in/cas.v2/url_scheme.go new file mode 100644 index 0000000000000000000000000000000000000000..83820c5b1ea43eb5137408ac0d428d6b13b8fe62 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/url_scheme.go @@ -0,0 +1,79 @@ +package cas + +import ( + "net/url" + "path" +) + +// URLScheme creates the url which are required to handle the cas protocol. +type URLScheme interface { + Login() (*url.URL, error) + Logout() (*url.URL, error) + Validate() (*url.URL, error) + ServiceValidate() (*url.URL, error) + RestGrantingTicket() (*url.URL, error) + RestServiceTicket(tgt string) (*url.URL, error) + RestLogout(tgt string) (*url.URL, error) +} + +// NewDefaultURLScheme creates a URLScheme which uses the cas default urls +func NewDefaultURLScheme(base *url.URL) *DefaultURLScheme { + return &DefaultURLScheme{ + base: base, + LoginPath: "login", + LogoutPath: "logout", + ValidatePath: "validate", + ServiceValidatePath: "serviceValidate", + RestEndpoint: path.Join("v1", "tickets"), + } +} + +// DefaultURLScheme is a configurable URLScheme. Use NewDefaultURLScheme to create DefaultURLScheme with the default cas +// urls. +type DefaultURLScheme struct { + base *url.URL + LoginPath string + LogoutPath string + ValidatePath string + ServiceValidatePath string + RestEndpoint string +} + +// Login returns the url for the cas login page +func (scheme *DefaultURLScheme) Login() (*url.URL, error) { + return scheme.createURL(scheme.LoginPath) +} + +// Logout returns the url for the cas logut page +func (scheme *DefaultURLScheme) Logout() (*url.URL, error) { + return scheme.createURL(scheme.LogoutPath) +} + +// Validate returns the url for the request validation endpoint +func (scheme *DefaultURLScheme) Validate() (*url.URL, error) { + return scheme.createURL(scheme.ValidatePath) +} + +// ServiceValidate returns the url for the service validation endpoint +func (scheme *DefaultURLScheme) ServiceValidate() (*url.URL, error) { + return scheme.createURL(scheme.ServiceValidatePath) +} + +// RestGrantingTicket returns the url for requesting an granting ticket via rest api +func (scheme *DefaultURLScheme) RestGrantingTicket() (*url.URL, error) { + return scheme.createURL(scheme.RestEndpoint) +} + +// RestServiceTicket returns the url for requesting an service ticket via rest api +func (scheme *DefaultURLScheme) RestServiceTicket(tgt string) (*url.URL, error) { + return scheme.createURL(path.Join(scheme.RestEndpoint, tgt)) +} + +// RestLogout returns the url for destroying an granting ticket via rest api +func (scheme *DefaultURLScheme) RestLogout(tgt string) (*url.URL, error) { + return scheme.createURL(path.Join(scheme.RestEndpoint, tgt)) +} + +func (scheme *DefaultURLScheme) createURL(urlPath string) (*url.URL, error) { + return scheme.base.Parse(path.Join(scheme.base.Path, urlPath)) +} diff --git a/vendor/gopkg.in/cas.v2/xml_service_response.go b/vendor/gopkg.in/cas.v2/xml_service_response.go new file mode 100644 index 0000000000000000000000000000000000000000..5a5536915735881e85ef9df9ad852714806e5e76 --- /dev/null +++ b/vendor/gopkg.in/cas.v2/xml_service_response.go @@ -0,0 +1,95 @@ +package cas + +import ( + "encoding/xml" + "time" +) + +type xmlServiceResponse struct { + XMLName xml.Name `xml:"http://www.yale.edu/tp/cas serviceResponse"` + + Failure *xmlAuthenticationFailure + Success *xmlAuthenticationSuccess +} + +type xmlAuthenticationFailure struct { + XMLName xml.Name `xml:"authenticationFailure"` + Code string `xml:"code,attr"` + Message string `xml:",innerxml"` +} + +type xmlAuthenticationSuccess struct { + XMLName xml.Name `xml:"authenticationSuccess"` + User string `xml:"user"` + ProxyGrantingTicket string `xml:"proxyGrantingTicket,omitempty"` + Proxies *xmlProxies `xml:"proxies"` + Attributes *xmlAttributes `xml:"attributes"` + ExtraAttributes []*xmlAnyAttribute `xml:",any"` +} + +type xmlProxies struct { + XMLName xml.Name `xml:"proxies"` + Proxies []string `xml:"proxy"` +} + +func (p *xmlProxies) AddProxy(proxy string) { + p.Proxies = append(p.Proxies, proxy) +} + +type xmlAttributes struct { + XMLName xml.Name `xml:"attributes"` + AuthenticationDate time.Time `xml:"authenticationDate"` + LongTermAuthenticationRequestTokenUsed bool `xml:"longTermAuthenticationRequestTokenUsed"` + IsFromNewLogin bool `xml:"isFromNewLogin"` + MemberOf []string `xml:"memberOf"` + UserAttributes *xmlUserAttributes + ExtraAttributes []*xmlAnyAttribute `xml:",any"` +} + +type xmlUserAttributes struct { + XMLName xml.Name `xml:"userAttributes"` + Attributes []*xmlNamedAttribute `xml:"attribute"` + AnyAttributes []*xmlAnyAttribute `xml:",any"` +} + +type xmlNamedAttribute struct { + XMLName xml.Name `xml:"attribute"` + Name string `xml:"name,attr,omitempty"` + Value string `xml:",innerxml"` +} + +type xmlAnyAttribute struct { + XMLName xml.Name + Value string `xml:",chardata"` +} + +func (xsr *xmlServiceResponse) marshalXML(indent int) ([]byte, error) { + if indent == 0 { + return xml.Marshal(xsr) + } + + indentStr := "" + for i := 0; i < indent; i++ { + indentStr += " " + } + + return xml.MarshalIndent(xsr, "", indentStr) +} + +func failureServiceResponse(code, message string) *xmlServiceResponse { + return &xmlServiceResponse{ + Failure: &xmlAuthenticationFailure{ + Code: code, + Message: message, + }, + } +} + +func successServiceResponse(username, pgt string) *xmlServiceResponse { + return &xmlServiceResponse{ + Success: &xmlAuthenticationSuccess{ + User: username, + ProxyGrantingTicket: pgt, + }, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 807904705dc963ee4127ab5f1b58a6a09b540fcd..66b8ae0fb7b2855f0f3f44007fb12e003bb72fd3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -851,6 +851,8 @@ google.golang.org/protobuf/types/known/wrapperspb gopkg.in/alecthomas/kingpin.v2 # gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d => gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d gopkg.in/asn1-ber.v1 +# gopkg.in/cas.v2 v2.2.0 => gopkg.in/cas.v2 v2.2.0 +gopkg.in/cas.v2 # gopkg.in/inf.v0 v0.9.1 => gopkg.in/inf.v0 v0.9.1 gopkg.in/inf.v0 # gopkg.in/natefinch/lumberjack.v2 v2.0.0 => gopkg.in/natefinch/lumberjack.v2 v2.0.0