auth

Paddy 2015-01-24 Parent:4f5d13d2f7c7 Child:f474ce964dcf

130:6c755b23ec80 Go to Latest

auth/client.go

Change normalization flags to a constant. Let's use a constant so we can ensure we're using the same flags everywhere. Otherwise, we can get weird data corruption because we use the wrong flags.

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