auth

Paddy 2015-01-18 Parent:823517aad893 Child:d14f0a81498c

123:0a1e16b9c141 Go to Latest

auth/client.go

Refactor verifyClient, implement refresh tokens. Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient out of each of the GrantType's validation functions and into the access token endpoint, where it will be called before the GrantType's validation function. Yay, less code repetition. And seeing as we always want to verify the client, that seems like a good way to prevent things like 118a69954621 from happening. This did, however, force us to add an AllowsPublic property to the GrantType, so the token endpoint knows whether or not a public Client is valid for any given GrantType. We also implemented the refresh token grant type, which required adding ClientID and RefreshRevoked as properties on the Token type. We need ClientID because we need to constrain refresh tokens to the client that issued them. We also should probably keep track of which tokens belong to which clients, just as a general rule of thumb. RefreshRevoked had to be created, next to Revoked, because the AccessToken could be revoked and the RefreshToken still valid, or vice versa. Notably, when you issue a new refresh token, the old one is revoked, but the access token is still valid. It remains to be seen whether this is a good way to track things or not. The number of duplicated properties lead me to believe our type is not a great representation of the underlying concepts.

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