Repository: samitpal/simple-sso Branch: master Commit: 72eb3da9990a Files: 24 Total size: 37.1 KB Directory structure: gitextract_hkivw28_/ ├── .travis.yml ├── LICENSE.md ├── README.md ├── example_app/ │ └── main.go ├── key_pair/ │ ├── README.md │ ├── demo.rsa │ └── demo.rsa.pub ├── ldap/ │ ├── config.go │ ├── config_test.go │ ├── ldap.go │ └── ldap_test.go ├── main.go ├── ssl_certs/ │ ├── README.md │ ├── cert.pem │ └── key.pem ├── sso/ │ ├── sso.go │ └── sso_test.go ├── templates/ │ ├── footer.html │ ├── header.html │ └── login.html └── util/ ├── test/ │ ├── test_key.pem │ └── test_key.pub ├── util.go └── util_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .travis.yml ================================================ sudo: false language: go install: - go get -u github.com/jteeuwen/go-bindata/... - go get -u github.com/dgrijalva/jwt-go/... - go get -u github.com/gorilla/mux/... - go get -u gopkg.in/ldap.v2/... - go get -u github.com/gorilla/handlers/... - go get -u github.com/samitpal/goProbe/... before_script: - go generate go: - 1.5 script: - go vet ./... - go test ./... ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) [2016] [Samit Pal] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.org/samitpal/simple-sso.svg?branch=master)](https://travis-ci.org/samitpal/simple-sso) [google group](https://groups.google.com/forum/#!forum/simple-sso) Summary ------------------ simple-sso is an SSO service with support for roles based authorization written in the Go programming language. For browser based applications the service exposes the /sso handler which sets the sso cookie for a given domain. For instance if the login service runs as login.example.com, the sso cookie domain could be configured as example.com. That way any application running under a subdomain of example.com will be able to leverage the sso service (see [rfc6265](https://tools.ietf.org/html/rfc6265#page-6)). The value of the sso cookie is a [jwt](https://jwt.io/) token signed by the rsa private key of the simple-sso service. To use this service the application needs to have the corresponding public key in order to decrypt the cookie. The app checks for the presence of the sso cookie and in the absence of that it redirects to the /sso handler of the sample-sso service setting the **s_url** parameter to its url. The login service is expected to redirect the user back to **s_url** post authentication. See the code under example_app directory. simple-sso exposes /auth_token handler which can be used to download the encrypted jwt token. The downloaded token can potentially be passed via Authorization headers by client applications to server apps hopefully using ssl. simple-sso also has a form of authorization capabilities. It can optionally pack in the roles (e.g openldap groups) information in the cookie/jwt based on a config environment variables.. They say a picture is thousand times more effective, so here is a diagram which shows traffic flow with simple-sso. ![alt tag](https://docs.google.com/drawings/d/1blQbqjT4lb0nu_lX-WO2OaQPvhg5I2pF0LvPZnQ9ywA/pub?w=960&h=720) Installation ------------------- ##### To build from source follow the steps below: ```sh $ go get -u github.com/jteeuwen/go-bindata/... $ go get -u github.com/samitpal/simple-sso/... $ export PATH=$PATH:$GOPATH/bin $ go generate $ go install ``` Running the binary ------------------- Just run the simple-sso binary. Following principles of 12 factor app, simple-sso uses environment variables for its configurations. These are. | Variable | Default value | Purpose | |---------------|--------------|------------| | sso_ssl_cert_path | ssl_certs/cert.pem | ssl certificate path. | | sso_ssl_key_path |ssl_certs/key.pem | ssl certificate private key. | | sso_private_key_path | key_pair/demo.rsa | rsa private key path used to sign the token. | | sso_weblog_dir | - | Directory path where access hits are logged. | | sso_user_roles | false | Whether to pack in the roles info within the token. | | sso_cookie_name | SSO_C | Name of the sso cookie. | | sso_cookie_domain | 127.0.0.1 | Domain name of the cookie. | | sso_cookie_validhours | 20 | Cookie validity in hours. | | sso_ldap_host | localhost | Ldap host. | | sso_ldap_port | 389 | Ldap host port. | | sso_ldap_ssl | false | whether to use ssl. | | sso_ldap_basedn | - | Ldap base dn. | | sso_ldap_binddn | - | Ldap bind dn if anonymous bind is disallowed. | | sso_ldap_bindpasswd | - | Ldap bind password if anonymous bind is disallowed. | Caveats ------------------ * Since time is of essence in this infrastructure, the server time needs to be set and managed correctly. * Communication between this service and the ldap infrastruture should be encrypted. * This has been tested with openldap. ================================================ FILE: example_app/main.go ================================================ package main import ( "crypto/rsa" "fmt" jwt "github.com/dgrijalva/jwt-go" "github.com/gorilla/mux" "github.com/samitpal/simple-sso/util" "io/ioutil" "log" "net/http" "strings" ) var parsedPubKey *rsa.PublicKey func init() { key, _ := ioutil.ReadFile("../key_pair/demo.rsa.pub") // this is the public key of the login (simple-sso) server parsedPubKey, _ = jwt.ParseRSAPublicKeyFromPEM(key) } func cookieCheck(w http.ResponseWriter, r *http.Request) { c, err := r.Cookie("SSO_C") if err == http.ErrNoCookie { // we redirect to the login service setting the appropriate s_url to come back after auth. http.Redirect(w, r, "https://127.0.0.1:8081/sso?s_url=https://127.0.0.1:8082/cookie", 301) return } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } parts := strings.Split(strings.Split(c.String(), "=")[1], ".") err = jwt.SigningMethodRS512.Verify(strings.Join(parts[0:2], "."), parts[2], parsedPubKey) if err != nil { log.Fatalf("[%v] Error while verifying key: %v", strings.Split(c.String(), "=")[1], err) } tokenString := strings.Split(c.String(), "=")[1] token, err := jwt.ParseWithClaims(tokenString, &util.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { return parsedPubKey, nil }) claims, ok := token.Claims.(*util.CustomClaims) // claims.User and claims.Roles are what we are interested in. if ok && token.Valid { fmt.Printf("User: %v Roles: %v Tok_Expires: %v \n", claims.User, claims.Roles, claims.StandardClaims.ExpiresAt) } else { fmt.Println(err) } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "You have visited a cookietest page.\n\n") fmt.Fprintf(w, "User: %v, Roles: %v, Tok_Expires: %v\n", claims.User, claims.Roles, claims.StandardClaims.ExpiresAt) return } func authTokCheck(w http.ResponseWriter, r *http.Request) { h := r.Header.Get("Authorization") if h == "" { http.Error(w, "No Authorization header", http.StatusInternalServerError) return } parts := strings.Split(strings.Split(h, " ")[1], ".") err := jwt.SigningMethodRS512.Verify(strings.Join(parts[0:2], "."), parts[2], parsedPubKey) if err != nil { log.Fatalf("[%v] Error while verifying key: %v", strings.Split(h, "=")[1], err) } tokenString := strings.Split(h, " ")[1] token, err := jwt.ParseWithClaims(tokenString, &util.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { return parsedPubKey, nil }) claims, ok := token.Claims.(*util.CustomClaims) // claims.User and claims.Roles are what we are interested in. if ok && token.Valid { fmt.Printf("User: %v Roles: %v Tok_Expires: %v \n", claims.User, claims.Roles, claims.StandardClaims.ExpiresAt) } else { fmt.Println(err) } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "You have visited a auth tokentest page.\n\n") fmt.Fprintf(w, "User: %v, Roles: %v, Tok_Expires: %v\n", claims.User, claims.Roles, claims.StandardClaims.ExpiresAt) return } func main() { log.Println("Starting app server.") r := mux.NewRouter() r.HandleFunc("/cookie", cookieCheck) r.HandleFunc("/auth_token", authTokCheck) http.Handle("/", r) err := http.ListenAndServeTLS(":8082", "../ssl_certs/cert.pem", "../ssl_certs/key.pem", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } ================================================ FILE: key_pair/README.md ================================================ This directory contains the rsa key pair. simple-sso uses the private key to sign the jwt tokens. The client apps need to use the public key to decrypt the same ================================================ FILE: key_pair/demo.rsa ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEA1YOCGREepbXPOou0dLcZy8XXp4jD1If6fVFAYx/sa72/ih+B NGkxCuq20sLqoPQb273bixUPwWtjqWJCaHzf5WgPwh2zej7V3FiwnMTDuAPLdo+6 0FWhEUSb9pZlnoxXIQWjUabixahmpyJVBdavbReBNQr3xkeio87vDSpSzI9qsfGl SkA2mm14GIgnBGaNEwxka+YTLZgZOMtNMw38zW+t2PVmy5ul+pFXMvG87FN4fiSu dQdIeS7YyWGUEuCV/091neOpp4oWdwAkik/+oVE4VcIUvgvsmtV6z3KNYUcYfRoC EWaBoe8I78hy+nssDUKlT7XBN56hIXek6P9TewIDAQABAoIBAFnAj0a0QJrOA0+L /I53jZtwDgg54IANrQlSx2sjt0FPIR4RwkFi2p/JLJMKJpEELFXByHD9qILY/qrs SBgeLgwEI2OpEpIXqdSXX552xAMtbTDomFINPMjCe4E7lXoBanrSIOYo7fjComwt bWon5dRI5iKC+sbZxA9x5GE3YljkR5lAu01RFbc0iDThNsBJYAFxsLgOmLgBF8xO frmZt+CkgfNsFq1PguX3AoL7es6kwWtn2yLlIqxp/QOsuNuFnjo+hQ7WnaHyKqBF r/dbF58qXxvSnGWzVsz29hYglNWyTSPDUOwWeES14qGZsal9KUGSubd9MKNC3Dks T0aMdxECgYEA+x7m2Bt04RKLqtE5Oh5L9RhANZ7/B8qfgFIjKv7EJ2ovqTWUCVsy ljHuSo2YN7kQ1/NfTGiOzv6JRdeNtwCqbBYU619KyhICVanUBz4i+6rtqQPhhp9U WuXRp1rjE3haxGBPkY+Ze58mMiMGLvOtjFA+5jze6AWiXB8ksz7olgMCgYEA2amL rwvxlPV8M+knBtRkIc7nCWZ6Lz2sL8VXHvqWQpvY6XVMXOo+OJxXGzw5LMo4th4q qEPxyuUpMuGt+3SYtD2w2+nL6bLNutmj1lSAW0Rac1tW7kH08RLTRaQhy/qbB5s/ pi1M24h0yPCuRmm+EhXeuOReP62seFc5lJw8bykCgYEA4Nye4MxVMGUm42JN2Bjg 8ysv89PXkeaCRKlIDGvswU54NxBe6rHa7lrvgZqgvuTcjELFBuppVjjeOsf1gfT6 paZwPQMrOR4/MO3Nil69fJVmEn4DKETriClaPn1H8FtJC6ciGLl5OhUcYrCyDMDu mkIQ0KGZCDJjXBIXDto58nkCgYEAmeX5L+GgBJS2JvYZdAjEa+shDFJ63eAbWQON IAhKKfqLmjYnsiKlr91K8aTZQEQTaSFXQ/YWhkEVqjZLj9nXBsn/vN5IIYsdT5oG 78p7nwxrb9kLVBcqmzGWVE1C4DjnWK96h4LMLwUCnfkfIAYwMBVqjwxZX2jq44O4 4My/JlECgYEAwIrodHWjWhY1h5R9N3NXCKP9QPuevJWgvkZjhXDo+nPZE7T37yMe I+Y1+yXGEKP0d56uhfnRg2GaIBOE+KMdC/mxRNYyhuIQRIro/N3brbILp1nK8Gpo PaBcKoBjqxclV7sDeHfzCCB4GQTAVa2HFPpYvLVRo8hSiR4bdSBcjVo= -----END RSA PRIVATE KEY----- ================================================ FILE: key_pair/demo.rsa.pub ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1YOCGREepbXPOou0dLcZ y8XXp4jD1If6fVFAYx/sa72/ih+BNGkxCuq20sLqoPQb273bixUPwWtjqWJCaHzf 5WgPwh2zej7V3FiwnMTDuAPLdo+60FWhEUSb9pZlnoxXIQWjUabixahmpyJVBdav bReBNQr3xkeio87vDSpSzI9qsfGlSkA2mm14GIgnBGaNEwxka+YTLZgZOMtNMw38 zW+t2PVmy5ul+pFXMvG87FN4fiSudQdIeS7YyWGUEuCV/091neOpp4oWdwAkik/+ oVE4VcIUvgvsmtV6z3KNYUcYfRoCEWaBoe8I78hy+nssDUKlT7XBN56hIXek6P9T ewIDAQAB -----END PUBLIC KEY----- ================================================ FILE: ldap/config.go ================================================ package ldap import ( "crypto/rsa" "errors" jwt "github.com/dgrijalva/jwt-go" "io/ioutil" "log" "os" "strconv" "github.com/samitpal/simple-sso/sso" ) var PrivateKey *rsa.PrivateKey var BaseConf *sso.BaseConfig type LdapConfig struct { host string port int ssl bool basedn string binddn string bindPasswd string } func setupBaseConfig() { var err error BaseConf, err = sso.SetupBaseConfig() if err != nil { log.Fatal(err) } privateKeyData, err := ioutil.ReadFile(BaseConf.PrivateKeyPath) if err != nil { log.Fatal(err) } PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(privateKeyData) if err != nil { log.Fatal(err) } } // setDefaultString returns a given default string. func setDefaultString(s string, d string) string { if s == "" { return d } return s } // ldapConfig sets up ldap config from the env. func (l *LdapConfig) setupLdapConfig() error { l.host = setDefaultString(os.Getenv(sso.ConfMap["sso_ldap_host"]), "localhost") port, err := strconv.Atoi(setDefaultString(os.Getenv(sso.ConfMap["sso_ldap_port"]), "389")) if err != nil { return err } l.port = port ssl, err := strconv.ParseBool(setDefaultString(os.Getenv(sso.ConfMap["sso_ldap_ssl"]), "false")) if err != nil { return err } l.ssl = ssl l.basedn = os.Getenv(sso.ConfMap["sso_ldap_basedn"]) l.binddn = os.Getenv(sso.ConfMap["sso_ldap_binddn"]) l.bindPasswd = os.Getenv(sso.ConfMap["sso_ldap_bindpasswd"]) if l.binddn != "" && l.bindPasswd == "" { return errors.New("Bind dn is set but bind password is not set.") } return nil } ================================================ FILE: ldap/config_test.go ================================================ package ldap import ( "github.com/samitpal/simple-sso/sso" "os" "reflect" "testing" ) func TestSetupDefaultString(t *testing.T) { s := "return_me" d := "not_me" r := setDefaultString(s, d) if r != s { t.Errorf("Got: %s Want: %s", r, s) } s1 := "" d1 := "return_me" r = setDefaultString(s1, d1) if r != d1 { t.Errorf("Got: %s Want: %s", r, d1) } } func TestSetupLdapConfig(t *testing.T) { os.Setenv(sso.ConfMap["sso_ldap_host"], "host") os.Setenv(sso.ConfMap["sso_ldap_port"], "123") os.Setenv(sso.ConfMap["sso_ldap_ssl"], "true") os.Setenv(sso.ConfMap["sso_ldap_basedn"], "basedn") os.Setenv(sso.ConfMap["sso_ldap_binddn"], "binddn") os.Setenv(sso.ConfMap["sso_ldap_bindpasswd"], "bindpasswd") l := LdapConfig{} err := l.setupLdapConfig() if err != nil { t.Errorf("Error: %v", err) } w := LdapConfig{"host", 123, true, "basedn", "binddn", "bindpasswd"} if !reflect.DeepEqual(l, w) { t.Errorf("Got: %v\n \tWant: %v", l, w) } _ = os.Unsetenv(sso.ConfMap["sso_ldap_host"]) _ = os.Unsetenv(sso.ConfMap["sso_ldap_port"]) _ = os.Unsetenv(sso.ConfMap["sso_ldap_ssl"]) err = l.setupLdapConfig() if err != nil { t.Errorf("Error: %v", err) } w = LdapConfig{"localhost", 389, false, "basedn", "binddn", "bindpasswd"} if !reflect.DeepEqual(l, w) { t.Errorf("Got: %v\n \tWant: %v", l, w) } } ================================================ FILE: ldap/ldap.go ================================================ // package ldap is an sso implementation. It uses an ldap backend to authenticate and optionally // utilize ldap group memberships for setting up roles in the cookie/jwt which can later be used // by applications for authorization. package ldap import ( "crypto/tls" "fmt" "gopkg.in/ldap.v2" "net/http" "time" "github.com/samitpal/simple-sso/sso" "github.com/samitpal/simple-sso/util" ) type LdapSSO struct { Cookie *sso.CookieConfig Ldap *LdapConfig } var ( ErrUserNotFound = sso.ErrUserNotFound ErrUnauthorized = sso.ErrUnAuthorized ) func NewLdapSSO() (*LdapSSO, error) { setupBaseConfig() c, err := sso.SetupCookieConfig() if err != nil { return nil, err } l := new(LdapConfig) err = l.setupLdapConfig() if err != nil { return nil, err } return &LdapSSO{c, l}, nil } func (ls LdapSSO) Auth(u string, p string) (*string, *[]string, error) { ldap.DefaultTimeout = 20 * time.Second // applies to Dial and DialTLS methods. l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ls.Ldap.host, ls.Ldap.port)) if err != nil { return nil, nil, err } defer l.Close() // Reconnect with TLS if sso_ldap_ssl env is set. if ls.Ldap.ssl { err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) if err != nil { return nil, nil, err } } // First bind with a read only user if ls.Ldap.binddn != "" { err = l.Bind(ls.Ldap.binddn, ls.Ldap.bindPasswd) if err != nil { return nil, nil, err } } // Search for the given username searchRequestUser := ldap.NewSearchRequest( ls.Ldap.basedn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 30, false, // sets a time limit of 30 secs fmt.Sprintf("(&(objectClass=inetOrgPerson)(uid=%s))", u), []string{"dn"}, nil, ) sru, err := l.Search(searchRequestUser) if err != nil { return nil, nil, err } if len(sru.Entries) != 1 { return nil, nil, ErrUserNotFound } userdn := sru.Entries[0].DN // Bind as the user to verify their password err = l.Bind(userdn, p) if err != nil { return nil, nil, ErrUnauthorized } // Now find the group membership (if sso_user_roles env is true). var g []string if BaseConf.UserRoles { searchRequestGroups := ldap.NewSearchRequest( ls.Ldap.basedn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 30, false, // sets a time limit of 30 secs fmt.Sprintf("(&(objectClass=posixGroup)(memberUid=%s))", u), []string{"cn"}, nil, ) srg, err := l.Search(searchRequestGroups) if err != nil { return &u, nil, err } g = srg.Entries[0].GetAttributeValues("cn") } return &u, &g, nil } func (ls LdapSSO) CTValidHours() int64 { return ls.Cookie.ValidHours } func (ls LdapSSO) BuildJWTToken(u string, g []string, exp time.Time) (string, error) { return util.GenJWT(u, g, PrivateKey, exp.Unix()) } func (ls LdapSSO) CookieName() string { return ls.Cookie.Name } func (ls LdapSSO) CookieDomain() string { return ls.Cookie.Domain } func (ls LdapSSO) BuildCookie(s string, exp time.Time) http.Cookie { c := http.Cookie{ Name: ls.Cookie.Name, Value: s, Domain: ls.Cookie.Domain, Path: "/", Expires: exp, MaxAge: int(ls.Cookie.ValidHours * 3600), Secure: true, HttpOnly: true, } return c } func (ls LdapSSO) Logout(expT time.Time) http.Cookie { c := http.Cookie{ Name: ls.Cookie.Name, Value: "", Domain: ls.Cookie.Domain, Path: "/", Expires: expT, MaxAge: -1, Secure: true, HttpOnly: true, } return c } ================================================ FILE: ldap/ldap_test.go ================================================ package ldap import ( "github.com/samitpal/simple-sso/sso" "net/http" "os" "reflect" "strconv" "testing" "time" ) func init() { os.Setenv(sso.ConfMap["sso_private_key_path"], "../util/test/test_key.pem") } func TestBuildCookie(t *testing.T) { os.Setenv(sso.ConfMap["sso_cookie_name"], "LoginCookie") os.Setenv(sso.ConfMap["sso_cookie_domain"], "test.com") ls, err := NewLdapSSO() if err != nil { t.Errorf("Error: %v", err) } cv := "Cookie Value" expTime := time.Now().Add(time.Hour * time.Duration(ls.CTValidHours())) expectedCookie := http.Cookie{ Name: ls.CookieName(), Value: cv, Domain: ls.CookieDomain(), Path: "/", Expires: expTime, MaxAge: int(ls.CTValidHours() * 3600), Secure: true, HttpOnly: true, } recCookie := ls.BuildCookie(cv, expTime) if !reflect.DeepEqual(expectedCookie, recCookie) { t.Errorf("Got %v\n Want: %v", recCookie, expectedCookie) } } func TestLogout(t *testing.T) { os.Setenv(sso.ConfMap["sso_cookie_name"], "LoginCookie") os.Setenv(sso.ConfMap["sso_cookie_domain"], "test.com") ls, err := NewLdapSSO() if err != nil { t.Errorf("Error: %v", err) } expTime := time.Now().Add(time.Hour * time.Duration(-1)) expectedCookie := http.Cookie{ Name: ls.CookieName(), Value: "", Domain: ls.CookieDomain(), Path: "/", Expires: expTime, MaxAge: -1, Secure: true, HttpOnly: true, } recCookie := ls.Logout(expTime) if !reflect.DeepEqual(expectedCookie, recCookie) { t.Errorf("Got: %v\n Want: %v", recCookie, expectedCookie) } } func TestCTValidHours(t *testing.T) { vh := "30" os.Setenv(sso.ConfMap["sso_cookie_validhours"], vh) ls, err := NewLdapSSO() if err != nil { t.Errorf("Error: %v", err) } i, _ := strconv.Atoi(vh) if ls.CTValidHours() != int64(i) { t.Errorf("Got: %d\n Want: %d", ls.CTValidHours(), i) } } func TestCookieName(t *testing.T) { cn := "my cookie" os.Setenv(sso.ConfMap["sso_cookie_name"], cn) ls, err := NewLdapSSO() if err != nil { t.Errorf("Error: %v", err) } if ls.CookieName() != cn { t.Errorf("Got: %s\n Want: %s", ls.CookieName(), cn) } } func TestCookieDomain(t *testing.T) { dn := "mydomain.com" os.Setenv(sso.ConfMap["sso_cookie_domain"], dn) ls, err := NewLdapSSO() if err != nil { t.Errorf("Error: %v", err) } if ls.CookieDomain() != dn { t.Errorf("Got: %s\n Want: %s", ls.CookieDomain(), dn) } } ================================================ FILE: main.go ================================================ package main //go:generate go-bindata templates/... import ( "fmt" "github.com/gorilla/handlers" "github.com/gorilla/mux" weblog "github.com/samitpal/goProbe/log" "html/template" "log" "net/http" "os" "time" "github.com/samitpal/simple-sso/ldap" "github.com/samitpal/simple-sso/sso" ) var lsso sso.SSOer var templates = template.New("") func init() { var err error lsso, err = ldap.NewLdapSSO() if err != nil { log.Fatalf("Error initializing ldap sso: %s", err) } for _, path := range AssetNames() { bytes, err := Asset(path) if err != nil { log.Fatalf("Unable to parse: path=%s, err=%s", path, err) } templates.New(path).Parse(string(bytes)) } } type TmplData struct { QueryString string Error bool } func renderTemplate(w http.ResponseWriter, tmpl string, p interface{}) { err := templates.ExecuteTemplate(w, tmpl, p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // handleSSOGetRequest presents the login form func handleSSOGetRequest(w http.ResponseWriter, r *http.Request) { err := false if r.URL.Query().Get("auth_error") != "" { err = true } tmplData := TmplData{QueryString: r.URL.Query().Get("s_url"), Error: err} renderTemplate(w, "templates/login.html", &tmplData) } // handleSSOPostRequest sets the sso cookie. func handleSSOPostRequest(w http.ResponseWriter, r *http.Request) { r.ParseForm() p_uri := r.PostFormValue("query_string") u, g, err := lsso.Auth(r.PostFormValue("username"), r.PostFormValue("password")) if u != nil { vh := lsso.CTValidHours() exp := time.Now().Add(time.Hour * time.Duration(vh)).UTC() tok, _ := lsso.BuildJWTToken(*u, *g, exp) c := lsso.BuildCookie(tok, exp) http.SetCookie(w, &c) http.Redirect(w, r, p_uri, 301) return } if err != nil { if sso.Err401Map[err] { log.Println(err) http.Redirect(w, r, fmt.Sprintf("/sso?s_url=%s&auth_error=true", p_uri), 301) return } log.Println(err) w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Not able to service this request. Please try again later.") return } } // handleAuthTokenRequest generates the raw jwt token and sends it across. func handleAuthTokenRequest(w http.ResponseWriter, r *http.Request) { r.ParseForm() u, g, err := lsso.Auth(r.PostFormValue("username"), r.PostFormValue("password")) if u != nil { tok, _ := lsso.BuildJWTToken(*u, *g, time.Now().Add(time.Hour*time.Duration(lsso.CTValidHours())).UTC()) w.WriteHeader(http.StatusOK) fmt.Fprint(w, tok) return } if err != nil { if sso.Err401Map[err] { log.Println(err) fmt.Fprintf(w, "Unauthorized.") return } w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Not able to service the request. Please try again later.") return } } // handleLogoutRequest function invalidates the sso cookie. func handleLogoutRequest(w http.ResponseWriter, r *http.Request) { expT := time.Now().Add(time.Hour * time.Duration(-1)) lc := lsso.Logout(expT) http.SetCookie(w, &lc) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "You have been logged out.") return } // handleTestRequest function is just for the purpose of testing. func handleTestRequest(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "You have visited a test page.") return } func main() { log.Println("Starting login server.") r := mux.NewRouter() var fh *os.File var err error wld := ldap.BaseConf.WeblogDir if wld != "" { fh, err = weblog.SetupWebLog(wld, time.Now()) if err != nil { log.Fatalf("Failed to set up logging: %v", err) } } else { fh = os.Stdout // logs web accesses to stdout. May not be thread safe. } r.Handle("/sso", handlers.CombinedLoggingHandler(fh, http.HandlerFunc(handleSSOPostRequest))).Methods("POST") r.Handle("/sso", handlers.CombinedLoggingHandler(fh, http.HandlerFunc(handleSSOGetRequest))).Methods("GET") r.Handle("/logout", handlers.CombinedLoggingHandler(fh, http.HandlerFunc(handleLogoutRequest))).Methods("GET") r.Handle("/auth_token", handlers.CombinedLoggingHandler(fh, http.HandlerFunc(handleAuthTokenRequest))).Methods("POST") r.Handle("/test", handlers.CombinedLoggingHandler(fh, http.HandlerFunc(handleTestRequest))).Methods("GET") http.Handle("/", r) err = http.ListenAndServeTLS(":8081", ldap.BaseConf.SSLCertPath, ldap.BaseConf.SSLKeyPath, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } ================================================ FILE: ssl_certs/README.md ================================================ This directory contains the ssl certificate and the ssl key. simple-sso runs on https and the cert and the key are passed to ListenAndServeTLS. ================================================ FILE: ssl_certs/cert.pem ================================================ -----BEGIN CERTIFICATE----- MIICfzCCAegCCQDGlpg4vnJQDjANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMC SU4xEjAQBgNVBAgMCUtBUk5BVEFLQTESMBAGA1UEBwwJQkFOR0FMT1JFMQ4wDAYD VQQKDAVzYW1pdDEOMAwGA1UECwwFc2FtaXQxEjAQBgNVBAMMCTEyNy4wLjAuMTEY MBYGCSqGSIb3DQEJARYJYWFhQGcuY29tMB4XDTE2MDgwODA2MTUxMVoXDTE3MDgw OTA2MTUxMVowgYMxCzAJBgNVBAYTAklOMRIwEAYDVQQIDAlLQVJOQVRBS0ExEjAQ BgNVBAcMCUJBTkdBTE9SRTEOMAwGA1UECgwFc2FtaXQxDjAMBgNVBAsMBXNhbWl0 MRIwEAYDVQQDDAkxMjcuMC4wLjExGDAWBgkqhkiG9w0BCQEWCWFhYUBnLmNvbTCB nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA2Mo/2ICS+hK5yo73ckX88w2TPUwT 8dLqna4XA1At6KjNbCCWce9UwRLAwj+bHNXKKMKtBP6acG9FZxdKYPcT3+qrnh/O 40wq/j6yB/ON3wQaGzLkIhr/3nrf/AeG9g47Gxrg6jXdSHmH3RNif3MjRD3X76HL 46+yb0C7bUDJwXcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQBKtrEIUN+iUDs5CBdz 0tdwfBa9Vq2ympprGOqwDwYsqFDz95SSPHg96R8WsnO/AL27oGDsc3pL3WFh4UZI T+lunFNXfm+gfDB/w5N63lO6WTCnmLmUKsHK0HDKbzUyuTPTiQ9owqMGiolo5KJY bqRppccEsPF07d1iUQkhTfrIKQ== -----END CERTIFICATE----- ================================================ FILE: ssl_certs/key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDYyj/YgJL6ErnKjvdyRfzzDZM9TBPx0uqdrhcDUC3oqM1sIJZx 71TBEsDCP5sc1coowq0E/ppwb0VnF0pg9xPf6queH87jTCr+PrIH843fBBobMuQi Gv/eet/8B4b2DjsbGuDqNd1IeYfdE2J/cyNEPdfvocvjr7JvQLttQMnBdwIDAQAB AoGAYYbjIBf/hwbjlE+q3DrGJ+XEhn/yPQkgyRznd3MbpB5Eg89JPypnG5C/LOQG ePtovduOkL+lZM16EH221VZyFqauNt/V5w24ufMQZ1xuJ4WgOeGssZNA7qb+XbqI p0Mck5CDmJO97F6zxQsaOFX7bxX4mx93CC1o7RHAY+daxykCQQD4YRh/gEztP2F1 zVcqSPaxbfxCkBFiAgDyf1WXfHCL86zOMzbR9okOvVWgQcLEivhRKg6Vd/qcyjmQ xq3jQ/fDAkEA33EI6YqEqSlizTtTV25TYI8AoJ0c7DMMo3Ns1fDtB4DT/mH+PkGf ajv0+lHGT0Z3ugIRf55wgcmmcnoESMXoPQJAGjb9Q+/BrsSiv7E1gvQCfYWTO19D RmnZub5wxTVQF6VXVsgXACAaJSEcmXZ3XREh1kcvFN196PB7FOmzTqpMywJBAMTX aJmNTRdVfVP+CorAh7VN5aiZIKy4wE6SVfQXnkj45kl4/KjN2OmWzldjiQe3tavp PI8n/kdoZTj+Yx3VM6UCQDVKlhN6gp+3bW9TUT/eBN0vR0GKBmExzAmNcBRGJjLB SDbx5tNKi0xhBIECGGWwpuZTZMCoh5LtJpD2z7BmiHA= -----END RSA PRIVATE KEY----- ================================================ FILE: sso/sso.go ================================================ package sso import ( "errors" "net/http" "os" "strconv" "time" ) var ( ErrUnAuthorized = errors.New("Not Authorized") ErrUserNotFound = errors.New("User Not Found") ) // SSOImplementer is what it needs to be implemented for sso functionality. type SSOer interface { // Auth takes user,password strings as arguments and returns the user, user roles (e.g ldap groups) // (string slice) if the call succeds. Auth should return the ErrUnAuthorized or ErrUserNotFound error if // auth fails or if the user is not found respectively. Auth(string, string) (*string, *[]string, error) // CTValidHours returns the cookie/jwt token validity in hours. CTValidHours() int64 CookieName() string CookieDomain() string // BuildJWTToken takes the user and the user roles info which is then signed by the private // key of the login server. The expiry of the token is set per the third argument. BuildJWTToken(string, []string, time.Time) (string, error) // BuildCookie takes the jwt token and returns a cookie and sets the expiration time of the same to that of // the second arg. BuildCookie(string, time.Time) http.Cookie // Logout sets the expiration time of the cookie in the past rendering it unusable. Logout(time.Time) http.Cookie } var Err401Map = map[error]bool{ ErrUnAuthorized: true, ErrUserNotFound: true, } // All environment variables config goes here for better tracking. var ConfMap = map[string]string{ // ssl certs. "sso_ssl_cert_path": "sso_ssl_cert_path", "sso_ssl_key_path": "sso_ssl_key_path", // private key path for signing the jwt. "sso_private_key_path": "sso_private_key_path", // weblog dir path "sso_weblog_dir": "sso_weblog_dir", // User roles for authorization, (true/false) "sso_user_roles": "sso_user_roles", // cookie configs. "sso_cookie_name": "sso_cookie_name", "sso_cookie_domain": "sso_cookie_domain", "sso_cookie_validhours": "sso_cookie_validhours", // ldap configs. This should go into the respective package. "sso_ldap_host": "sso_ldap_host", "sso_ldap_port": "sso_ldap_port", "sso_ldap_ssl": "sso_ldap_ssl", "sso_ldap_basedn": "sso_ldap_basedn", "sso_ldap_binddn": "sso_ldap_binddn", "sso_ldap_bindpasswd": "sso_ldap_bindpasswd", } // setDefaultString returns a given default string. func setDefaultString(s string, d string) string { if s == "" { return d } return s } type BaseConfig struct { SSLCertPath string SSLKeyPath string PrivateKeyPath string WeblogDir string UserRoles bool } // SetupBaseConfig function setups some generic configs func SetupBaseConfig() (*BaseConfig, error) { sslCertPath := setDefaultString(os.Getenv(ConfMap["sso_ssl_cert_path"]), "ssl_certs/cert.pem") sslKeyPath := setDefaultString(os.Getenv(ConfMap["sso_ssl_key_path"]), "ssl_certs/key.pem") privateKeyPath := setDefaultString(os.Getenv(ConfMap["sso_private_key_path"]), "key_pair/demo.rsa") weblogDir := setDefaultString(os.Getenv(ConfMap["sso_weblog_dir"]), "") userRoles, err := strconv.ParseBool(setDefaultString(os.Getenv(ConfMap["sso_user_roles"]), "false")) if err != nil { return nil, err } return &BaseConfig{sslCertPath, sslKeyPath, privateKeyPath, weblogDir, userRoles}, nil } type CookieConfig struct { Name string Domain string ValidHours int64 } // SetupCookieConfig sets up cookie config. func SetupCookieConfig() (*CookieConfig, error) { name := setDefaultString(os.Getenv(ConfMap["sso_cookie_name"]), "SSO_C") domain := setDefaultString(os.Getenv(ConfMap["sso_cookie_domain"]), "127.0.0.1") validHours, err := strconv.Atoi(setDefaultString(os.Getenv(ConfMap["sso_cookie_validhours"]), "20")) if err != nil { return nil, err } return &CookieConfig{name, domain, int64(validHours)}, nil } ================================================ FILE: sso/sso_test.go ================================================ package sso import ( "os" "reflect" "testing" ) func TestSetupDefaultString(t *testing.T) { s := "return_me" d := "not_me" r := setDefaultString(s, d) if r != s { t.Errorf("Got: %s Want: %s", r, s) } s1 := "" d1 := "return_me" r = setDefaultString(s1, d1) if r != d1 { t.Errorf("Got: %s Want: %s", r, d1) } } func TestSetupBaseConfig(t *testing.T) { expBaseConfig := BaseConfig{ "ssl_certs/cert.pem", "ssl_certs/key.pem", "key_pair/demo.rsa", "", false, } b, err := SetupBaseConfig() if err != nil { t.Error(err) } if !reflect.DeepEqual(expBaseConfig, *b) { t.Errorf("Got: %v\n\t Want: %v", *b, expBaseConfig) } expBaseConfig = BaseConfig{ "ssl_certs/certreal.pem", "ssl_certs/keyreal.pem", "key_pair/privatereal.rsa", "/tmp/weblog", true, } os.Setenv(ConfMap["sso_ssl_cert_path"], "ssl_certs/certreal.pem") os.Setenv(ConfMap["sso_ssl_key_path"], "ssl_certs/keyreal.pem") os.Setenv(ConfMap["sso_private_key_path"], "key_pair/privatereal.rsa") os.Setenv(ConfMap["sso_weblog_dir"], "/tmp/weblog") os.Setenv(ConfMap["sso_user_roles"], "true") b, err = SetupBaseConfig() if err != nil { t.Error(err) } if !reflect.DeepEqual(expBaseConfig, *b) { t.Errorf("Got: %v\n\t Want: %v", *b, expBaseConfig) } } func TestSetupCookieConfig(t *testing.T) { expCookie := CookieConfig{ "SSO_C", "127.0.0.1", 20, } c, err := SetupCookieConfig() if err != nil { t.Error(err) } if !reflect.DeepEqual(expCookie, *c) { t.Errorf("Got: %v\n Want: %v", *c, expCookie) } expCookie = CookieConfig{ "Cookie", "abc.com", 10, } os.Setenv((ConfMap["sso_cookie_name"]), "Cookie") os.Setenv((ConfMap["sso_cookie_domain"]), "abc.com") os.Setenv((ConfMap["sso_cookie_validhours"]), "10") c, err = SetupCookieConfig() if err != nil { t.Error(err) } if !reflect.DeepEqual(expCookie, *c) { t.Errorf("Got: %v\n Want: %v", *c, expCookie) } } ================================================ FILE: templates/footer.html ================================================ {{ define "footer" }} {{ end }} ================================================ FILE: templates/header.html ================================================ {{ define "header" }} Login {{ end }} ================================================ FILE: templates/login.html ================================================ {{ template "header" }}

SSO Login Form {{ if .Error }} Invalid credentials. {{ end }}

{{ template "footer" }} ================================================ FILE: util/test/test_key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAoWydaG49rGhkMw/SfLPhiXZtx1hbzmdLYz0BEiu+hhWH2PCs d83J/nymDPDdVaQrT0HQ2v3y0SbYeKvBCViyfSXi6cnpRzNTzQyQfypT79xHc1+i jwUvpG1Ts6zZwfwJGbKgZsz8EDp2Jna/fPTOnEHOfRhvfRR9H0RUNsLgPiJ1YUH5 oWeE7sQk/l9CGtkhXKsYVbUV3bx1DQw9TTQtgFKT4VQCNJ3KEHowD6Xdcw8Dzwab jAnPB9zI+7yWWAjZPSNJlAjTpV3rE4Yj1vd8hEuk2wIgV6INLLuVHmShy8YrUkOW upyU2So/pRcLwRR+TBXcqsYFpIt+tIAQth7eDwIDAQABAoIBAGioPN3KK54uCFi6 t2M2VNGEwOPvu4X0noH2uU0Io3vXVb4nPApol7+xHQ9i0n2F9LZsG3cAEn/byZli 8cKXiRFukNG2oNISyxA0RzLLRKRMkt6QcJp9aEgYwZ3KQVxthZDtqOU9nWcAID4L 21aueY4BdFjSkOXtdLni2R6v9icRtJWPtsiVBmIk+9UrFmDraDT//rPAe8gClvoC pCedLPWzTLuDDXMlfFnfK6QZ8iChY5osH/yxEzXats5u+TM2ZLiONvdrRYzq9ioi 6l+agEyRtT+KjzFTcu707HMzmcXiNkh7C/okLER260GlG3yqGab7kSoE61J7AKpY pEgH/zECgYEAz3ul2xY9l26BNHfRF+VK5aayNZ+9YL1yX95jA7fJAK7u1ZlMhYBK WTdW+1X8spuNlvUX1oKekEiPPW5ViBlDhAIkgC7c5kAMk3+mjaYDYwq5yCRbTx7N y51MWBaqMEd2Izn+PKhwKeJkqsiMbWBuAhDSyI7I3pxiELPc4oUIfI0CgYEAxyvM dh7WHWqZfnwHvS6FDnYFrzL0OljG7P4ElhO94dBpefczKYXU8a1veQEbbE4wGVw3 wUKgG8wHLota+940lwqPpC/1M/Je51zGVs4/YDOJJrRQqwxyDELbuifRHY5pGCpK Hu9cRw0qWCmrsFVQWclPleCL1tfslTd+IqHAlAsCgYAowfdgxEuxFaoX7nmKoiZG WqqjUg/Xkx+GqZ71ugKoObT9DLI1f3Aben2BvfB3/Yqg3uCh6OLRIQ/SV3xB0gSr R+h3rb0DFg3iY68KIFSF/jNkl4/ASSLQHsRCgaFI/qC8ZsYEkGoIMErqKZ88VTcG /NsLPtFCuaGh+lMnxE5YeQKBgQC6wOPPoixsmsbgZdYv2o3iuGGuHJ4Kk7G7CJgu TMaQFYbBWTw85AN+tXw/vv0CufG55dFVwm40gkP9raebYYh4U+vKLTnDArFgSYqk XHHqd4hTpWG6cUoDGzHCxJD9IMqEYSrtBM3GxZ592lzlU6mq9utMAqe8xOxOIiGA waC8bwKBgCHC8s0pxjsuqBa3uyg8QgiGHS0dnsMV8mvrzf9DkeQ/DnhZjblKltJl UFD2glN8EdKr/+cZsMIrQm70SqGa/jAdzwPzGBOoMdDyCM9OmaHGWsgkXqmA9Cr0 V7JoxLW7cgJztNfu02XS9BE8Ua6qx4rqCLsh8XKmtT1pA/XLPuCD -----END RSA PRIVATE KEY----- ================================================ FILE: util/test/test_key.pub ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoWydaG49rGhkMw/SfLPh iXZtx1hbzmdLYz0BEiu+hhWH2PCsd83J/nymDPDdVaQrT0HQ2v3y0SbYeKvBCViy fSXi6cnpRzNTzQyQfypT79xHc1+ijwUvpG1Ts6zZwfwJGbKgZsz8EDp2Jna/fPTO nEHOfRhvfRR9H0RUNsLgPiJ1YUH5oWeE7sQk/l9CGtkhXKsYVbUV3bx1DQw9TTQt gFKT4VQCNJ3KEHowD6Xdcw8DzwabjAnPB9zI+7yWWAjZPSNJlAjTpV3rE4Yj1vd8 hEuk2wIgV6INLLuVHmShy8YrUkOWupyU2So/pRcLwRR+TBXcqsYFpIt+tIAQth7e DwIDAQAB -----END PUBLIC KEY----- ================================================ FILE: util/util.go ================================================ package util import ( "crypto/rsa" jwt "github.com/dgrijalva/jwt-go" ) type CustomClaims struct { User string `json:"user"` Roles []string `json:"roles"` jwt.StandardClaims } // GenJWT generates the jwt token. Among other stuff, it packs in the authenticated user name and the roles that the // user belongs to and an expiration time. The info is then signed by the private key of the login server. func GenJWT(u string, g []string, p *rsa.PrivateKey, t int64) (string, error) { claims := CustomClaims{ u, g, jwt.StandardClaims{ ExpiresAt: t, Issuer: "Login_Server", }, } token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims) return token.SignedString(p) } ================================================ FILE: util/util_test.go ================================================ package util import ( "io/ioutil" jwt "github.com/dgrijalva/jwt-go" "testing" ) func TestGenJWT(t *testing.T) { keyData, _ := ioutil.ReadFile("test/test_key.pem") key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData) signature := "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdF9hY2NudCIsInJvbGVzIjpbInJvbGUxIiwicm9sZTIiXSwiZXhwIjoxMjM0LCJpc3MiOiJMb2dpbl9TZXJ2ZXIifQ.hYZ38sZjFUenWVlMlimDxd2z8M1LFTR9zs8_O9RGnxM8n0UJO8GGn12qY2-XrBCv2BLIh2bJXvCee2hDSZO8F9jvKXXMJyYoEtABYrA5MSYm33J1BfcWYsBqKAIFKiTtDrns297OX9nkLyt4_q3J7qUU8EjE6d1Xhc_vqvL-FVjlETwuAqbUBlkRdb_5yNQ03bNzVi7lvIOMEQ4qyOWw3DkudFDGTRQqaHuYT0MgKWU5A_CyEYSOsuIO6ZI77gQyFOrkc2vM1kSo9xPVEoF_34A5w1TWuySJ6c7Sc7JiSOWA5zrTsX6TavvejhfbTeqK5MTfD4AD9wBS_gVeSgdp7Q" u := "test_accnt" r := []string{"role1", "role2"} s, err := GenJWT(u, r, key, 1234) if err !=nil { t.Errorf("Error: %v", err) } if signature != s { t.Errorf("Got: %s\n Want: %s", s, signature) } }