auth

Paddy 2015-01-19 Parent:d14f0a81498c Child:4f5d13d2f7c7

128:23c1a07c8a61 Go to Latest

auth/client.go

Add our BUG notices. Rather than keeping the list of things to implement or test on sticky notes attached to my monitor, let's give them BUG designations within the code. Now `godoc . bugs` will list them out for us. Isn't that nice?

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