master
Raw Download raw file
  1// proxyauth package provides data models and associated functions required to
  2// facilitate a proxy authentication server as specified in SPEC.md
  3package proxyauth
  4
  5import (
  6	"crypto/sha256"
  7	"encoding/base64"
  8	"encoding/json"
  9	"fmt"
 10	"net/http"
 11	"os"
 12
 13	log "github.com/Sirupsen/logrus"
 14	"github.com/gorilla/mux"
 15)
 16
 17// Proxy data model of a slice of Domains searched when authentication request is made.
 18// Proxy is the highest level data model in proxyauth, the golang type analogous to users.json
 19type Proxy struct {
 20	Domains []Domain
 21}
 22
 23// Domains are identified by their Address (unique) and contain a slice of all the
 24// registered Users for that domain
 25type Domain struct {
 26	Address string `json:"domain"`
 27	Users   []User `json:"users"`
 28}
 29
 30// Users are identified by their Username (unique) and each has a Base 64 encoded, SHA 256 digest
 31// of the users password.  See SPEC.md or b64sha256 for specific implementation details
 32type User struct {
 33	Username string `json:"username"`
 34	Password string `json:"password"`
 35}
 36
 37// Response is used for communicating the result from an attempted authentication
 38type Response struct {
 39	Success bool   `json:"access_granted"`
 40	Reason  string `json:"reason,omitempty"`
 41}
 42
 43// generate and return base64 encoded sha256 digest of provided password
 44func b64sha256(password string) string {
 45	s256 := sha256.New()
 46	s256.Write([]byte(password))
 47	return base64.StdEncoding.EncodeToString(s256.Sum(nil))
 48}
 49
 50// Parse the json users file (filePath) and return the proxy data type.
 51func NewProxy(filePath string) (*Proxy, error) {
 52	usersJson, err := os.Open(filePath)
 53	if err != nil {
 54		return nil, err
 55	}
 56
 57	p := &Proxy{}
 58	err = json.NewDecoder(usersJson).Decode(&p.Domains)
 59	if err != nil {
 60		return nil, err
 61	}
 62
 63	for i, d := range p.Domains {
 64		for j, u := range d.Users {
 65			p.Domains[i].Users[j].Password = "{SHA256}" + b64sha256(u.Password)
 66			log.WithFields(log.Fields{
 67				"domain": d.Address,
 68				"user":   u.Username,
 69			}).Info("User Initalized")
 70		}
 71	}
 72	return p, nil
 73
 74}
 75
 76// search within the proxy for a domain
 77func (p *Proxy) get(reqDomain string) (*Domain, error) {
 78	for i, d := range p.Domains {
 79		if d.Address == reqDomain {
 80			return &p.Domains[i], nil
 81		}
 82	}
 83	return nil, fmt.Errorf("No such domain")
 84}
 85
 86// search within a domain for a user
 87func (d *Domain) get(reqUser string) (*User, error) {
 88	for i, u := range d.Users {
 89		if u.Username == reqUser {
 90			return &d.Users[i], nil
 91		}
 92	}
 93	return nil, fmt.Errorf("No such user")
 94}
 95
 96// Per policy: in case of authentication failure or validation errors.
 97// The 'reason' is always is always same: "denied by policy".
 98// Additionally, success is always simply access_granted: true.
 99// Having this separated as a function will be useful if different
100// responses are needed in the future
101// Assumption: 200 OK is golang default
102func writeSuccess(w http.ResponseWriter, success bool) {
103	var r *Response
104	w.Header().Set("Content-Type", "application/json")
105	out := json.NewEncoder(w)
106	if success {
107		r = &Response{
108			Success: success,
109		}
110	} else {
111		r = &Response{
112			Success: success,
113			Reason:  "denied by policy",
114		}
115	}
116
117	out.Encode(r)
118}
119
120// used for testing, less performant than using io.Writer interface
121func successBody(success bool) string {
122	var r *Response
123	if success {
124		r = &Response{
125			Success: success,
126		}
127	} else {
128		r = &Response{
129			Success: success,
130			Reason:  "denied by policy",
131		}
132	}
133	// live dangerously, ignoring error for this specific use case
134	b, _ := json.Marshal(r)
135	return string(b) + "\n" // http body fix, expects newline at end
136}
137
138// Proxy Authentication handler expects: domain (mux url variable) username and password
139// (query parameters). HTTP response returns the appropriate json response and Status
140// Code as specified in SPEC.md
141func (p *Proxy) Authenticate() http.Handler {
142	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143		logfields := log.Fields{
144			"Method": r.Method,
145		}
146
147		// domain lookup
148		vars := mux.Vars(r)
149		urlDomain, ok := vars["domain"]
150		if !ok {
151			w.WriteHeader(500) // Server error
152			logfields["Status"] = 500
153			log.WithFields(logfields).Warn("Domain parsing failed")
154			return
155		}
156		d, err := p.get(urlDomain)
157		logfields["Domain"] = urlDomain
158		if err != nil {
159			w.WriteHeader(404) // No such domain
160			logfields["Status"] = 404
161			log.WithFields(logfields).Info("No such domain")
162			return
163		}
164
165		// parse parameters
166		err = r.ParseForm()
167		if err != nil {
168			w.WriteHeader(500)
169			logfields["Status"] = 500
170			log.WithFields(logfields).Warn("Parse form failure")
171			return
172		}
173		username := r.Form.Get("username")
174		if username == "" {
175			writeSuccess(w, false)
176			log.WithFields(logfields).Info("No username provided")
177			return
178		}
179		logfields["Username"] = username
180		password := r.Form.Get("password")
181		if password == "" {
182			writeSuccess(w, false)
183			log.WithFields(logfields).Info("No password provided")
184			return
185		}
186
187		// user lookup
188		u, err := d.get(username)
189		if err != nil {
190			writeSuccess(w, false)
191			log.WithFields(logfields).Info("No such user")
192			return
193		}
194
195		// password validation
196		if u.Password != password {
197			writeSuccess(w, false)
198			log.WithFields(logfields).Info("Password mismatch")
199			return
200		} else {
201			writeSuccess(w, true)
202			log.WithFields(logfields).Info("Successful authentication")
203			return
204		}
205
206	})
207}