auth
auth/client.go
Add support for registering Clients. Add an API endpoint to register Clients, which was the last step necessary before the OAuth2 integration could be tried out.
1 package auth
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "github.com/gorilla/mux"
9 "net/http"
10 "net/url"
11 "time"
13 "code.secondbit.org/uuid.hg"
14 )
16 var (
17 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
18 ErrNoClientStore = errors.New("no clientStore was specified for the Context")
19 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
20 ErrClientNotFound = errors.New("client not found in clientStore")
21 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
22 // the same ID already exists in the clientStore.
23 ErrClientAlreadyExists = errors.New("client already exists in clientStore")
25 // ErrEmptyChange is returned when a Change has all its properties set to nil.
26 ErrEmptyChange = errors.New("change must have at least one property set")
27 // ErrClientNameTooShort is returned when a Client's Name property is too short.
28 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
29 // ErrClientNameTooLong is returned when a Client's Name property is too long.
30 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
31 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
32 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
33 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
34 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
35 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
36 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
37 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
38 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
39 )
41 // Client represents a client that grants access
42 // to the auth server, exchanging grants for tokens,
43 // and tokens for access.
44 type Client struct {
45 ID uuid.ID
46 Secret string
47 OwnerID uuid.ID
48 Name string
49 Logo string
50 Website string
51 Type string
52 }
54 // ApplyChange applies the properties of the passed
55 // ClientChange to the Client object it is called on.
56 func (c *Client) ApplyChange(change ClientChange) {
57 if change.Secret != nil {
58 c.Secret = *change.Secret
59 }
60 if change.OwnerID != nil {
61 c.OwnerID = change.OwnerID
62 }
63 if change.Name != nil {
64 c.Name = *change.Name
65 }
66 if change.Logo != nil {
67 c.Logo = *change.Logo
68 }
69 if change.Website != nil {
70 c.Website = *change.Website
71 }
72 }
74 // ClientChange represents a bundle of options for
75 // updating a Client's mutable data.
76 type ClientChange struct {
77 Secret *string
78 OwnerID uuid.ID
79 Name *string
80 Logo *string
81 Website *string
82 }
84 // Validate checks the ClientChange it is called on
85 // and asserts its internal validity, or lack thereof.
86 func (c ClientChange) Validate() error {
87 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
88 return ErrEmptyChange
89 }
90 if c.Name != nil && len(*c.Name) < 2 {
91 return ErrClientNameTooShort
92 }
93 if c.Name != nil && len(*c.Name) > 32 {
94 return ErrClientNameTooLong
95 }
96 if c.Logo != nil && *c.Logo != "" {
97 if len(*c.Logo) > 1024 {
98 return ErrClientLogoTooLong
99 }
100 u, err := url.Parse(*c.Logo)
101 if err != nil || !u.IsAbs() {
102 return ErrClientLogoNotURL
103 }
104 }
105 if c.Website != nil && *c.Website != "" {
106 if len(*c.Website) > 140 {
107 return ErrClientWebsiteTooLong
108 }
109 u, err := url.Parse(*c.Website)
110 if err != nil || !u.IsAbs() {
111 return ErrClientWebsiteNotURL
112 }
113 }
114 return nil
115 }
117 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
118 enc := json.NewEncoder(w)
119 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
120 if !fromAuthHeader {
121 if !allowPublic {
122 w.WriteHeader(http.StatusBadRequest)
123 renderJSONError(enc, "unauthorized_client")
124 return nil, false
125 }
126 clientIDStr = r.PostFormValue("client_id")
127 }
128 clientID, err := uuid.Parse(clientIDStr)
129 if err != nil {
130 w.WriteHeader(http.StatusUnauthorized)
131 if fromAuthHeader {
132 w.Header().Set("WWW-Authenticate", "Basic")
133 }
134 renderJSONError(enc, "invalid_client")
135 return nil, false
136 }
137 client, err := context.GetClient(clientID)
138 if err == ErrClientNotFound {
139 w.WriteHeader(http.StatusUnauthorized)
140 if fromAuthHeader {
141 w.Header().Set("WWW-Authenticate", "Basic")
142 }
143 renderJSONError(enc, "invalid_client")
144 return nil, false
145 } else if err != nil {
146 w.WriteHeader(http.StatusInternalServerError)
147 renderJSONError(enc, "server_error")
148 return nil, false
149 }
150 if client.Secret != clientSecret {
151 w.WriteHeader(http.StatusUnauthorized)
152 if fromAuthHeader {
153 w.Header().Set("WWW-Authenticate", "Basic")
154 }
155 renderJSONError(enc, "invalid_client")
156 return nil, false
157 }
158 return clientID, true
159 }
161 // Endpoint represents a single URI that a Client
162 // controls. Users will be redirected to these URIs
163 // following successful authorization grants and
164 // exchanges for access tokens.
165 type Endpoint struct {
166 ID uuid.ID
167 ClientID uuid.ID
168 URI url.URL
169 Added time.Time
170 }
172 type sortedEndpoints []Endpoint
174 func (s sortedEndpoints) Len() int {
175 return len(s)
176 }
178 func (s sortedEndpoints) Less(i, j int) bool {
179 return s[i].Added.Before(s[j].Added)
180 }
182 func (s sortedEndpoints) Swap(i, j int) {
183 s[i], s[j] = s[j], s[i]
184 }
186 type clientStore interface {
187 getClient(id uuid.ID) (Client, error)
188 saveClient(client Client) error
189 updateClient(id uuid.ID, change ClientChange) error
190 deleteClient(id uuid.ID) error
191 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
193 addEndpoint(client uuid.ID, endpoint Endpoint) error
194 removeEndpoint(client, endpoint uuid.ID) error
195 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
196 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
197 countEndpoints(client uuid.ID) (int64, error)
198 }
200 func (m *memstore) getClient(id uuid.ID) (Client, error) {
201 m.clientLock.RLock()
202 defer m.clientLock.RUnlock()
203 c, ok := m.clients[id.String()]
204 if !ok {
205 return Client{}, ErrClientNotFound
206 }
207 return c, nil
208 }
210 func (m *memstore) saveClient(client Client) error {
211 m.clientLock.Lock()
212 defer m.clientLock.Unlock()
213 if _, ok := m.clients[client.ID.String()]; ok {
214 return ErrClientAlreadyExists
215 }
216 m.clients[client.ID.String()] = client
217 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
218 return nil
219 }
221 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
222 m.clientLock.Lock()
223 defer m.clientLock.Unlock()
224 c, ok := m.clients[id.String()]
225 if !ok {
226 return ErrClientNotFound
227 }
228 c.ApplyChange(change)
229 m.clients[id.String()] = c
230 return nil
231 }
233 func (m *memstore) deleteClient(id uuid.ID) error {
234 client, err := m.getClient(id)
235 if err != nil {
236 return err
237 }
238 m.clientLock.Lock()
239 defer m.clientLock.Unlock()
240 delete(m.clients, id.String())
241 pos := -1
242 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
243 if item.Equal(id) {
244 pos = p
245 break
246 }
247 }
248 if pos >= 0 {
249 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
250 }
251 return nil
252 }
254 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
255 ids := m.lookupClientsByProfileID(ownerID.String())
256 if len(ids) > num+offset {
257 ids = ids[offset : num+offset]
258 } else if len(ids) > offset {
259 ids = ids[offset:]
260 } else {
261 return []Client{}, nil
262 }
263 clients := []Client{}
264 for _, id := range ids {
265 client, err := m.getClient(id)
266 if err != nil {
267 return []Client{}, err
268 }
269 clients = append(clients, client)
270 }
271 return clients, nil
272 }
274 func (m *memstore) addEndpoint(client uuid.ID, endpoint Endpoint) error {
275 m.endpointLock.Lock()
276 defer m.endpointLock.Unlock()
277 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint)
278 return nil
279 }
281 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
282 m.endpointLock.Lock()
283 defer m.endpointLock.Unlock()
284 pos := -1
285 for p, item := range m.endpoints[client.String()] {
286 if item.ID.Equal(endpoint) {
287 pos = p
288 break
289 }
290 }
291 if pos >= 0 {
292 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
293 }
294 return nil
295 }
297 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
298 m.endpointLock.RLock()
299 defer m.endpointLock.RUnlock()
300 for _, candidate := range m.endpoints[client.String()] {
301 if endpoint == candidate.URI.String() {
302 return true, nil
303 }
304 }
305 return false, nil
306 }
308 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
309 m.endpointLock.RLock()
310 defer m.endpointLock.RUnlock()
311 return m.endpoints[client.String()], nil
312 }
314 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
315 m.endpointLock.RLock()
316 defer m.endpointLock.RUnlock()
317 return int64(len(m.endpoints[client.String()])), nil
318 }
320 type newClientReq struct {
321 Name string `json:"name"`
322 Logo string `json:"logo"`
323 Website string `json:"website"`
324 Type string `json:"type"`
325 Endpoints []string `json:"endpoints"`
326 }
328 func RegisterClientHandlers(r *mux.Router, context Context) {
329 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
330 }
332 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
333 username, password, ok := r.BasicAuth()
334 if !ok {
335 // TODO(paddy): return error
336 return
337 }
338 profile, err := authenticate(username, password, c)
339 if err != nil {
340 // TODO(paddy): return error
341 return
342 }
343 var req newClientReq
344 decoder := json.NewDecoder(r.Body)
345 err = decoder.Decode(&req)
346 if err != nil {
347 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
348 return
349 }
350 secret := make([]byte, 32)
351 _, err = rand.Read(secret)
352 if err != nil {
353 // TODO(paddy): return error
354 return
355 }
356 client := Client{
357 ID: uuid.NewID(),
358 Secret: hex.EncodeToString(secret),
359 OwnerID: profile.ID,
360 Name: req.Name,
361 Logo: req.Logo,
362 Website: req.Website,
363 Type: req.Type,
364 }
365 err = c.SaveClient(client)
366 if err != nil {
367 // TODO(paddy): return error
368 return
369 }
370 endpoints := []Endpoint{}
371 for _, u := range req.Endpoints {
372 uri, err := url.Parse(u)
373 if err != nil {
374 // TODO(paddy): add error to response
375 continue
376 }
377 endpoint := Endpoint{
378 ID: uuid.NewID(),
379 ClientID: client.ID,
380 URI: *uri,
381 Added: time.Now(),
382 }
383 err = c.AddEndpoint(client.ID, endpoint)
384 if err != nil {
385 // TODO(paddy): return error
386 return
387 }
388 endpoints = append(endpoints, endpoint)
389 }
390 resp := response{
391 Clients: []Client{client},
392 Endpoints: endpoints,
393 }
394 encode(w, r, http.StatusCreated, resp)
395 }