auth

Paddy 2015-01-05 Parent:2e4b5722eed0 Child:5bd46746b809

111:224f0610d3e7 Go to Latest

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.

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@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 }