master
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}