auth
auth/client.go
Enable CSRF protection, add expiration to sessions. Sessions gain a CSRF token, which is passed as a parameter to the login page. The login page now checks for that CSRF token, and logs a CSRF attempt if the token does not match. I also added an expiration to sessions, so they don't last forever. Sessions should be pretty short--we just need to stay logged in for long enough to approve the OAuth request. Everything after that should be cookie based. Finally, I added a configuration parameter to control whether the session cookie should be set to Secure, requiring the use of HTTPS. For production use, this flag is a requirement, but it makes testing extremely difficult, so we need a way to disable it.
| 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@131 | 59 clientTypePublic = "public" |
| paddy@131 | 60 clientTypeConfidential = "confidential" |
| paddy@131 | 61 minClientNameLen = 2 |
| paddy@131 | 62 maxClientNameLen = 24 |
| paddy@131 | 63 defaultClientResponseSize = 20 |
| paddy@131 | 64 maxClientResponseSize = 50 |
| paddy@130 | 65 |
| paddy@130 | 66 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery |
| paddy@115 | 67 ) |
| paddy@115 | 68 |
| paddy@25 | 69 // Client represents a client that grants access |
| paddy@25 | 70 // to the auth server, exchanging grants for tokens, |
| paddy@25 | 71 // and tokens for access. |
| paddy@0 | 72 type Client struct { |
| paddy@116 | 73 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 74 Secret string `json:"secret,omitempty"` |
| paddy@116 | 75 OwnerID uuid.ID `json:"owner_id,omitempty"` |
| paddy@116 | 76 Name string `json:"name,omitempty"` |
| paddy@116 | 77 Logo string `json:"logo,omitempty"` |
| paddy@116 | 78 Website string `json:"website,omitempty"` |
| paddy@116 | 79 Type string `json:"type,omitempty"` |
| paddy@0 | 80 } |
| paddy@0 | 81 |
| paddy@57 | 82 // ApplyChange applies the properties of the passed |
| paddy@57 | 83 // ClientChange to the Client object it is called on. |
| paddy@39 | 84 func (c *Client) ApplyChange(change ClientChange) { |
| paddy@39 | 85 if change.Secret != nil { |
| paddy@39 | 86 c.Secret = *change.Secret |
| paddy@39 | 87 } |
| paddy@39 | 88 if change.OwnerID != nil { |
| paddy@39 | 89 c.OwnerID = change.OwnerID |
| paddy@39 | 90 } |
| paddy@39 | 91 if change.Name != nil { |
| paddy@39 | 92 c.Name = *change.Name |
| paddy@39 | 93 } |
| paddy@39 | 94 if change.Logo != nil { |
| paddy@39 | 95 c.Logo = *change.Logo |
| paddy@39 | 96 } |
| paddy@39 | 97 if change.Website != nil { |
| paddy@39 | 98 c.Website = *change.Website |
| paddy@39 | 99 } |
| paddy@39 | 100 } |
| paddy@39 | 101 |
| paddy@57 | 102 // ClientChange represents a bundle of options for |
| paddy@57 | 103 // updating a Client's mutable data. |
| paddy@31 | 104 type ClientChange struct { |
| paddy@41 | 105 Secret *string |
| paddy@41 | 106 OwnerID uuid.ID |
| paddy@41 | 107 Name *string |
| paddy@41 | 108 Logo *string |
| paddy@41 | 109 Website *string |
| paddy@31 | 110 } |
| paddy@31 | 111 |
| paddy@57 | 112 // Validate checks the ClientChange it is called on |
| paddy@57 | 113 // and asserts its internal validity, or lack thereof. |
| paddy@39 | 114 func (c ClientChange) Validate() error { |
| paddy@42 | 115 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil { |
| paddy@42 | 116 return ErrEmptyChange |
| paddy@42 | 117 } |
| paddy@41 | 118 if c.Name != nil && len(*c.Name) < 2 { |
| paddy@41 | 119 return ErrClientNameTooShort |
| paddy@41 | 120 } |
| paddy@41 | 121 if c.Name != nil && len(*c.Name) > 32 { |
| paddy@41 | 122 return ErrClientNameTooLong |
| paddy@41 | 123 } |
| paddy@42 | 124 if c.Logo != nil && *c.Logo != "" { |
| paddy@42 | 125 if len(*c.Logo) > 1024 { |
| paddy@42 | 126 return ErrClientLogoTooLong |
| paddy@42 | 127 } |
| paddy@42 | 128 u, err := url.Parse(*c.Logo) |
| paddy@42 | 129 if err != nil || !u.IsAbs() { |
| paddy@42 | 130 return ErrClientLogoNotURL |
| paddy@42 | 131 } |
| paddy@41 | 132 } |
| paddy@42 | 133 if c.Website != nil && *c.Website != "" { |
| paddy@42 | 134 if len(*c.Website) > 140 { |
| paddy@42 | 135 return ErrClientWebsiteTooLong |
| paddy@42 | 136 } |
| paddy@42 | 137 u, err := url.Parse(*c.Website) |
| paddy@42 | 138 if err != nil || !u.IsAbs() { |
| paddy@42 | 139 return ErrClientWebsiteNotURL |
| paddy@42 | 140 } |
| paddy@41 | 141 } |
| paddy@39 | 142 return nil |
| paddy@39 | 143 } |
| paddy@39 | 144 |
| paddy@123 | 145 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) { |
| paddy@85 | 146 enc := json.NewEncoder(w) |
| paddy@85 | 147 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 148 if !fromAuthHeader { |
| paddy@85 | 149 clientIDStr = r.PostFormValue("client_id") |
| paddy@85 | 150 } |
| paddy@123 | 151 if clientIDStr == "" { |
| 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@123 | 157 return nil, "", false |
| paddy@123 | 158 } |
| paddy@129 | 159 if !allowPublic && !fromAuthHeader { |
| paddy@129 | 160 w.WriteHeader(http.StatusBadRequest) |
| paddy@129 | 161 renderJSONError(enc, "unauthorized_client") |
| paddy@129 | 162 return nil, "", false |
| paddy@129 | 163 } |
| paddy@123 | 164 clientID, err := uuid.Parse(clientIDStr) |
| paddy@123 | 165 if err != nil { |
| paddy@123 | 166 log.Println("Error decoding client ID:", err) |
| paddy@123 | 167 w.WriteHeader(http.StatusUnauthorized) |
| paddy@123 | 168 if fromAuthHeader { |
| paddy@123 | 169 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@123 | 170 } |
| paddy@123 | 171 renderJSONError(enc, "invalid_client") |
| paddy@123 | 172 return nil, "", false |
| paddy@123 | 173 } |
| paddy@123 | 174 return clientID, clientSecret, true |
| paddy@123 | 175 } |
| paddy@123 | 176 |
| paddy@123 | 177 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { |
| paddy@123 | 178 enc := json.NewEncoder(w) |
| paddy@123 | 179 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic) |
| paddy@123 | 180 if !ok { |
| paddy@85 | 181 return nil, false |
| paddy@85 | 182 } |
| paddy@123 | 183 _, _, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 184 client, err := context.GetClient(clientID) |
| paddy@85 | 185 if err == ErrClientNotFound { |
| paddy@85 | 186 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 187 if fromAuthHeader { |
| paddy@85 | 188 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 189 } |
| paddy@85 | 190 renderJSONError(enc, "invalid_client") |
| paddy@85 | 191 return nil, false |
| paddy@85 | 192 } else if err != nil { |
| paddy@85 | 193 w.WriteHeader(http.StatusInternalServerError) |
| paddy@85 | 194 renderJSONError(enc, "server_error") |
| paddy@85 | 195 return nil, false |
| paddy@85 | 196 } |
| paddy@113 | 197 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret. |
| paddy@85 | 198 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 199 if fromAuthHeader { |
| paddy@85 | 200 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 201 } |
| paddy@85 | 202 renderJSONError(enc, "invalid_client") |
| paddy@85 | 203 return nil, false |
| paddy@85 | 204 } |
| paddy@85 | 205 return clientID, true |
| paddy@85 | 206 } |
| paddy@85 | 207 |
| paddy@57 | 208 // Endpoint represents a single URI that a Client |
| paddy@57 | 209 // controls. Users will be redirected to these URIs |
| paddy@57 | 210 // following successful authorization grants and |
| paddy@57 | 211 // exchanges for access tokens. |
| paddy@41 | 212 type Endpoint struct { |
| paddy@116 | 213 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 214 ClientID uuid.ID `json:"client_id,omitempty"` |
| paddy@116 | 215 URI string `json:"uri,omitempty"` |
| paddy@116 | 216 NormalizedURI string `json:"-"` |
| paddy@116 | 217 Added time.Time `json:"added,omitempty"` |
| paddy@116 | 218 } |
| paddy@116 | 219 |
| paddy@116 | 220 func normalizeURIString(in string) (string, error) { |
| paddy@130 | 221 n, err := purell.NormalizeURLString(in, normalizeFlags) |
| paddy@116 | 222 if err != nil { |
| paddy@116 | 223 log.Println(err) |
| paddy@116 | 224 return in, ErrEndpointURINotURL |
| paddy@116 | 225 } |
| paddy@116 | 226 return n, nil |
| paddy@116 | 227 } |
| paddy@116 | 228 |
| paddy@116 | 229 func normalizeURI(in *url.URL) string { |
| paddy@130 | 230 return purell.NormalizeURL(in, normalizeFlags) |
| paddy@41 | 231 } |
| paddy@41 | 232 |
| paddy@41 | 233 type sortedEndpoints []Endpoint |
| paddy@41 | 234 |
| paddy@41 | 235 func (s sortedEndpoints) Len() int { |
| paddy@41 | 236 return len(s) |
| paddy@41 | 237 } |
| paddy@41 | 238 |
| paddy@41 | 239 func (s sortedEndpoints) Less(i, j int) bool { |
| paddy@41 | 240 return s[i].Added.Before(s[j].Added) |
| paddy@41 | 241 } |
| paddy@41 | 242 |
| paddy@41 | 243 func (s sortedEndpoints) Swap(i, j int) { |
| paddy@41 | 244 s[i], s[j] = s[j], s[i] |
| paddy@41 | 245 } |
| paddy@41 | 246 |
| paddy@57 | 247 type clientStore interface { |
| paddy@57 | 248 getClient(id uuid.ID) (Client, error) |
| paddy@57 | 249 saveClient(client Client) error |
| paddy@57 | 250 updateClient(id uuid.ID, change ClientChange) error |
| paddy@57 | 251 deleteClient(id uuid.ID) error |
| paddy@57 | 252 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) |
| paddy@41 | 253 |
| paddy@115 | 254 addEndpoints(client uuid.ID, endpoint []Endpoint) error |
| paddy@57 | 255 removeEndpoint(client, endpoint uuid.ID) error |
| paddy@58 | 256 checkEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@57 | 257 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@57 | 258 countEndpoints(client uuid.ID) (int64, error) |
| paddy@0 | 259 } |
| paddy@31 | 260 |
| paddy@57 | 261 func (m *memstore) getClient(id uuid.ID) (Client, error) { |
| paddy@31 | 262 m.clientLock.RLock() |
| paddy@31 | 263 defer m.clientLock.RUnlock() |
| paddy@31 | 264 c, ok := m.clients[id.String()] |
| paddy@31 | 265 if !ok { |
| paddy@31 | 266 return Client{}, ErrClientNotFound |
| paddy@31 | 267 } |
| paddy@31 | 268 return c, nil |
| paddy@31 | 269 } |
| paddy@31 | 270 |
| paddy@57 | 271 func (m *memstore) saveClient(client Client) error { |
| paddy@31 | 272 m.clientLock.Lock() |
| paddy@31 | 273 defer m.clientLock.Unlock() |
| paddy@31 | 274 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 275 return ErrClientAlreadyExists |
| paddy@31 | 276 } |
| paddy@31 | 277 m.clients[client.ID.String()] = client |
| paddy@31 | 278 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 279 return nil |
| paddy@31 | 280 } |
| paddy@31 | 281 |
| paddy@57 | 282 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 283 m.clientLock.Lock() |
| paddy@39 | 284 defer m.clientLock.Unlock() |
| paddy@39 | 285 c, ok := m.clients[id.String()] |
| paddy@39 | 286 if !ok { |
| paddy@39 | 287 return ErrClientNotFound |
| paddy@39 | 288 } |
| paddy@39 | 289 c.ApplyChange(change) |
| paddy@39 | 290 m.clients[id.String()] = c |
| paddy@31 | 291 return nil |
| paddy@31 | 292 } |
| paddy@31 | 293 |
| paddy@57 | 294 func (m *memstore) deleteClient(id uuid.ID) error { |
| paddy@57 | 295 client, err := m.getClient(id) |
| paddy@31 | 296 if err != nil { |
| paddy@31 | 297 return err |
| paddy@31 | 298 } |
| paddy@31 | 299 m.clientLock.Lock() |
| paddy@31 | 300 defer m.clientLock.Unlock() |
| paddy@31 | 301 delete(m.clients, id.String()) |
| paddy@31 | 302 pos := -1 |
| paddy@31 | 303 for p, item := range m.profileClientLookup[client.OwnerID.String()] { |
| paddy@31 | 304 if item.Equal(id) { |
| paddy@31 | 305 pos = p |
| paddy@31 | 306 break |
| paddy@31 | 307 } |
| paddy@31 | 308 } |
| paddy@31 | 309 if pos >= 0 { |
| paddy@31 | 310 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...) |
| paddy@31 | 311 } |
| paddy@31 | 312 return nil |
| paddy@31 | 313 } |
| paddy@31 | 314 |
| paddy@57 | 315 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 316 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@31 | 317 if len(ids) > num+offset { |
| paddy@31 | 318 ids = ids[offset : num+offset] |
| paddy@31 | 319 } else if len(ids) > offset { |
| paddy@31 | 320 ids = ids[offset:] |
| paddy@31 | 321 } else { |
| paddy@31 | 322 return []Client{}, nil |
| paddy@31 | 323 } |
| paddy@31 | 324 clients := []Client{} |
| paddy@31 | 325 for _, id := range ids { |
| paddy@57 | 326 client, err := m.getClient(id) |
| paddy@31 | 327 if err != nil { |
| paddy@31 | 328 return []Client{}, err |
| paddy@31 | 329 } |
| paddy@31 | 330 clients = append(clients, client) |
| paddy@31 | 331 } |
| paddy@31 | 332 return clients, nil |
| paddy@31 | 333 } |
| paddy@41 | 334 |
| paddy@115 | 335 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error { |
| paddy@41 | 336 m.endpointLock.Lock() |
| paddy@41 | 337 defer m.endpointLock.Unlock() |
| paddy@115 | 338 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...) |
| paddy@41 | 339 return nil |
| paddy@41 | 340 } |
| paddy@41 | 341 |
| paddy@57 | 342 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 343 m.endpointLock.Lock() |
| paddy@41 | 344 defer m.endpointLock.Unlock() |
| paddy@41 | 345 pos := -1 |
| paddy@41 | 346 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 347 if item.ID.Equal(endpoint) { |
| paddy@41 | 348 pos = p |
| paddy@41 | 349 break |
| paddy@41 | 350 } |
| paddy@41 | 351 } |
| paddy@41 | 352 if pos >= 0 { |
| paddy@41 | 353 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 354 } |
| paddy@41 | 355 return nil |
| paddy@41 | 356 } |
| paddy@41 | 357 |
| paddy@58 | 358 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 359 m.endpointLock.RLock() |
| paddy@41 | 360 defer m.endpointLock.RUnlock() |
| paddy@41 | 361 for _, candidate := range m.endpoints[client.String()] { |
| paddy@116 | 362 if endpoint == candidate.NormalizedURI { |
| paddy@41 | 363 return true, nil |
| paddy@41 | 364 } |
| paddy@41 | 365 } |
| paddy@41 | 366 return false, nil |
| paddy@41 | 367 } |
| paddy@41 | 368 |
| paddy@57 | 369 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 370 m.endpointLock.RLock() |
| paddy@41 | 371 defer m.endpointLock.RUnlock() |
| paddy@41 | 372 return m.endpoints[client.String()], nil |
| paddy@41 | 373 } |
| paddy@54 | 374 |
| paddy@57 | 375 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) { |
| paddy@54 | 376 m.endpointLock.RLock() |
| paddy@54 | 377 defer m.endpointLock.RUnlock() |
| paddy@54 | 378 return int64(len(m.endpoints[client.String()])), nil |
| paddy@54 | 379 } |
| paddy@108 | 380 |
| paddy@108 | 381 type newClientReq struct { |
| paddy@108 | 382 Name string `json:"name"` |
| paddy@108 | 383 Logo string `json:"logo"` |
| paddy@108 | 384 Website string `json:"website"` |
| paddy@108 | 385 Type string `json:"type"` |
| paddy@108 | 386 Endpoints []string `json:"endpoints"` |
| paddy@108 | 387 } |
| paddy@108 | 388 |
| paddy@108 | 389 func RegisterClientHandlers(r *mux.Router, context Context) { |
| paddy@108 | 390 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST") |
| paddy@131 | 391 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET") |
| paddy@131 | 392 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET") |
| paddy@128 | 393 // BUG(paddy): We need to implement a handler to update a client. |
| paddy@128 | 394 // 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 | 395 // BUG(paddy): We need to implement a handler to add an endpoint to a client. |
| paddy@128 | 396 // BUG(paddy): We need to implement a handler to remove an endpoint from a client. |
| paddy@128 | 397 // BUG(paddy): We need to implement a handler to list endpoints. |
| paddy@108 | 398 } |
| paddy@108 | 399 |
| paddy@108 | 400 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@115 | 401 errors := []requestError{} |
| paddy@108 | 402 username, password, ok := r.BasicAuth() |
| paddy@108 | 403 if !ok { |
| paddy@115 | 404 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 405 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 406 return |
| paddy@108 | 407 } |
| paddy@108 | 408 profile, err := authenticate(username, password, c) |
| paddy@108 | 409 if err != nil { |
| paddy@115 | 410 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 411 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 412 return |
| paddy@108 | 413 } |
| paddy@108 | 414 var req newClientReq |
| paddy@108 | 415 decoder := json.NewDecoder(r.Body) |
| paddy@108 | 416 err = decoder.Decode(&req) |
| paddy@108 | 417 if err != nil { |
| paddy@108 | 418 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@108 | 419 return |
| paddy@108 | 420 } |
| paddy@116 | 421 if req.Type == "" { |
| paddy@116 | 422 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"}) |
| paddy@116 | 423 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential { |
| paddy@115 | 424 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"}) |
| paddy@116 | 425 } |
| paddy@116 | 426 if req.Name == "" { |
| paddy@116 | 427 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"}) |
| paddy@116 | 428 } else if len(req.Name) < minClientNameLen { |
| paddy@116 | 429 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@116 | 430 } else if len(req.Name) > maxClientNameLen { |
| paddy@116 | 431 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@116 | 432 } |
| paddy@116 | 433 if len(errors) > 0 { |
| paddy@115 | 434 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@108 | 435 return |
| paddy@108 | 436 } |
| paddy@108 | 437 client := Client{ |
| paddy@108 | 438 ID: uuid.NewID(), |
| paddy@108 | 439 OwnerID: profile.ID, |
| paddy@108 | 440 Name: req.Name, |
| paddy@108 | 441 Logo: req.Logo, |
| paddy@108 | 442 Website: req.Website, |
| paddy@108 | 443 Type: req.Type, |
| paddy@108 | 444 } |
| paddy@118 | 445 if client.Type == clientTypeConfidential { |
| paddy@115 | 446 secret := make([]byte, 32) |
| paddy@115 | 447 _, err = rand.Read(secret) |
| paddy@115 | 448 if err != nil { |
| paddy@115 | 449 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@115 | 450 return |
| paddy@115 | 451 } |
| paddy@115 | 452 client.Secret = hex.EncodeToString(secret) |
| paddy@115 | 453 } |
| paddy@108 | 454 err = c.SaveClient(client) |
| paddy@108 | 455 if err != nil { |
| paddy@115 | 456 if err == ErrClientAlreadyExists { |
| paddy@115 | 457 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"}) |
| paddy@115 | 458 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@115 | 459 return |
| paddy@115 | 460 } |
| paddy@115 | 461 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@108 | 462 return |
| paddy@108 | 463 } |
| paddy@108 | 464 endpoints := []Endpoint{} |
| paddy@115 | 465 for pos, u := range req.Endpoints { |
| paddy@108 | 466 uri, err := url.Parse(u) |
| paddy@108 | 467 if err != nil { |
| paddy@115 | 468 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@108 | 469 continue |
| paddy@108 | 470 } |
| paddy@116 | 471 if !uri.IsAbs() { |
| paddy@116 | 472 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@116 | 473 continue |
| paddy@116 | 474 } |
| paddy@108 | 475 endpoint := Endpoint{ |
| paddy@108 | 476 ID: uuid.NewID(), |
| paddy@108 | 477 ClientID: client.ID, |
| paddy@116 | 478 URI: uri.String(), |
| paddy@108 | 479 Added: time.Now(), |
| paddy@108 | 480 } |
| paddy@108 | 481 endpoints = append(endpoints, endpoint) |
| paddy@108 | 482 } |
| paddy@115 | 483 err = c.AddEndpoints(client.ID, endpoints) |
| paddy@115 | 484 if err != nil { |
| paddy@115 | 485 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@115 | 486 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}}) |
| paddy@115 | 487 return |
| paddy@115 | 488 } |
| paddy@108 | 489 resp := response{ |
| paddy@108 | 490 Clients: []Client{client}, |
| paddy@108 | 491 Endpoints: endpoints, |
| paddy@116 | 492 Errors: errors, |
| paddy@108 | 493 } |
| paddy@108 | 494 encode(w, r, http.StatusCreated, resp) |
| paddy@108 | 495 } |
| paddy@121 | 496 |
| paddy@131 | 497 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@131 | 498 errors := []requestError{} |
| paddy@131 | 499 vars := mux.Vars(r) |
| paddy@131 | 500 if vars["id"] == "" { |
| paddy@131 | 501 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@131 | 502 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 503 return |
| paddy@131 | 504 } |
| paddy@131 | 505 id, err := uuid.Parse(vars["id"]) |
| paddy@131 | 506 if err != nil { |
| paddy@131 | 507 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@131 | 508 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 509 return |
| paddy@131 | 510 } |
| paddy@131 | 511 client, err := c.GetClient(id) |
| paddy@131 | 512 if err != nil { |
| paddy@131 | 513 if err == ErrClientNotFound { |
| paddy@131 | 514 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@131 | 515 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 516 return |
| paddy@131 | 517 } |
| paddy@131 | 518 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 519 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 520 return |
| paddy@131 | 521 } |
| paddy@131 | 522 client.Secret = "" |
| paddy@131 | 523 // BUG(paddy): How should auth be handled for retrieving clients? |
| paddy@131 | 524 resp := response{ |
| paddy@131 | 525 Clients: []Client{client}, |
| paddy@131 | 526 Errors: errors, |
| paddy@131 | 527 } |
| paddy@131 | 528 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 529 } |
| paddy@131 | 530 |
| paddy@131 | 531 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@131 | 532 errors := []requestError{} |
| paddy@131 | 533 var err error |
| paddy@131 | 534 // BUG(paddy): If ids are provided in query params, retrieve only those clients |
| paddy@131 | 535 // BUG(paddy): We should have auth when listing clients |
| paddy@131 | 536 num := defaultClientResponseSize |
| paddy@131 | 537 offset := 0 |
| paddy@131 | 538 ownerIDStr := r.URL.Query().Get("owner_id") |
| paddy@131 | 539 numStr := r.URL.Query().Get("num") |
| paddy@131 | 540 offsetStr := r.URL.Query().Get("offset") |
| paddy@131 | 541 if numStr != "" { |
| paddy@131 | 542 num, err = strconv.Atoi(numStr) |
| paddy@131 | 543 if err != nil { |
| paddy@131 | 544 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@131 | 545 } |
| paddy@131 | 546 if num > maxClientResponseSize { |
| paddy@131 | 547 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@131 | 548 } |
| paddy@131 | 549 } |
| paddy@131 | 550 if offsetStr != "" { |
| paddy@131 | 551 offset, err = strconv.Atoi(offsetStr) |
| paddy@131 | 552 if err != nil { |
| paddy@131 | 553 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@131 | 554 } |
| paddy@131 | 555 } |
| paddy@131 | 556 if ownerIDStr == "" { |
| paddy@131 | 557 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"}) |
| paddy@131 | 558 } |
| paddy@131 | 559 if len(errors) > 0 { |
| paddy@131 | 560 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 561 return |
| paddy@131 | 562 } |
| paddy@131 | 563 ownerID, err := uuid.Parse(ownerIDStr) |
| paddy@131 | 564 if err != nil { |
| paddy@131 | 565 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"}) |
| paddy@131 | 566 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 567 return |
| paddy@131 | 568 } |
| paddy@131 | 569 clients, err := c.ListClientsByOwner(ownerID, num, offset) |
| paddy@131 | 570 if err != nil { |
| paddy@131 | 571 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 572 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 573 return |
| paddy@131 | 574 } |
| paddy@131 | 575 for pos, client := range clients { |
| paddy@131 | 576 client.Secret = "" |
| paddy@131 | 577 clients[pos] = client |
| paddy@131 | 578 } |
| paddy@131 | 579 resp := response{ |
| paddy@131 | 580 Clients: clients, |
| paddy@131 | 581 Errors: errors, |
| paddy@131 | 582 } |
| paddy@131 | 583 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 584 } |
| paddy@131 | 585 |
| paddy@121 | 586 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) { |
| paddy@121 | 587 scope = r.PostFormValue("scope") |
| paddy@121 | 588 valid = true |
| paddy@121 | 589 return |
| paddy@121 | 590 } |
| paddy@124 | 591 |
| paddy@124 | 592 func clientCredentialsAuditString(r *http.Request) string { |
| paddy@124 | 593 return "client_credentials" |
| paddy@124 | 594 } |