auth

Paddy 2015-01-18 Parent:565a9335e035 Child:0a1e16b9c141

121:823517aad893 Go to Latest

auth/client.go

Implement client_credentials grant. Implement granting an access token (ProfileID set to nil) for a client based on client credentials.

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