auth

Paddy 2015-01-10 Parent:2e4b5722eed0 Child:fa8ee6a4507c

113:5bd46746b809 Go to Latest

auth/client.go

Let's test our verifyClient function. C'mon, it'll be fun! Add a function that tests the verifyClient function to our unit test suite. Basically, make sure that all the conceivable types of input have the right logic flow for what a "valid client" is. Also leave a note in client.go that makes it clear that public clients _should not be issued secrets in the first place_, because a public client that is issued a secret and specifies its client ID using the `client_id` POST body format will be told that it is not a valid client. While there are ways around this, the spec clearly states that non-confidential clients are not supposed to be issued secrets, so this seems like a nice way to conform to the spec or break trying.

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@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@113 150 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
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 }