未验证 提交 470a87ef 编写于 作者: K KubeSphere CI Bot 提交者: GitHub

Merge pull request #3352 from wansir/feature-cas

support CAS identity provider
......@@ -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
......
......@@ -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=
......
/*
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
}
......@@ -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"
......
......@@ -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)
......
# 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
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.
# 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.
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)
}
/*
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
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
)
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=
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")
}
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
}
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, "", " ")
}
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
}
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)
})
}
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
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)
}
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()
}
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
}
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
}
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
}
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
}
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))
}
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,
},
}
}
......@@ -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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册