auth
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.
| 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 } |