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