auth

Paddy 2015-01-14 Parent:5bd46746b809 Child:e000b1c24fc0

115:fa8ee6a4507c Go to Latest

auth/client.go

Turn AddEndpoint into AddEndpoints. Because one is a special case of many, it makes sense to be able to add multiple endpoints in a single call to the database. So we've converted the AddEndpoint method into an AddEndpoints method and updated our tests appropriately. We also filled in the errors when creating a client through the API, and moved things around to optimize for the maximum number of errors returned in a single call.

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