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