auth

Paddy 2015-01-18 Parent:e000b1c24fc0 Child:565a9335e035

117:da77e083cf02 Go to Latest

auth/client.go

Change TODO to BUG. This is a refactor, not code to be added. It should show up in the gofmt bugs list.

History
1 package auth
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "log"
9 "net/http"
10 "net/url"
11 "strconv"
12 "time"
14 "github.com/PuerkitoBio/purell"
15 "github.com/gorilla/mux"
17 "code.secondbit.org/uuid.hg"
18 )
20 var (
21 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
22 ErrNoClientStore = errors.New("no clientStore was specified for the Context")
23 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
24 ErrClientNotFound = errors.New("client not found in clientStore")
25 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
26 // the same ID already exists in the clientStore.
27 ErrClientAlreadyExists = errors.New("client already exists in clientStore")
29 // ErrEmptyChange is returned when a Change has all its properties set to nil.
30 ErrEmptyChange = errors.New("change must have at least one property set")
31 // ErrClientNameTooShort is returned when a Client's Name property is too short.
32 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
33 // ErrClientNameTooLong is returned when a Client's Name property is too long.
34 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
35 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
36 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
37 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
38 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
39 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
40 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
41 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
42 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
43 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
44 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
45 )
47 const (
48 clientTypePublic = "public"
49 clientTypeConfidential = "confidential"
50 minClientNameLen = 2
51 maxClientNameLen = 24
52 )
54 // Client represents a client that grants access
55 // to the auth server, exchanging grants for tokens,
56 // and tokens for access.
57 type Client struct {
58 ID uuid.ID `json:"id,omitempty"`
59 Secret string `json:"secret,omitempty"`
60 OwnerID uuid.ID `json:"owner_id,omitempty"`
61 Name string `json:"name,omitempty"`
62 Logo string `json:"logo,omitempty"`
63 Website string `json:"website,omitempty"`
64 Type string `json:"type,omitempty"`
65 }
67 // ApplyChange applies the properties of the passed
68 // ClientChange to the Client object it is called on.
69 func (c *Client) ApplyChange(change ClientChange) {
70 if change.Secret != nil {
71 c.Secret = *change.Secret
72 }
73 if change.OwnerID != nil {
74 c.OwnerID = change.OwnerID
75 }
76 if change.Name != nil {
77 c.Name = *change.Name
78 }
79 if change.Logo != nil {
80 c.Logo = *change.Logo
81 }
82 if change.Website != nil {
83 c.Website = *change.Website
84 }
85 }
87 // ClientChange represents a bundle of options for
88 // updating a Client's mutable data.
89 type ClientChange struct {
90 Secret *string
91 OwnerID uuid.ID
92 Name *string
93 Logo *string
94 Website *string
95 }
97 // Validate checks the ClientChange it is called on
98 // and asserts its internal validity, or lack thereof.
99 func (c ClientChange) Validate() error {
100 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
101 return ErrEmptyChange
102 }
103 if c.Name != nil && len(*c.Name) < 2 {
104 return ErrClientNameTooShort
105 }
106 if c.Name != nil && len(*c.Name) > 32 {
107 return ErrClientNameTooLong
108 }
109 if c.Logo != nil && *c.Logo != "" {
110 if len(*c.Logo) > 1024 {
111 return ErrClientLogoTooLong
112 }
113 u, err := url.Parse(*c.Logo)
114 if err != nil || !u.IsAbs() {
115 return ErrClientLogoNotURL
116 }
117 }
118 if c.Website != nil && *c.Website != "" {
119 if len(*c.Website) > 140 {
120 return ErrClientWebsiteTooLong
121 }
122 u, err := url.Parse(*c.Website)
123 if err != nil || !u.IsAbs() {
124 return ErrClientWebsiteNotURL
125 }
126 }
127 return nil
128 }
130 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
131 enc := json.NewEncoder(w)
132 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
133 if !fromAuthHeader {
134 if !allowPublic {
135 w.WriteHeader(http.StatusBadRequest)
136 renderJSONError(enc, "unauthorized_client")
137 return nil, false
138 }
139 clientIDStr = r.PostFormValue("client_id")
140 }
141 clientID, err := uuid.Parse(clientIDStr)
142 if err != nil {
143 w.WriteHeader(http.StatusUnauthorized)
144 if fromAuthHeader {
145 w.Header().Set("WWW-Authenticate", "Basic")
146 }
147 renderJSONError(enc, "invalid_client")
148 return nil, false
149 }
150 client, err := context.GetClient(clientID)
151 if err == ErrClientNotFound {
152 w.WriteHeader(http.StatusUnauthorized)
153 if fromAuthHeader {
154 w.Header().Set("WWW-Authenticate", "Basic")
155 }
156 renderJSONError(enc, "invalid_client")
157 return nil, false
158 } else if err != nil {
159 w.WriteHeader(http.StatusInternalServerError)
160 renderJSONError(enc, "server_error")
161 return nil, false
162 }
163 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
164 w.WriteHeader(http.StatusUnauthorized)
165 if fromAuthHeader {
166 w.Header().Set("WWW-Authenticate", "Basic")
167 }
168 renderJSONError(enc, "invalid_client")
169 return nil, false
170 }
171 return clientID, true
172 }
174 // Endpoint represents a single URI that a Client
175 // controls. Users will be redirected to these URIs
176 // following successful authorization grants and
177 // exchanges for access tokens.
178 type Endpoint struct {
179 ID uuid.ID `json:"id,omitempty"`
180 ClientID uuid.ID `json:"client_id,omitempty"`
181 URI string `json:"uri,omitempty"`
182 NormalizedURI string `json:"-"`
183 Added time.Time `json:"added,omitempty"`
184 }
186 func normalizeURIString(in string) (string, error) {
187 n, err := purell.NormalizeURLString(in, purell.FlagsUsuallySafeNonGreedy|purell.FlagSortQuery)
188 if err != nil {
189 log.Println(err)
190 return in, ErrEndpointURINotURL
191 }
192 return n, nil
193 }
195 func normalizeURI(in *url.URL) string {
196 return purell.NormalizeURL(in, purell.FlagsUsuallySafeNonGreedy|purell.FlagSortQuery)
197 }
199 type sortedEndpoints []Endpoint
201 func (s sortedEndpoints) Len() int {
202 return len(s)
203 }
205 func (s sortedEndpoints) Less(i, j int) bool {
206 return s[i].Added.Before(s[j].Added)
207 }
209 func (s sortedEndpoints) Swap(i, j int) {
210 s[i], s[j] = s[j], s[i]
211 }
213 type clientStore interface {
214 getClient(id uuid.ID) (Client, error)
215 saveClient(client Client) error
216 updateClient(id uuid.ID, change ClientChange) error
217 deleteClient(id uuid.ID) error
218 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
220 addEndpoints(client uuid.ID, endpoint []Endpoint) error
221 removeEndpoint(client, endpoint uuid.ID) error
222 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
223 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
224 countEndpoints(client uuid.ID) (int64, error)
225 }
227 func (m *memstore) getClient(id uuid.ID) (Client, error) {
228 m.clientLock.RLock()
229 defer m.clientLock.RUnlock()
230 c, ok := m.clients[id.String()]
231 if !ok {
232 return Client{}, ErrClientNotFound
233 }
234 return c, nil
235 }
237 func (m *memstore) saveClient(client Client) error {
238 m.clientLock.Lock()
239 defer m.clientLock.Unlock()
240 if _, ok := m.clients[client.ID.String()]; ok {
241 return ErrClientAlreadyExists
242 }
243 m.clients[client.ID.String()] = client
244 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
245 return nil
246 }
248 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
249 m.clientLock.Lock()
250 defer m.clientLock.Unlock()
251 c, ok := m.clients[id.String()]
252 if !ok {
253 return ErrClientNotFound
254 }
255 c.ApplyChange(change)
256 m.clients[id.String()] = c
257 return nil
258 }
260 func (m *memstore) deleteClient(id uuid.ID) error {
261 client, err := m.getClient(id)
262 if err != nil {
263 return err
264 }
265 m.clientLock.Lock()
266 defer m.clientLock.Unlock()
267 delete(m.clients, id.String())
268 pos := -1
269 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
270 if item.Equal(id) {
271 pos = p
272 break
273 }
274 }
275 if pos >= 0 {
276 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
277 }
278 return nil
279 }
281 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
282 ids := m.lookupClientsByProfileID(ownerID.String())
283 if len(ids) > num+offset {
284 ids = ids[offset : num+offset]
285 } else if len(ids) > offset {
286 ids = ids[offset:]
287 } else {
288 return []Client{}, nil
289 }
290 clients := []Client{}
291 for _, id := range ids {
292 client, err := m.getClient(id)
293 if err != nil {
294 return []Client{}, err
295 }
296 clients = append(clients, client)
297 }
298 return clients, nil
299 }
301 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
302 m.endpointLock.Lock()
303 defer m.endpointLock.Unlock()
304 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
305 return nil
306 }
308 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
309 m.endpointLock.Lock()
310 defer m.endpointLock.Unlock()
311 pos := -1
312 for p, item := range m.endpoints[client.String()] {
313 if item.ID.Equal(endpoint) {
314 pos = p
315 break
316 }
317 }
318 if pos >= 0 {
319 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
320 }
321 return nil
322 }
324 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
325 m.endpointLock.RLock()
326 defer m.endpointLock.RUnlock()
327 for _, candidate := range m.endpoints[client.String()] {
328 if endpoint == candidate.NormalizedURI {
329 return true, nil
330 }
331 }
332 return false, nil
333 }
335 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
336 m.endpointLock.RLock()
337 defer m.endpointLock.RUnlock()
338 return m.endpoints[client.String()], nil
339 }
341 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
342 m.endpointLock.RLock()
343 defer m.endpointLock.RUnlock()
344 return int64(len(m.endpoints[client.String()])), nil
345 }
347 type newClientReq struct {
348 Name string `json:"name"`
349 Logo string `json:"logo"`
350 Website string `json:"website"`
351 Type string `json:"type"`
352 Endpoints []string `json:"endpoints"`
353 }
355 func RegisterClientHandlers(r *mux.Router, context Context) {
356 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
357 }
359 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
360 errors := []requestError{}
361 username, password, ok := r.BasicAuth()
362 if !ok {
363 errors = append(errors, requestError{Slug: requestErrAccessDenied})
364 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
365 return
366 }
367 profile, err := authenticate(username, password, c)
368 if err != nil {
369 errors = append(errors, requestError{Slug: requestErrAccessDenied})
370 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
371 return
372 }
373 var req newClientReq
374 decoder := json.NewDecoder(r.Body)
375 err = decoder.Decode(&req)
376 if err != nil {
377 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
378 return
379 }
380 if req.Type == "" {
381 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
382 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
383 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
384 }
385 if req.Name == "" {
386 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
387 } else if len(req.Name) < minClientNameLen {
388 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
389 } else if len(req.Name) > maxClientNameLen {
390 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
391 }
392 if len(errors) > 0 {
393 encode(w, r, http.StatusBadRequest, response{Errors: errors})
394 return
395 }
396 client := Client{
397 ID: uuid.NewID(),
398 OwnerID: profile.ID,
399 Name: req.Name,
400 Logo: req.Logo,
401 Website: req.Website,
402 Type: req.Type,
403 }
404 if client.Type == clientTypePublic {
405 secret := make([]byte, 32)
406 _, err = rand.Read(secret)
407 if err != nil {
408 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
409 return
410 }
411 client.Secret = hex.EncodeToString(secret)
412 }
413 err = c.SaveClient(client)
414 if err != nil {
415 if err == ErrClientAlreadyExists {
416 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
417 encode(w, r, http.StatusBadRequest, response{Errors: errors})
418 return
419 }
420 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
421 return
422 }
423 endpoints := []Endpoint{}
424 for pos, u := range req.Endpoints {
425 uri, err := url.Parse(u)
426 if err != nil {
427 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
428 continue
429 }
430 if !uri.IsAbs() {
431 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
432 continue
433 }
434 endpoint := Endpoint{
435 ID: uuid.NewID(),
436 ClientID: client.ID,
437 URI: uri.String(),
438 Added: time.Now(),
439 }
440 endpoints = append(endpoints, endpoint)
441 }
442 err = c.AddEndpoints(client.ID, endpoints)
443 if err != nil {
444 errors = append(errors, requestError{Slug: requestErrActOfGod})
445 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
446 return
447 }
448 resp := response{
449 Clients: []Client{client},
450 Endpoints: endpoints,
451 Errors: errors,
452 }
453 encode(w, r, http.StatusCreated, resp)
454 }