auth
auth/client.go
Fill in gaps in AuthorizationCodeStore tests, add authCodeGrantValidate tests. Fill in some holes in AuthorizationCodeStore tests (mainly the lack of ProfileID and Used being compared, and the omission of useAuthorizationCode in the TestauthorizationCodeStore test). Start testing the authCodeGrantValidate function, that tests a grant claim made using an AuthorizationCode. Right now, we're only testing that omitting a code yields an invalid_request error.
1 package auth
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "github.com/gorilla/mux"
9 "net/http"
10 "net/url"
11 "time"
13 "code.secondbit.org/uuid.hg"
14 )
16 var (
17 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
18 ErrNoClientStore = errors.New("no clientStore was specified for the Context")
19 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
20 ErrClientNotFound = errors.New("client not found in clientStore")
21 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
22 // the same ID already exists in the clientStore.
23 ErrClientAlreadyExists = errors.New("client already exists in clientStore")
25 // ErrEmptyChange is returned when a Change has all its properties set to nil.
26 ErrEmptyChange = errors.New("change must have at least one property set")
27 // ErrClientNameTooShort is returned when a Client's Name property is too short.
28 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
29 // ErrClientNameTooLong is returned when a Client's Name property is too long.
30 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
31 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
32 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
33 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
34 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
35 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
36 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
37 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
38 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
39 )
41 // Client represents a client that grants access
42 // to the auth server, exchanging grants for tokens,
43 // and tokens for access.
44 type Client struct {
45 ID uuid.ID
46 Secret string
47 OwnerID uuid.ID
48 Name string
49 Logo string
50 Website string
51 Type string
52 }
54 // ApplyChange applies the properties of the passed
55 // ClientChange to the Client object it is called on.
56 func (c *Client) ApplyChange(change ClientChange) {
57 if change.Secret != nil {
58 c.Secret = *change.Secret
59 }
60 if change.OwnerID != nil {
61 c.OwnerID = change.OwnerID
62 }
63 if change.Name != nil {
64 c.Name = *change.Name
65 }
66 if change.Logo != nil {
67 c.Logo = *change.Logo
68 }
69 if change.Website != nil {
70 c.Website = *change.Website
71 }
72 }
74 // ClientChange represents a bundle of options for
75 // updating a Client's mutable data.
76 type ClientChange struct {
77 Secret *string
78 OwnerID uuid.ID
79 Name *string
80 Logo *string
81 Website *string
82 }
84 // Validate checks the ClientChange it is called on
85 // and asserts its internal validity, or lack thereof.
86 func (c ClientChange) Validate() error {
87 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
88 return ErrEmptyChange
89 }
90 if c.Name != nil && len(*c.Name) < 2 {
91 return ErrClientNameTooShort
92 }
93 if c.Name != nil && len(*c.Name) > 32 {
94 return ErrClientNameTooLong
95 }
96 if c.Logo != nil && *c.Logo != "" {
97 if len(*c.Logo) > 1024 {
98 return ErrClientLogoTooLong
99 }
100 u, err := url.Parse(*c.Logo)
101 if err != nil || !u.IsAbs() {
102 return ErrClientLogoNotURL
103 }
104 }
105 if c.Website != nil && *c.Website != "" {
106 if len(*c.Website) > 140 {
107 return ErrClientWebsiteTooLong
108 }
109 u, err := url.Parse(*c.Website)
110 if err != nil || !u.IsAbs() {
111 return ErrClientWebsiteNotURL
112 }
113 }
114 return nil
115 }
117 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
118 enc := json.NewEncoder(w)
119 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
120 if !fromAuthHeader {
121 if !allowPublic {
122 w.WriteHeader(http.StatusBadRequest)
123 renderJSONError(enc, "unauthorized_client")
124 return nil, false
125 }
126 clientIDStr = r.PostFormValue("client_id")
127 }
128 clientID, err := uuid.Parse(clientIDStr)
129 if err != nil {
130 w.WriteHeader(http.StatusUnauthorized)
131 if fromAuthHeader {
132 w.Header().Set("WWW-Authenticate", "Basic")
133 }
134 renderJSONError(enc, "invalid_client")
135 return nil, false
136 }
137 client, err := context.GetClient(clientID)
138 if err == ErrClientNotFound {
139 w.WriteHeader(http.StatusUnauthorized)
140 if fromAuthHeader {
141 w.Header().Set("WWW-Authenticate", "Basic")
142 }
143 renderJSONError(enc, "invalid_client")
144 return nil, false
145 } else if err != nil {
146 w.WriteHeader(http.StatusInternalServerError)
147 renderJSONError(enc, "server_error")
148 return nil, false
149 }
150 if client.Secret != clientSecret {
151 w.WriteHeader(http.StatusUnauthorized)
152 if fromAuthHeader {
153 w.Header().Set("WWW-Authenticate", "Basic")
154 }
155 renderJSONError(enc, "invalid_client")
156 return nil, false
157 }
158 return clientID, true
159 }
161 // Endpoint represents a single URI that a Client
162 // controls. Users will be redirected to these URIs
163 // following successful authorization grants and
164 // exchanges for access tokens.
165 type Endpoint struct {
166 ID uuid.ID
167 ClientID uuid.ID
168 URI url.URL
169 Added time.Time
170 }
172 type sortedEndpoints []Endpoint
174 func (s sortedEndpoints) Len() int {
175 return len(s)
176 }
178 func (s sortedEndpoints) Less(i, j int) bool {
179 return s[i].Added.Before(s[j].Added)
180 }
182 func (s sortedEndpoints) Swap(i, j int) {
183 s[i], s[j] = s[j], s[i]
184 }
186 type clientStore interface {
187 getClient(id uuid.ID) (Client, error)
188 saveClient(client Client) error
189 updateClient(id uuid.ID, change ClientChange) error
190 deleteClient(id uuid.ID) error
191 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
193 addEndpoint(client uuid.ID, endpoint Endpoint) error
194 removeEndpoint(client, endpoint uuid.ID) error
195 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
196 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
197 countEndpoints(client uuid.ID) (int64, error)
198 }
200 func (m *memstore) getClient(id uuid.ID) (Client, error) {
201 m.clientLock.RLock()
202 defer m.clientLock.RUnlock()
203 c, ok := m.clients[id.String()]
204 if !ok {
205 return Client{}, ErrClientNotFound
206 }
207 return c, nil
208 }
210 func (m *memstore) saveClient(client Client) error {
211 m.clientLock.Lock()
212 defer m.clientLock.Unlock()
213 if _, ok := m.clients[client.ID.String()]; ok {
214 return ErrClientAlreadyExists
215 }
216 m.clients[client.ID.String()] = client
217 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
218 return nil
219 }
221 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
222 m.clientLock.Lock()
223 defer m.clientLock.Unlock()
224 c, ok := m.clients[id.String()]
225 if !ok {
226 return ErrClientNotFound
227 }
228 c.ApplyChange(change)
229 m.clients[id.String()] = c
230 return nil
231 }
233 func (m *memstore) deleteClient(id uuid.ID) error {
234 client, err := m.getClient(id)
235 if err != nil {
236 return err
237 }
238 m.clientLock.Lock()
239 defer m.clientLock.Unlock()
240 delete(m.clients, id.String())
241 pos := -1
242 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
243 if item.Equal(id) {
244 pos = p
245 break
246 }
247 }
248 if pos >= 0 {
249 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
250 }
251 return nil
252 }
254 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
255 ids := m.lookupClientsByProfileID(ownerID.String())
256 if len(ids) > num+offset {
257 ids = ids[offset : num+offset]
258 } else if len(ids) > offset {
259 ids = ids[offset:]
260 } else {
261 return []Client{}, nil
262 }
263 clients := []Client{}
264 for _, id := range ids {
265 client, err := m.getClient(id)
266 if err != nil {
267 return []Client{}, err
268 }
269 clients = append(clients, client)
270 }
271 return clients, nil
272 }
274 func (m *memstore) addEndpoint(client uuid.ID, endpoint Endpoint) error {
275 m.endpointLock.Lock()
276 defer m.endpointLock.Unlock()
277 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint)
278 return nil
279 }
281 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
282 m.endpointLock.Lock()
283 defer m.endpointLock.Unlock()
284 pos := -1
285 for p, item := range m.endpoints[client.String()] {
286 if item.ID.Equal(endpoint) {
287 pos = p
288 break
289 }
290 }
291 if pos >= 0 {
292 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
293 }
294 return nil
295 }
297 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
298 m.endpointLock.RLock()
299 defer m.endpointLock.RUnlock()
300 for _, candidate := range m.endpoints[client.String()] {
301 if endpoint == candidate.URI.String() {
302 return true, nil
303 }
304 }
305 return false, nil
306 }
308 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
309 m.endpointLock.RLock()
310 defer m.endpointLock.RUnlock()
311 return m.endpoints[client.String()], nil
312 }
314 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
315 m.endpointLock.RLock()
316 defer m.endpointLock.RUnlock()
317 return int64(len(m.endpoints[client.String()])), nil
318 }
320 type newClientReq struct {
321 Name string `json:"name"`
322 Logo string `json:"logo"`
323 Website string `json:"website"`
324 Type string `json:"type"`
325 Endpoints []string `json:"endpoints"`
326 }
328 func RegisterClientHandlers(r *mux.Router, context Context) {
329 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
330 }
332 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
333 username, password, ok := r.BasicAuth()
334 if !ok {
335 // TODO(paddy): return error
336 return
337 }
338 profile, err := authenticate(username, password, c)
339 if err != nil {
340 // TODO(paddy): return error
341 return
342 }
343 var req newClientReq
344 decoder := json.NewDecoder(r.Body)
345 err = decoder.Decode(&req)
346 if err != nil {
347 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
348 return
349 }
350 secret := make([]byte, 32)
351 _, err = rand.Read(secret)
352 if err != nil {
353 // TODO(paddy): return error
354 return
355 }
356 client := Client{
357 ID: uuid.NewID(),
358 Secret: hex.EncodeToString(secret),
359 OwnerID: profile.ID,
360 Name: req.Name,
361 Logo: req.Logo,
362 Website: req.Website,
363 Type: req.Type,
364 }
365 err = c.SaveClient(client)
366 if err != nil {
367 // TODO(paddy): return error
368 return
369 }
370 endpoints := []Endpoint{}
371 for _, u := range req.Endpoints {
372 uri, err := url.Parse(u)
373 if err != nil {
374 // TODO(paddy): add error to response
375 continue
376 }
377 endpoint := Endpoint{
378 ID: uuid.NewID(),
379 ClientID: client.ID,
380 URI: *uri,
381 Added: time.Now(),
382 }
383 err = c.AddEndpoint(client.ID, endpoint)
384 if err != nil {
385 // TODO(paddy): return error
386 return
387 }
388 endpoints = append(endpoints, endpoint)
389 }
390 resp := response{
391 Clients: []Client{client},
392 Endpoints: endpoints,
393 }
394 encode(w, r, http.StatusCreated, resp)
395 }