├── 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 |
--------------------------------------------------------------------------------