auth

Paddy 2015-01-19 Parent:d14f0a81498c Child:23c1a07c8a61

126:34de07217709 Go to Latest

auth/client.go

Test around client types and secrets. Implement a test that the CreateClient handler will correctly create a confidential client and issue a secret for it. Also, just generally test that clients that are confidential are issued secrets and clients that are public are not.

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