├── LICENSE ├── README.md ├── admin └── services.go ├── config └── config.toml.example ├── main.go ├── security ├── ticket.go └── user.go ├── server.go ├── spec ├── v1.go ├── v1_test.go ├── v2.go ├── v3.go └── xml │ ├── v2Proxy.go │ └── v2Validation.go ├── storage ├── interface.go └── memory.go ├── test └── login.go ├── tools ├── config.go └── log.go ├── types ├── cas_error.go ├── config.go ├── cors.go ├── server.go ├── service.go ├── ticket.go └── user.go └── validators ├── format.go ├── proxy.go ├── request.go ├── service.go └── ticket.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matthew Valimaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cas-server 2 | Implementation of [JASIG CAS protocol] in Go lang. Supports all protocol versions (v1, v2 and v3). 3 | 4 | ## Configuration 5 | You can configure certain aspects of `cas-server` with command line arguments but majority of the configuration will 6 | require a [TOML] formatted configuration file. For an example configuration please see `config/config.toml.example`. 7 | 8 | ### Options 9 | |Section | Key | Type | Example | Notes | 10 | |--------| --- |:-------:| :----------------------- | :------------------------ | 11 | |Services| ID | [array] | ["https://google.com.*"] | Go [regexp] is supported. | 12 | 13 | ## Running 14 | `cas-server -config /etc/cas-server/config.toml` 15 | 16 | ## Features 17 | * Storage 18 | * In memory 19 | 20 | ## Project goals 21 | * Not to replace [JASIG CAS] but to offer competition with reduced feature set. 22 | * Easier to develop for and easier to work with than [JASIG CAS]. 23 | * Minimal dependencies on 3rd party Go libraries. 24 | * This Readme has to be enough to get `cas-server` running correctly and securely. 25 | 26 | [JASIG CAS protocol]: https://jasig.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html 27 | [JASIG CAS]: https://github.com/Jasig/cas 28 | [TOML]: https://github.com/toml-lang/toml 29 | [array]: https://github.com/toml-lang/toml#user-content-array 30 | [regexp]: https://golang.org/pkg/regexp/ 31 | -------------------------------------------------------------------------------- /admin/services.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | "encoding/json" 6 | 7 | "github.com/matthewvalimaki/cas-server/tools" 8 | "github.com/matthewvalimaki/cas-server/types" 9 | ) 10 | 11 | var ( 12 | config *types.Config 13 | ) 14 | 15 | // SupportServices adds support for services related admin feature 16 | func SupportServices(cfg *types.Config) { 17 | config = cfg 18 | } 19 | 20 | // HandleServices handles `/admin/services` request 21 | func HandleServices(w http.ResponseWriter, r *http.Request) { 22 | tools.LogAdmin("Service listing requested") 23 | 24 | js, err := json.Marshal(config) 25 | if err != nil { 26 | http.Error(w, err.Error(), http.StatusInternalServerError) 27 | return 28 | } 29 | 30 | w.Header().Set("Content-Type", "application/json;charset=UTF-8") 31 | 32 | w.Write(js) 33 | } -------------------------------------------------------------------------------- /config/config.toml.example: -------------------------------------------------------------------------------- 1 | [Cors] 2 | Origin = ["*"] 3 | Methods = ["GET"] 4 | 5 | [Servers] 6 | [Servers.External] 7 | Port = 10000 8 | SSL = false 9 | CACert = "/etc/ssl/certs/external-bundle-ssl.ca-bundle" 10 | CAKey = "/etc/ssl/private/external-key-ssl.key" 11 | 12 | [Servers.Internal] 13 | Port = 10001 14 | SSL = false 15 | CACert = "/etc/ssl/certs/internal-bundle-ssl.ca-bundle" 16 | CAKey = "/etc/ssl/private/internal-key-ssl.key" 17 | 18 | [Services] 19 | [Services.Google] 20 | ID = ["https://google.com"] 21 | ProxyServices = ["Yahoo"] 22 | 23 | [Services.Yahoo] 24 | ID = ["https://yahoo.com"] 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | "runtime" 10 | 11 | "github.com/matthewvalimaki/cas-server/admin" 12 | "github.com/matthewvalimaki/cas-server/tools" 13 | "github.com/matthewvalimaki/cas-server/spec" 14 | "github.com/matthewvalimaki/cas-server/test" 15 | "github.com/matthewvalimaki/cas-server/storage" 16 | ) 17 | 18 | func main() { 19 | // get configuration location 20 | var configPath string 21 | flag.StringVar(&configPath, "config", "", "Path to config file") 22 | flag.Parse() 23 | 24 | if configPath == "" { 25 | tools.Log("Command line argument `-config` must be set") 26 | return 27 | } 28 | 29 | config, err := tools.NewConfig(configPath) 30 | 31 | if err != nil { 32 | tools.LogError(err.Error()) 33 | return 34 | } 35 | 36 | admin.SupportServices(config) 37 | 38 | storage := storage.NewMemoryStorage() 39 | 40 | spec.SupportV1(storage, config) 41 | 42 | test.SupportTest() 43 | 44 | startServers(config) 45 | 46 | // keep server running until interrupt 47 | c := make(chan os.Signal, 1) 48 | signal.Notify(c, os.Interrupt) 49 | signal.Notify(c, syscall.SIGTERM) 50 | go func() { 51 | <-c 52 | os.Exit(1) 53 | }() 54 | 55 | for { 56 | runtime.Gosched() 57 | time.Sleep(5 * time.Second) 58 | } 59 | } -------------------------------------------------------------------------------- /security/ticket.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | "github.com/matthewvalimaki/cas-server/storage" 8 | "github.com/matthewvalimaki/cas-server/tools" 9 | "github.com/matthewvalimaki/cas-server/types" 10 | ) 11 | 12 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" 13 | 14 | // createNewTicket creates a new ticket 15 | func createNewTicket(ticketType string) types.Ticket { 16 | c := 100 17 | b := make([]byte, c) 18 | for i := range b { 19 | b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] 20 | } 21 | 22 | return types.Ticket{ 23 | Ticket: ticketType + "-" + string(b), 24 | Type: ticketType, 25 | } 26 | } 27 | 28 | // CreateNewProxyGrantingTicket creates new PGT 29 | func CreateNewProxyGrantingTicket() (*types.Ticket, *types.CasError) { 30 | ticket := createNewTicket("PGT") 31 | 32 | return &ticket, nil 33 | } 34 | 35 | // CreateNewProxyGrantingTicketIOU creates new proxy granting ticket for a service 36 | // see: https://jasig.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#proxy-granting-ticket-iou 37 | func CreateNewProxyGrantingTicketIOU() (*types.Ticket, *types.CasError) { 38 | ticket := createNewTicket("PGTIOU") 39 | 40 | return &ticket, nil 41 | } 42 | 43 | // CreateNewProxyTicket creates new Proxy Ticket (PT) 44 | func CreateNewProxyTicket() (*types.Ticket, *types.CasError) { 45 | ticket := createNewTicket("PT") 46 | 47 | return &ticket, nil 48 | } 49 | 50 | // CreateNewServiceTicket creates a new Service Ticket 51 | // see: https://jasig.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#service-ticket 52 | func CreateNewServiceTicket(strg storage.IStorage, serviceID string) (*types.Ticket, error) { 53 | ticket := createNewTicket("ST") 54 | ticket.Service = serviceID 55 | 56 | strg.SaveTicket(&ticket) 57 | 58 | tools.LogST(&ticket, "ticket created") 59 | 60 | return &ticket, nil 61 | } 62 | 63 | // ValidateServiceTicket checks if given Service Ticket exists and is valid 64 | func ValidateServiceTicket(strg storage.IStorage, t *types.Ticket) *types.CasError { 65 | tools.LogST(t, t.Type+" validation requested") 66 | 67 | ticket := strg.DoesTicketExist(t.Ticket) 68 | if ticket != nil { 69 | tools.LogST(ticket, ticket.Type+" validation succeeded (ticket was found)") 70 | 71 | strg.DeleteTicket(ticket.Ticket) 72 | 73 | tools.LogST(ticket, ticket.Type+" deleted") 74 | 75 | return nil 76 | } 77 | 78 | tools.LogST(t, ticket.Type+" validation failed (ticket was not found)") 79 | 80 | return &types.CasError{Error: fmt.Errorf("The ticket `%s` is invalid.", t.Ticket), CasErrorCode: types.CAS_ERROR_CODE_INVALID_TICKET} 81 | } 82 | 83 | // ValidateProxyGrantingTicket checks if given PGT exists 84 | func ValidateProxyGrantingTicket(strg storage.IStorage, t *types.Ticket) *types.CasError { 85 | tools.LogPGT(t, "PGT validation requested") 86 | 87 | ticket := strg.DoesTicketExist(t.Ticket) 88 | if ticket != nil { 89 | tools.LogPGT(ticket, "PGT validation succeeded (ticket was found)") 90 | 91 | // only deleted PGT ticket if its old 92 | if ticket.Old() { 93 | strg.DeleteTicket(ticket.Ticket) 94 | tools.LogPGT(ticket, "PGT deleted") 95 | } 96 | 97 | return nil 98 | } 99 | 100 | tools.LogPGT(t, "PGT validation failed (ticket was not found)") 101 | 102 | return &types.CasError{Error: fmt.Errorf("The ticket `%s` is invalid.", t.Ticket), CasErrorCode: types.CAS_ERROR_CODE_INVALID_TICKET} 103 | } 104 | -------------------------------------------------------------------------------- /security/user.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // IsRemoteAddrBanned checks if address is banned 8 | func IsRemoteAddrBanned(remoteAddr string) bool { 9 | return true 10 | } 11 | 12 | // ProcessFailedLogin processes failed login attempt 13 | func ProcessFailedLogin(remoteAddr string) { 14 | _, _, err := net.SplitHostPort(remoteAddr) 15 | if err != nil { 16 | return 17 | } 18 | } -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/matthewvalimaki/cas-server/types" 8 | "github.com/matthewvalimaki/cas-server/tools" 9 | "github.com/matthewvalimaki/cas-server/spec" 10 | "github.com/matthewvalimaki/cas-server/admin" 11 | ) 12 | 13 | var ( 14 | servers []*types.Server 15 | ) 16 | 17 | // StartServers starts listening per server configuration 18 | func startServers(config *types.Config) error { 19 | mux := getCorsMux() 20 | 21 | for _, server := range config.Servers { 22 | if server.SSL { 23 | go startServer(mux, config.Cors, server.PortToString(), server.CACert, server.CAKey) 24 | 25 | tools.Log(fmt.Sprintf("cas-server is now started up and binding to all interfaces with port `%d` using SSL", server.Port)) 26 | } else { 27 | go startServer(mux, config.Cors, server.PortToString(), "", "") 28 | 29 | tools.Log(fmt.Sprintf("cas-server is now started up and binding to all interfaces with port `%d`", server.Port)) 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func startServer(mux *http.ServeMux, cors *types.Cors, port string, cacert string, cakey string) { 37 | var err error 38 | 39 | if cacert != "" { 40 | err = http.ListenAndServeTLS(":" + port, cacert, cakey, corsHandler(mux, cors)) 41 | } else { 42 | err = http.ListenAndServe(":" + port, corsHandler(mux, cors)) 43 | } 44 | 45 | if err != nil { 46 | tools.LogError(err.Error()) 47 | return 48 | } 49 | } 50 | 51 | func getCorsMux() *http.ServeMux { 52 | mux := http.NewServeMux() 53 | mux.HandleFunc("/admin/services", admin.HandleServices) 54 | 55 | // v1 56 | mux.HandleFunc("/login", spec.HandleLogin) 57 | mux.HandleFunc("/validate", spec.HandleValidate) 58 | 59 | // v2 60 | mux.HandleFunc("/serviceValidate", spec.HandleValidateV2) 61 | mux.HandleFunc("/proxyValidate", spec.HandleValidateV2) 62 | mux.HandleFunc("/proxy", spec.HandleProxyV2) 63 | 64 | // v3 65 | mux.HandleFunc("/p3/serviceValidate", spec.HandleValidateV2) 66 | mux.HandleFunc("/p3/proxyValidate", spec.HandleValidateV2) 67 | 68 | return mux 69 | } 70 | 71 | func corsHandler(next http.Handler, cors *types.Cors) http.Handler { 72 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 73 | if cors != nil { 74 | if cors.Origin != nil { 75 | w.Header().Set("Access-Control-Allow-Origin", cors.OriginToString()) 76 | } 77 | 78 | if cors.Methods != nil { 79 | w.Header().Set("Access-Control-Allow-Methods", cors.MethodsToString()) 80 | } 81 | 82 | if cors.Credentials != false { 83 | w.Header().Set("Access-Control-Allow-Credentials", "true") 84 | } else { 85 | w.Header().Set("Access-Control-Allow-Credentials", "false") 86 | } 87 | } 88 | 89 | // If this was preflight options request let's write empty ok response and return 90 | if r.Method == "OPTIONS" { 91 | w.WriteHeader(http.StatusOK) 92 | w.Write(nil) 93 | return 94 | } 95 | 96 | next.ServeHTTP(w, r) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /spec/v1.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "fmt" 5 | "errors" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/matthewvalimaki/cas-server/tools" 10 | "github.com/matthewvalimaki/cas-server/validators" 11 | "github.com/matthewvalimaki/cas-server/types" 12 | "github.com/matthewvalimaki/cas-server/storage" 13 | "github.com/matthewvalimaki/cas-server/security" 14 | ) 15 | 16 | var ( 17 | specTemplatePath = "spec/tmpl/" 18 | 19 | strg storage.IStorage 20 | config *types.Config 21 | ) 22 | 23 | // SupportV1 enables spec v1 support 24 | func SupportV1(strgObject storage.IStorage, cfg *types.Config) { 25 | strg = strgObject 26 | config = cfg 27 | } 28 | 29 | // HandleLogin handles calls to login 30 | func HandleLogin(w http.ResponseWriter, r *http.Request) { 31 | if config == nil { 32 | err := &types.CasError{Error: errors.New("`config` has not been set"), CasErrorCode: types.CAS_ERROR_CODE_INTERNAL_ERROR} 33 | loginResponse(err, nil, w, r) 34 | tools.LogRequest(r, err.Error.Error()) 35 | return 36 | } 37 | 38 | if strg == nil { 39 | err := &types.CasError{Error: errors.New("`strg` has not been set"), CasErrorCode: types.CAS_ERROR_CODE_INTERNAL_ERROR} 40 | loginResponse(err, nil, w, r) 41 | tools.LogRequest(r, err.Error.Error()) 42 | return 43 | } 44 | 45 | err := validators.ValidateRequest(r) 46 | if err != nil { 47 | loginResponse(err, nil, w, r) 48 | tools.LogRequest(r, err.Error.Error()) 49 | return 50 | } 51 | 52 | service := r.URL.Query().Get("service") 53 | err = validators.ValidateService(service, config) 54 | if err != nil { 55 | loginResponse(err, nil, w, r) 56 | tools.LogService(service, err.Error.Error()) 57 | security.ProcessFailedLogin(r.RemoteAddr) 58 | return 59 | } 60 | 61 | var serviceTicket, _ = security.CreateNewServiceTicket(strg, service) 62 | 63 | loginResponse(nil, serviceTicket, w, r) 64 | } 65 | 66 | func loginResponse(casError *types.CasError, ticket *types.Ticket, w http.ResponseWriter, r *http.Request) { 67 | if casError != nil { 68 | if casError.CasErrorCode == types.CAS_ERROR_CODE_INTERNAL_ERROR { 69 | w.WriteHeader(http.StatusInternalServerError) 70 | } 71 | 72 | fmt.Fprintf(w, casError.Error.Error()) 73 | return 74 | } 75 | 76 | if strings.Contains(ticket.Service, "?") { 77 | http.Redirect(w, r, ticket.Service + "&ticket=" + ticket.Ticket, http.StatusFound) 78 | } else { 79 | http.Redirect(w, r, ticket.Service + "?ticket=" + ticket.Ticket, http.StatusFound) 80 | } 81 | } 82 | 83 | // HandleValidate handles validation request 84 | func HandleValidate(w http.ResponseWriter, r *http.Request) { 85 | serviceTicket, err := runValidators(w, r) 86 | 87 | if err != nil { 88 | validateResponse(false, err, nil, w, r) 89 | return 90 | } 91 | 92 | validateResponse(true, nil, serviceTicket, w, r) 93 | } 94 | 95 | func runValidators(w http.ResponseWriter, r *http.Request) (*types.Ticket, *types.CasError) { 96 | ticket := r.URL.Query().Get("ticket") 97 | err := validators.ValidateTicket(ticket) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | service := r.URL.Query().Get("service") 103 | serviceError := validators.ValidateService(service, config) 104 | if serviceError != nil { 105 | return nil, serviceError 106 | } 107 | 108 | serviceTicket := &types.Ticket{Service: service, Ticket: ticket} 109 | err = security.ValidateServiceTicket(strg, serviceTicket) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return serviceTicket, nil 115 | } 116 | 117 | func validateResponse(valid bool, casError *types.CasError, ticket *types.Ticket, w http.ResponseWriter, r *http.Request) { 118 | if valid { 119 | fmt.Fprintf(w, "yes") 120 | } else { 121 | fmt.Fprintf(w, "no") 122 | } 123 | } -------------------------------------------------------------------------------- /spec/v1_test.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "strings" 7 | "net/http/httptest" 8 | "github.com/matthewvalimaki/cas-server/types" 9 | "github.com/matthewvalimaki/cas-server/storage" 10 | ) 11 | 12 | func TestHandleLoginStorageNotSet(t *testing.T) { 13 | config = &types.Config{} 14 | specTemplatePath = "tmpl/" 15 | 16 | req, _ := http.NewRequest("GET", "?service=http://test.com", nil) 17 | req.RemoteAddr = "127.0.0.1:1234" 18 | w := httptest.NewRecorder() 19 | 20 | handleLogin(w, req) 21 | 22 | if w.Code != http.StatusInternalServerError { 23 | t.Errorf("Login did not return `%d`", http.StatusInternalServerError) 24 | } 25 | 26 | if !strings.Contains(w.Body.String(), "`strg` has not been set") { 27 | t.Errorf("Validation should have returned ``strg` has not been set`.") 28 | } 29 | } 30 | 31 | func TestHandleLoginUnknownService(t *testing.T) { 32 | config = &types.Config{} 33 | strg = &storage.MemoryStorage{} 34 | specTemplatePath = "tmpl/" 35 | 36 | req, _ := http.NewRequest("GET", "?service=http://test.com", nil) 37 | req.RemoteAddr = "127.0.0.1:1234" 38 | w := httptest.NewRecorder() 39 | 40 | handleLogin(w, req) 41 | 42 | if !strings.Contains(w.Body.String(), "Unknown service") { 43 | t.Errorf("Validation should have returned `Unknown service`.") 44 | } 45 | } 46 | 47 | func TestHandleLoginSuccess(t *testing.T) { 48 | strg = storage.NewMemoryStorage() 49 | services := make(map[string]*types.Service) 50 | services["test"] = &types.Service{ID: []string{"http://test.com"}} 51 | config = &types.Config{Services: services} 52 | config.FlattenServiceIDs() 53 | 54 | specTemplatePath = "tmpl/" 55 | 56 | req, _ := http.NewRequest("GET", "?service=http://test.com", nil) 57 | req.RemoteAddr = "127.0.0.1:1234" 58 | w := httptest.NewRecorder() 59 | 60 | handleLogin(w, req) 61 | 62 | if w.Code != http.StatusFound { 63 | t.Errorf("Login did not return `%d`", http.StatusFound) 64 | } 65 | 66 | if !strings.Contains(w.HeaderMap.Get("location"), "ticket=") { 67 | t.Errorf("Redirect did not contain query parameter `ticket`") 68 | } 69 | } 70 | 71 | func TestValidate(t *testing.T) { 72 | strg = storage.NewMemoryStorage() 73 | services := make(map[string]*types.Service) 74 | services["test"] = &types.Service{ID: []string{"http://test.com"}} 75 | config = &types.Config{Services: services} 76 | config.FlattenServiceIDs() 77 | 78 | specTemplatePath = "tmpl/" 79 | 80 | // without ticket 81 | req, _ := http.NewRequest("GET", "?service=http://test.com", nil) 82 | req.RemoteAddr = "127.0.0.1:1234" 83 | w := httptest.NewRecorder() 84 | 85 | setupValidate(w, req) 86 | 87 | if w.Code != http.StatusOK { 88 | t.Errorf("Validate returned `%d` was expecting `%d`", w.Code, http.StatusOK) 89 | } 90 | 91 | if w.Body.String() != "no" { 92 | t.Errorf("Response body contained `%s` was expecting `no`", w.Body.String()) 93 | } 94 | 95 | // with ticket 96 | req, _ = http.NewRequest("GET", "?service=http://test.com", nil) 97 | req.RemoteAddr = "127.0.0.1:1234" 98 | w = httptest.NewRecorder() 99 | 100 | handleLogin(w, req) 101 | ticket := w.HeaderMap.Get("location")[strings.LastIndex(w.HeaderMap.Get("location"), "ticket=") + 7 : len(w.HeaderMap.Get("location"))] 102 | 103 | req, _ = http.NewRequest("GET", "?service=http://test.com&ticket=" + ticket, nil) 104 | req.RemoteAddr = "127.0.0.1:1234" 105 | w = httptest.NewRecorder() 106 | setupValidate(w, req) 107 | 108 | if w.Code != http.StatusOK { 109 | t.Errorf("Validate returned `%d` was expecting `%d`", w.Code, http.StatusOK) 110 | } 111 | 112 | if w.Body.String() != "yes" { 113 | t.Errorf("Response body contained `%s` was expecting `yes` for ticket `%s`", w.Body.String(), ticket) 114 | } 115 | } -------------------------------------------------------------------------------- /spec/v2.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/matthewvalimaki/cas-server/security" 8 | "github.com/matthewvalimaki/cas-server/spec/xml" 9 | "github.com/matthewvalimaki/cas-server/types" 10 | "github.com/matthewvalimaki/cas-server/validators" 11 | ) 12 | 13 | // HandleValidateV2 handles `/serviceValidate` request 14 | func HandleValidateV2(w http.ResponseWriter, r *http.Request) { 15 | // do format check here as it will affect response format 16 | format := r.URL.Query().Get("format") 17 | if len(format) > 0 { 18 | err := validators.ValidateFormat(format) 19 | 20 | if err != nil { 21 | validateResponseV2("XML", err, nil, w, r) 22 | return 23 | } 24 | } 25 | if len(format) == 0 { 26 | format = "XML" 27 | } 28 | 29 | _, err := runValidators(w, r) 30 | if err != nil { 31 | validateResponseV2(format, err, nil, w, r) 32 | return 33 | } 34 | 35 | _, pgtURL, proxyGrantingTicket, proxyGrantingTicketIOU, err := runValidatorsV2(w, r) 36 | if err != nil { 37 | validateResponseV2(format, err, nil, w, r) 38 | return 39 | } 40 | 41 | // see: https://jasig.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#servicevalidate-cas-20 42 | if pgtURL != "" { 43 | strg.SaveTicket(proxyGrantingTicket) 44 | 45 | validateResponseV2(format, nil, proxyGrantingTicketIOU, w, r) 46 | return 47 | } 48 | 49 | validateResponseV2(format, nil, nil, w, r) 50 | } 51 | 52 | func runValidatorsV2(w http.ResponseWriter, r *http.Request) (service *types.Service, pgtURL string, proxyGrantingTicket *types.Ticket, proxyGrantingTicketIOU *types.Ticket, err *types.CasError) { 53 | pgtURL = r.URL.Query().Get("pgtUrl") 54 | if len(pgtURL) > 0 { 55 | serviceParameter := r.URL.Query().Get("service") 56 | 57 | // make sure that pgtURL can be used with service 58 | service, err := validators.ValidateProxyGrantingURL(config, serviceParameter, pgtURL) 59 | if err != nil { 60 | return nil, "", nil, nil, err 61 | } 62 | 63 | // Make sure endpoint can be reached and uses SSL as dictated by CAS spec 64 | // see: https://jasig.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#head2.5.4 65 | err = validators.ValidateProxyURLEndpoint(pgtURL) 66 | if err != nil { 67 | return nil, "", nil, nil, err 68 | } 69 | 70 | // Generate PGT (ProxyGrantingTicket) and PGTIOU (ProxyGgrantingTicketIOU) 71 | proxyGrantingTicket, err := security.CreateNewProxyGrantingTicket() 72 | if err != nil { 73 | return nil, "", nil, nil, err 74 | } 75 | 76 | proxyGrantingTicketIOU, err := security.CreateNewProxyGrantingTicketIOU() 77 | if err != nil { 78 | return nil, "", nil, nil, err 79 | } 80 | 81 | strg.SaveTicket(proxyGrantingTicket) 82 | strg.SaveTicket(proxyGrantingTicketIOU) 83 | 84 | // reach out to proxy and then validate behavior 85 | err = validators.SendAndValidateProxyIDAndIOU(pgtURL, proxyGrantingTicket, proxyGrantingTicketIOU) 86 | if err != nil { 87 | return nil, "", nil, nil, err 88 | } 89 | 90 | return service, pgtURL, proxyGrantingTicket, proxyGrantingTicketIOU, nil 91 | } 92 | 93 | return nil, "", nil, nil, nil 94 | } 95 | 96 | func validateResponseV2(format string, casError *types.CasError, proxyGrantingTicketIOU *types.Ticket, w http.ResponseWriter, r *http.Request) { 97 | if format == "XML" { 98 | w.Header().Set("Content-Type", "application/xml;charset=UTF-8") 99 | } else { 100 | w.Header().Set("Content-Type", "application/json;charset=UTF-8") 101 | } 102 | 103 | if casError != nil { 104 | fmt.Fprintf(w, xml.V2ValidationFailure(casError, format)) 105 | return 106 | } 107 | 108 | fmt.Fprintf(w, xml.V2ValidationSuccess("test", proxyGrantingTicketIOU, format)) 109 | } 110 | 111 | // HandleProxyV2 handles `proxy` request 112 | func HandleProxyV2(w http.ResponseWriter, r *http.Request) { 113 | // do format check here as it will affect response format 114 | format := r.URL.Query().Get("format") 115 | if len(format) > 0 { 116 | err := validators.ValidateFormat(format) 117 | 118 | if err != nil { 119 | proxyResponseV2("XML", nil, err, w, r) 120 | return 121 | } 122 | } 123 | if len(format) == 0 { 124 | format = "XML" 125 | } 126 | 127 | err := runProxyValidatorsV2(w, r) 128 | if err != nil { 129 | proxyResponseV2(format, nil, err, w, r) 130 | return 131 | } 132 | 133 | proxyTicket, err := security.CreateNewProxyTicket() 134 | if err != nil { 135 | proxyResponseV2(format, nil, err, w, r) 136 | return 137 | } 138 | 139 | strg.SaveTicket(proxyTicket) 140 | 141 | proxyResponseV2(format, proxyTicket, nil, w, r) 142 | } 143 | 144 | func runProxyValidatorsV2(w http.ResponseWriter, r *http.Request) *types.CasError { 145 | pgt := r.URL.Query().Get("pgt") 146 | err := validators.ValidateTicket(pgt) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | targetService := r.URL.Query().Get("targetService") 152 | ticket := &types.Ticket{Service: targetService, Ticket: pgt} 153 | err = security.ValidateProxyGrantingTicket(strg, ticket) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func proxyResponseV2(format string, proxyTicket *types.Ticket, casError *types.CasError, w http.ResponseWriter, r *http.Request) { 162 | if format == "XML" { 163 | w.Header().Set("Content-Type", "application/xml;charset=UTF-8") 164 | } else { 165 | w.Header().Set("Content-Type", "application/json;charset=UTF-8") 166 | } 167 | 168 | if casError != nil { 169 | fmt.Fprintf(w, xml.V2ProxyFailure(casError, format)) 170 | return 171 | } 172 | 173 | fmt.Fprintf(w, xml.V2ProxySuccess(proxyTicket, format)) 174 | } 175 | -------------------------------------------------------------------------------- /spec/v3.go: -------------------------------------------------------------------------------- 1 | package spec 2 | -------------------------------------------------------------------------------- /spec/xml/v2Proxy.go: -------------------------------------------------------------------------------- 1 | package xml 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/matthewvalimaki/cas-server/types" 7 | ) 8 | 9 | // V2ProxyFailure produces XML string for failure 10 | func V2ProxyFailure(casError *types.CasError, format string) string { 11 | if format == "XML" { 12 | return fmt.Sprintf(`%s`, 13 | casError.CasErrorCode.String(), casError.Error.Error()) 14 | } 15 | 16 | return fmt.Sprintf(`{ 17 | "serviceResponse": { 18 | "proxyFailure": { 19 | "code": "%s", 20 | "description": "%s" 21 | } 22 | } 23 | }`, casError.CasErrorCode.String(), casError.Error.Error()) 24 | } 25 | 26 | // V2ProxySuccess produces XML string for success 27 | func V2ProxySuccess(proxyTicket *types.Ticket, format string) string { 28 | if format == "XML" { 29 | return fmt.Sprintf(` 30 | 31 | %s 32 | 33 | `, 34 | proxyTicket.Ticket) 35 | } 36 | 37 | return fmt.Sprintf(`{ 38 | "serviceResponse": { 39 | "proxySuccess": { 40 | "proxyTicket": "%s" 41 | } 42 | } 43 | }`, proxyTicket.Ticket) 44 | } 45 | -------------------------------------------------------------------------------- /spec/xml/v2Validation.go: -------------------------------------------------------------------------------- 1 | package xml 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/matthewvalimaki/cas-server/types" 7 | ) 8 | 9 | // V2ValidationFailure produces XML string for failure 10 | func V2ValidationFailure(casError *types.CasError, format string) string { 11 | if format == "XML" { 12 | return fmt.Sprintf(`%s`, 13 | casError.CasErrorCode.String(), casError.Error.Error()) 14 | } 15 | 16 | return fmt.Sprintf(`{ 17 | "serviceResponse": { 18 | "authenticationFailure": { 19 | "code": "%s", 20 | "description": "%s" 21 | } 22 | } 23 | }`, 24 | casError.CasErrorCode.String(), casError.Error.Error()) 25 | } 26 | 27 | // V2ValidationSuccess produces XML string for success 28 | func V2ValidationSuccess(username string, proxyGrantingTicket *types.Ticket, format string) string { 29 | ticket := "" 30 | 31 | if format == "XML" { 32 | if proxyGrantingTicket != nil { 33 | ticket = fmt.Sprintf("%s", proxyGrantingTicket.Ticket) 34 | } 35 | 36 | return fmt.Sprintf(` 37 | %s 38 | %s 39 | 40 | 2 41 | 42 | 43 | `, 44 | ticket, username) 45 | } 46 | 47 | if proxyGrantingTicket != nil { 48 | ticket = fmt.Sprintf(`"proxyGrantingTicket": "%s",`, proxyGrantingTicket.Ticket) 49 | } 50 | 51 | return fmt.Sprintf(`{ 52 | "serviceResponse": { 53 | "authenticationSuccess": {%s 54 | "user": "%s" 55 | } 56 | } 57 | }`, ticket, username) 58 | } 59 | -------------------------------------------------------------------------------- /storage/interface.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/matthewvalimaki/cas-server/types" 5 | ) 6 | 7 | // IStorage interface for all Storages 8 | type IStorage interface { 9 | SaveTicket(*types.Ticket) 10 | DoesTicketExist(ticket string) *types.Ticket 11 | DeleteTicket(ticket string) 12 | } 13 | -------------------------------------------------------------------------------- /storage/memory.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/matthewvalimaki/cas-server/types" 7 | ) 8 | 9 | // MemoryStorage is memory based Storage 10 | type MemoryStorage struct { 11 | tickets map[string]*types.Ticket 12 | } 13 | 14 | // NewMemoryStorage returns new instance of MemoryStorage 15 | func NewMemoryStorage() *MemoryStorage { 16 | return &MemoryStorage{ 17 | tickets: make(map[string]*types.Ticket), 18 | } 19 | } 20 | 21 | // DoesTicketExist checks if given ticket exists 22 | func (s *MemoryStorage) DoesTicketExist(ticket string) *types.Ticket { 23 | if t, ok := s.tickets[ticket]; ok { 24 | // check if ticket should be deleted 25 | if t.Old() { 26 | s.DeleteTicket(ticket) 27 | return nil 28 | } 29 | return t 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // SaveTicket stores the Ticket 36 | func (s *MemoryStorage) SaveTicket(ticket *types.Ticket) { 37 | ticket.Created = time.Now() 38 | s.tickets[ticket.Ticket] = ticket 39 | } 40 | 41 | // DeleteTicket deletes given ticket 42 | func (s *MemoryStorage) DeleteTicket(ticket string) { 43 | delete(s.tickets, ticket) 44 | } 45 | -------------------------------------------------------------------------------- /test/login.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // SupportTest adds support for internal managed tests 8 | func SupportTest() { 9 | validate() 10 | } 11 | 12 | func validate() { 13 | http.HandleFunc("/test/login-redirect", handleValidate) 14 | } 15 | 16 | func handleValidate(w http.ResponseWriter, r *http.Request) { 17 | ticket := r.URL.Query().Get("ticket") 18 | if len(ticket) == 0 { 19 | http.Error(w, "Query Parameter `ticket` must be defined.", http.StatusInternalServerError) 20 | return 21 | } 22 | 23 | http.Redirect(w, r, "https://cas.matthewvalimaki.com/cas/serviceValidate?service=http://127.0.0.1:10000/test/login-redirect&ticket=" + ticket, http.StatusFound) 24 | } -------------------------------------------------------------------------------- /tools/config.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "github.com/matthewvalimaki/cas-server/types" 5 | 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | // NewConfig loads configuratio 10 | func NewConfig(location string) (*types.Config, error) { 11 | var config types.Config 12 | _, err := toml.DecodeFile(location, &config) 13 | 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | config.FlattenServiceIDs() 19 | 20 | return &config, nil 21 | } 22 | -------------------------------------------------------------------------------- /tools/log.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/matthewvalimaki/cas-server/types" 10 | ) 11 | 12 | func timestamp() string { 13 | var timeNow = time.Now() 14 | 15 | return timeNow.Format(time.RFC3339) 16 | } 17 | 18 | func log(messageType string, message string) { 19 | fmt.Println(fmt.Sprintf("[%s] [%s] %s", timestamp(), messageType, message)) 20 | } 21 | 22 | // Log prints generic log message 23 | func Log(message string) { 24 | log("generic", message) 25 | } 26 | 27 | func LogError(message string) { 28 | log("error", message) 29 | } 30 | 31 | // LogST prints ST related log message 32 | func LogST(ticket *types.Ticket, message string) { 33 | log("ST", fmt.Sprintf("[%s] [%s] %s", ticket.Service, ticket.Ticket, message)) 34 | } 35 | 36 | // LogPGT prints PGT related log message 37 | func LogPGT(ticket *types.Ticket, message string) { 38 | log("LogPGT", fmt.Sprintf("[%s] [%s] %s", ticket.Service, ticket.Ticket, message)) 39 | } 40 | 41 | // LogService logs service related message 42 | func LogService(ID string, message string) { 43 | log("service", fmt.Sprintf("[%s] %s", ID, message)) 44 | } 45 | 46 | // LogAdmin logs admin message 47 | func LogAdmin(message string) { 48 | log("admin", message) 49 | } 50 | 51 | // LogRequest logs plain request 52 | func LogRequest(r *http.Request, message string) { 53 | ip, _, _ := net.SplitHostPort(r.RemoteAddr) 54 | 55 | log("request", fmt.Sprintf("[%s] %s", ip, message)) 56 | } -------------------------------------------------------------------------------- /types/cas_error.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // CasErrorCode type declaration 4 | type CasErrorCode int 5 | 6 | const ( 7 | // CAS_ERROR_CODE_INVALID_REQUEST "not all of the required request parameters were present" 8 | CAS_ERROR_CODE_INVALID_REQUEST CasErrorCode = 1 + iota 9 | // CAS_ERROR_CODE_INVALID_TICKET_SPEC "failure to meet the requirements of validation specification" 10 | CAS_ERROR_CODE_INVALID_TICKET_SPEC 11 | // CAS_ERROR_CODE_INVALID_TICKET "the ticket provided was not valid, or the ticket did not come from an initial login and renew was set on validation." 12 | CAS_ERROR_CODE_INVALID_TICKET 13 | // INVALID_SERVICE "the ticket provided was valid, but the service specified did not match the service associated with the ticket." 14 | CAS_ERROR_CODE_INVALID_SERVICE 15 | // CAS_ERROR_CODE_INTERNAL_ERROR "an internal error occurred during ticket validation" 16 | CAS_ERROR_CODE_INTERNAL_ERROR 17 | // CAS_ERROR_CODE_UNAUTHORIZED_SERVICE_PROXY "the service is not authorized to perform proxy authentication" 18 | CAS_ERROR_CODE_UNAUTHORIZED_SERVICE_PROXY 19 | // CAS_ERROR_CODE_INVALID_PROXY_CALLBACK "The proxy callback specified is invalid. The credentials specified for proxy authentication do not meet the security requirements" 20 | CAS_ERROR_CODE_INVALID_PROXY_CALLBACK 21 | ) 22 | 23 | // CasErrorCodes contains all error codes in string format 24 | var CasErrorCodes = [...]string { 25 | "INVALID_REQUEST", 26 | "INVALID_TICKET_SPEC", 27 | "INVALID_TICKET", 28 | "INVALID_SERVICE", 29 | "INTERNAL_ERROR", 30 | "UNAUTHORIZED_SERVICE_PROXY", 31 | "INVALID_PROXY_CALLBACK", 32 | } 33 | 34 | func (casErrorCode CasErrorCode) String() string { 35 | return CasErrorCodes[casErrorCode - 1] 36 | } 37 | 38 | // CasError contains CAS error information 39 | type CasError struct { 40 | Error error 41 | CasErrorCode CasErrorCode 42 | } -------------------------------------------------------------------------------- /types/config.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // Config contains all of configuration 8 | type Config struct { 9 | Servers map[string]*Server 10 | Services map[string]*Service 11 | Cors *Cors 12 | 13 | FlatServiceIDList map[string][]string 14 | } 15 | 16 | // FlattenServiceIDs takes all service ids and flattens them 17 | func (c *Config) FlattenServiceIDs() { 18 | c.FlatServiceIDList = make(map[string][]string) 19 | 20 | for key := range c.Services { 21 | for _, serviceID := range c.Services[key].ID { 22 | c.FlatServiceIDList[serviceID] = append(c.FlatServiceIDList[serviceID], key) 23 | } 24 | 25 | // sorting improves sort.SearchStrings performance 26 | // so do it once here 27 | sort.Strings(c.Services[key].ProxyServices) 28 | } 29 | } -------------------------------------------------------------------------------- /types/cors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Cors contains CORS configuration 4 | type Cors struct { 5 | Origin []string 6 | Methods []string 7 | Credentials bool 8 | } 9 | 10 | // OriginToString converts origin configuration to string 11 | func (c Cors) OriginToString() string { 12 | var values string 13 | for _, value := range c.Origin { 14 | if values != "" { 15 | values += "," 16 | } 17 | values += value 18 | } 19 | 20 | return values 21 | } 22 | 23 | // MethodsToString converts methods configuration to string 24 | func (c Cors) MethodsToString() string { 25 | var values string 26 | for _, value := range c.Methods { 27 | if values != "" { 28 | values += "," 29 | } 30 | values += value 31 | } 32 | 33 | return values 34 | } -------------------------------------------------------------------------------- /types/server.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | // Server contains server specific configuration 8 | type Server struct { 9 | Port int 10 | SSL bool 11 | CACert string 12 | CAKey string 13 | } 14 | 15 | // PortToString converts integer port to string 16 | func (s Server) PortToString() string { 17 | return strconv.Itoa(s.Port) 18 | } -------------------------------------------------------------------------------- /types/service.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // Service contains service definition 8 | type Service struct { 9 | ID []string 10 | ProxyServices []string 11 | } 12 | 13 | // HasProxyService checks if given serviceKey exists 14 | func (s Service) HasProxyService(serviceKey string) bool { 15 | i := sort.SearchStrings(s.ProxyServices, serviceKey) 16 | 17 | return i < len(s.ProxyServices) && s.ProxyServices[i] == serviceKey 18 | } -------------------------------------------------------------------------------- /types/ticket.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "time" 4 | 5 | // Ticket is used for ticket information generalization 6 | type Ticket struct { 7 | Ticket string 8 | Type string 9 | Service string 10 | Created time.Time 11 | } 12 | 13 | // Old calculates time from creation and compares to lifetime 14 | // configuration 15 | func (t *Ticket) Old() bool { 16 | // lifetime calculation 17 | return int(time.Now().Sub(t.Created).Minutes()) > 5 18 | } 19 | -------------------------------------------------------------------------------- /types/user.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // User holds user related data 4 | type User struct { 5 | FailedLoginCount int 6 | IP []string 7 | } 8 | 9 | // NewUser creates new user 10 | func NewUser(ip string) User { 11 | var user = User{FailedLoginCount: 0} 12 | user.IP = append(user.IP, ip) 13 | 14 | return user 15 | } -------------------------------------------------------------------------------- /validators/format.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/matthewvalimaki/cas-server/types" 7 | ) 8 | 9 | // ValidateFormat validates that format is of correct value 10 | func ValidateFormat(format string) *types.CasError { 11 | if format != "XML" && format != "JSON" { 12 | return &types.CasError{Error: errors.New("Query parameter `format` contained illegal value. Allowed values are `XML' and `JSON`."), CasErrorCode: types.CAS_ERROR_CODE_INVALID_PROXY_CALLBACK} 13 | } 14 | 15 | return nil 16 | } -------------------------------------------------------------------------------- /validators/proxy.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "fmt" 5 | "errors" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/matthewvalimaki/cas-server/types" 10 | ) 11 | 12 | // ValidateProxyGrantingURL validates proxy pgtURL 13 | func ValidateProxyGrantingURL(config *types.Config, serviceID string, pgtURL string) (*types.Service, *types.CasError) { 14 | // make sure we have the pgtURL specified as service in the first place 15 | proxyKeys, casError := validateServiceID(pgtURL, config) 16 | if casError != nil { 17 | return nil, &types.CasError{Error: errors.New("`pgtUrl` was not found from configuration"), CasErrorCode: types.CAS_ERROR_CODE_UNAUTHORIZED_SERVICE_PROXY} 18 | } 19 | 20 | // make sure the service exists 21 | serviceKeys, casError := validateServiceID(serviceID, config) 22 | if casError != nil { 23 | return nil, &types.CasError{Error: errors.New("`service` was not found from configuration"), CasErrorCode: types.CAS_ERROR_CODE_INVALID_SERVICE} 24 | } 25 | 26 | for _, serviceKey := range serviceKeys { 27 | for _, proxyKey := range proxyKeys { 28 | config.Services[serviceKey].HasProxyService(proxyKey) 29 | 30 | return config.Services[serviceKey], nil 31 | } 32 | } 33 | 34 | return nil, &types.CasError{Error: fmt.Errorf("Service `%s` is not allowed to get proxy ticket for proxy service `%s`", serviceID, pgtURL), CasErrorCode: types.CAS_ERROR_CODE_UNAUTHORIZED_SERVICE_PROXY} 35 | } 36 | 37 | // ValidateProxyURLEndpoint reaches out to the proxy URL 38 | // see: https://jasig.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html#head2.5.4 39 | func ValidateProxyURLEndpoint(pgtURL string) *types.CasError { 40 | _, err := http.Get(pgtURL) 41 | if err != nil { 42 | return &types.CasError{Error: fmt.Errorf("Proxy service `%s` validation failed with error: `%s`", pgtURL, err.Error()), CasErrorCode: types.CAS_ERROR_CODE_INVALID_PROXY_CALLBACK} 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // SendAndValidateProxyIDAndIOU reaches out to the proxy URL with query parameters 49 | func SendAndValidateProxyIDAndIOU(pgtURL string, proxyGrantingTicket *types.Ticket, proxyGrantingTicketIOU *types.Ticket) *types.CasError { 50 | pgtURLWithParameters := pgtURL 51 | 52 | if strings.Contains(pgtURL, "?") { 53 | pgtURLWithParameters += "&" 54 | } else { 55 | pgtURLWithParameters += "?" 56 | } 57 | 58 | pgtURLWithParameters += "pgtId=" + proxyGrantingTicket.Ticket 59 | pgtURLWithParameters += "&pgtIou=" + proxyGrantingTicketIOU.Ticket 60 | 61 | response, err := http.Get(pgtURLWithParameters) 62 | if err != nil { 63 | return &types.CasError{Error: fmt.Errorf("Proxy service `%s` validation failed with error: `%s`", pgtURL, err.Error()), CasErrorCode: types.CAS_ERROR_CODE_INVALID_PROXY_CALLBACK} 64 | } 65 | 66 | // enforce required status code check 67 | if response.StatusCode != http.StatusOK { 68 | return &types.CasError{Error: fmt.Errorf("Proxy service with CAS query parameters `%s` returned status code `%d` while `%d` is required", pgtURLWithParameters, response.StatusCode, http.StatusOK), CasErrorCode: types.CAS_ERROR_CODE_INVALID_PROXY_CALLBACK} 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /validators/request.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "errors" 7 | 8 | "github.com/matthewvalimaki/cas-server/types" 9 | ) 10 | 11 | // ValidateRequest executes validation against plain request 12 | func ValidateRequest(r *http.Request) *types.CasError { 13 | ip, _, err := net.SplitHostPort(r.RemoteAddr) 14 | if err != nil { 15 | return &types.CasError{Error: errors.New("Could not parse remote IP:Port."), CasErrorCode: types.CAS_ERROR_CODE_INTERNAL_ERROR} 16 | } 17 | 18 | casError := isRemoteAddrAllowed(ip) 19 | if casError != nil { 20 | return casError 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func isRemoteAddrAllowed(ip string) *types.CasError { 27 | return nil 28 | //return &types.CasError{Error: errors.New("The IP is currently not allowed."), CasErrorCode: types.CAS_ERROR_CODE_INTERNAL_ERROR} 29 | } -------------------------------------------------------------------------------- /validators/service.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "fmt" 5 | "errors" 6 | "regexp" 7 | 8 | "github.com/matthewvalimaki/cas-server/types" 9 | ) 10 | 11 | // ValidateService validates service 12 | func ValidateService(ID string, config *types.Config) (*types.CasError) { 13 | err := validateServiceIDLength(ID) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | _, err = validateServiceID(ID, config) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func validateServiceIDLength(ID string) *types.CasError { 27 | if len(ID) == 0 { 28 | return &types.CasError{Error: errors.New("Required query parameter `service` was not defined."), CasErrorCode: types.CAS_ERROR_CODE_INVALID_SERVICE} 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func validateServiceID(serviceID string, config *types.Config) (serviceKeys []string, casError *types.CasError) { 35 | for supportedServiceID := range config.FlatServiceIDList { 36 | if matched, _ := regexp.MatchString(supportedServiceID, serviceID); matched { 37 | return config.FlatServiceIDList[supportedServiceID], nil 38 | } 39 | } 40 | 41 | return nil, &types.CasError{Error: fmt.Errorf("Unknown service `%s`", serviceID), CasErrorCode: types.CAS_ERROR_CODE_INVALID_SERVICE} 42 | } -------------------------------------------------------------------------------- /validators/ticket.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "fmt" 5 | "errors" 6 | 7 | "github.com/matthewvalimaki/cas-server/types" 8 | ) 9 | 10 | // ValidateTicket validates ticket 11 | func ValidateTicket(ticket string) (*types.CasError) { 12 | err := validateTicketLength(ticket) 13 | if (err != nil) { 14 | return err 15 | } 16 | 17 | err = validateTicketFormat(ticket) 18 | if (err != nil) { 19 | return err 20 | } 21 | 22 | err = validateTicketTimestamp(ticket) 23 | if (err != nil) { 24 | return err 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func validateTicketLength(ticket string) *types.CasError { 31 | if len(ticket) == 0 { 32 | return &types.CasError{Error: errors.New("Required query parameter `ticket` was not defined."), CasErrorCode: types.CAS_ERROR_CODE_INVALID_REQUEST} 33 | } 34 | 35 | if len(ticket) < 32 { 36 | return &types.CasError{Error: fmt.Errorf("Ticket is not long enough. Minimum length is `%d` but length was `%d`.", 32, len(ticket)), CasErrorCode: types.CAS_ERROR_CODE_INVALID_TICKET_SPEC} 37 | } 38 | 39 | if len(ticket) > 256 { 40 | return &types.CasError{Error: fmt.Errorf("Ticket is too long. Maximum length is `%d` but length was `%d`.", 256, len(ticket)), CasErrorCode: types.CAS_ERROR_CODE_INVALID_TICKET_SPEC} 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func validateTicketFormat(ticket string) *types.CasError { 47 | if ticket[0:3] == "ST-" { 48 | return nil 49 | } else if ticket[0:4] == "PGT-" { 50 | return nil 51 | } else if ticket[0:3] == "PT-" { 52 | return nil 53 | } 54 | 55 | return &types.CasError{Error: errors.New("Required ticket prefix is missing. Supported prefixes are: [ST, PGT]"), CasErrorCode: types.CAS_ERROR_CODE_INVALID_TICKET_SPEC} 56 | } 57 | 58 | func validateTicketTimestamp(ticket string) *types.CasError { 59 | return nil 60 | } 61 | --------------------------------------------------------------------------------