auth
auth/client.go
Fix bug with response_type redirect, add tests. Test that we redirect with an error when an invalid response_type is supplied. Fix a bug that would not add any of our parameters to the redirect URL.
| paddy@6 | 1 package auth |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@31 | 4 "errors" |
| paddy@41 | 5 "net/url" |
| paddy@41 | 6 "time" |
| paddy@31 | 7 |
| paddy@45 | 8 "code.secondbit.org/uuid" |
| paddy@0 | 9 ) |
| paddy@0 | 10 |
| paddy@31 | 11 var ( |
| paddy@57 | 12 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first. |
| paddy@57 | 13 ErrNoClientStore = errors.New("no clientStore was specified for the Context") |
| paddy@57 | 14 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore. |
| paddy@57 | 15 ErrClientNotFound = errors.New("client not found in clientStore") |
| paddy@57 | 16 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with |
| paddy@57 | 17 // the same ID already exists in the clientStore. |
| paddy@57 | 18 ErrClientAlreadyExists = errors.New("client already exists in clientStore") |
| paddy@41 | 19 |
| paddy@57 | 20 // ErrEmptyChange is returned when a Change has all its properties set to nil. |
| paddy@57 | 21 ErrEmptyChange = errors.New("change must have at least one property set") |
| paddy@57 | 22 // ErrClientNameTooShort is returned when a Client's Name property is too short. |
| paddy@57 | 23 ErrClientNameTooShort = errors.New("client name must be at least 2 characters") |
| paddy@57 | 24 // ErrClientNameTooLong is returned when a Client's Name property is too long. |
| paddy@57 | 25 ErrClientNameTooLong = errors.New("client name must be at most 32 characters") |
| paddy@57 | 26 // ErrClientLogoTooLong is returned when a Client's Logo property is too long. |
| paddy@57 | 27 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters") |
| paddy@57 | 28 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL. |
| paddy@57 | 29 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL") |
| paddy@57 | 30 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long. |
| paddy@49 | 31 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters") |
| paddy@57 | 32 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL. |
| paddy@57 | 33 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL") |
| paddy@31 | 34 ) |
| paddy@31 | 35 |
| paddy@25 | 36 // Client represents a client that grants access |
| paddy@25 | 37 // to the auth server, exchanging grants for tokens, |
| paddy@25 | 38 // and tokens for access. |
| paddy@0 | 39 type Client struct { |
| paddy@41 | 40 ID uuid.ID |
| paddy@41 | 41 Secret string |
| paddy@41 | 42 OwnerID uuid.ID |
| paddy@41 | 43 Name string |
| paddy@41 | 44 Logo string |
| paddy@41 | 45 Website string |
| paddy@41 | 46 Type string |
| paddy@0 | 47 } |
| paddy@0 | 48 |
| paddy@57 | 49 // ApplyChange applies the properties of the passed |
| paddy@57 | 50 // ClientChange to the Client object it is called on. |
| paddy@39 | 51 func (c *Client) ApplyChange(change ClientChange) { |
| paddy@39 | 52 if change.Secret != nil { |
| paddy@39 | 53 c.Secret = *change.Secret |
| paddy@39 | 54 } |
| paddy@39 | 55 if change.OwnerID != nil { |
| paddy@39 | 56 c.OwnerID = change.OwnerID |
| paddy@39 | 57 } |
| paddy@39 | 58 if change.Name != nil { |
| paddy@39 | 59 c.Name = *change.Name |
| paddy@39 | 60 } |
| paddy@39 | 61 if change.Logo != nil { |
| paddy@39 | 62 c.Logo = *change.Logo |
| paddy@39 | 63 } |
| paddy@39 | 64 if change.Website != nil { |
| paddy@39 | 65 c.Website = *change.Website |
| paddy@39 | 66 } |
| paddy@39 | 67 } |
| paddy@39 | 68 |
| paddy@57 | 69 // ClientChange represents a bundle of options for |
| paddy@57 | 70 // updating a Client's mutable data. |
| paddy@31 | 71 type ClientChange struct { |
| paddy@41 | 72 Secret *string |
| paddy@41 | 73 OwnerID uuid.ID |
| paddy@41 | 74 Name *string |
| paddy@41 | 75 Logo *string |
| paddy@41 | 76 Website *string |
| paddy@31 | 77 } |
| paddy@31 | 78 |
| paddy@57 | 79 // Validate checks the ClientChange it is called on |
| paddy@57 | 80 // and asserts its internal validity, or lack thereof. |
| paddy@39 | 81 func (c ClientChange) Validate() error { |
| paddy@42 | 82 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil { |
| paddy@42 | 83 return ErrEmptyChange |
| paddy@42 | 84 } |
| paddy@41 | 85 if c.Name != nil && len(*c.Name) < 2 { |
| paddy@41 | 86 return ErrClientNameTooShort |
| paddy@41 | 87 } |
| paddy@41 | 88 if c.Name != nil && len(*c.Name) > 32 { |
| paddy@41 | 89 return ErrClientNameTooLong |
| paddy@41 | 90 } |
| paddy@42 | 91 if c.Logo != nil && *c.Logo != "" { |
| paddy@42 | 92 if len(*c.Logo) > 1024 { |
| paddy@42 | 93 return ErrClientLogoTooLong |
| paddy@42 | 94 } |
| paddy@42 | 95 u, err := url.Parse(*c.Logo) |
| paddy@42 | 96 if err != nil || !u.IsAbs() { |
| paddy@42 | 97 return ErrClientLogoNotURL |
| paddy@42 | 98 } |
| paddy@41 | 99 } |
| paddy@42 | 100 if c.Website != nil && *c.Website != "" { |
| paddy@42 | 101 if len(*c.Website) > 140 { |
| paddy@42 | 102 return ErrClientWebsiteTooLong |
| paddy@42 | 103 } |
| paddy@42 | 104 u, err := url.Parse(*c.Website) |
| paddy@42 | 105 if err != nil || !u.IsAbs() { |
| paddy@42 | 106 return ErrClientWebsiteNotURL |
| paddy@42 | 107 } |
| paddy@41 | 108 } |
| paddy@39 | 109 return nil |
| paddy@39 | 110 } |
| paddy@39 | 111 |
| paddy@57 | 112 // Endpoint represents a single URI that a Client |
| paddy@57 | 113 // controls. Users will be redirected to these URIs |
| paddy@57 | 114 // following successful authorization grants and |
| paddy@57 | 115 // exchanges for access tokens. |
| paddy@41 | 116 type Endpoint struct { |
| paddy@41 | 117 ID uuid.ID |
| paddy@41 | 118 ClientID uuid.ID |
| paddy@41 | 119 URI url.URL |
| paddy@41 | 120 Added time.Time |
| paddy@41 | 121 } |
| paddy@41 | 122 |
| paddy@41 | 123 type sortedEndpoints []Endpoint |
| paddy@41 | 124 |
| paddy@41 | 125 func (s sortedEndpoints) Len() int { |
| paddy@41 | 126 return len(s) |
| paddy@41 | 127 } |
| paddy@41 | 128 |
| paddy@41 | 129 func (s sortedEndpoints) Less(i, j int) bool { |
| paddy@41 | 130 return s[i].Added.Before(s[j].Added) |
| paddy@41 | 131 } |
| paddy@41 | 132 |
| paddy@41 | 133 func (s sortedEndpoints) Swap(i, j int) { |
| paddy@41 | 134 s[i], s[j] = s[j], s[i] |
| paddy@41 | 135 } |
| paddy@41 | 136 |
| paddy@57 | 137 type clientStore interface { |
| paddy@57 | 138 getClient(id uuid.ID) (Client, error) |
| paddy@57 | 139 saveClient(client Client) error |
| paddy@57 | 140 updateClient(id uuid.ID, change ClientChange) error |
| paddy@57 | 141 deleteClient(id uuid.ID) error |
| paddy@57 | 142 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) |
| paddy@41 | 143 |
| paddy@57 | 144 addEndpoint(client uuid.ID, endpoint Endpoint) error |
| paddy@57 | 145 removeEndpoint(client, endpoint uuid.ID) error |
| paddy@58 | 146 checkEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@57 | 147 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@57 | 148 countEndpoints(client uuid.ID) (int64, error) |
| paddy@0 | 149 } |
| paddy@31 | 150 |
| paddy@57 | 151 func (m *memstore) getClient(id uuid.ID) (Client, error) { |
| paddy@31 | 152 m.clientLock.RLock() |
| paddy@31 | 153 defer m.clientLock.RUnlock() |
| paddy@31 | 154 c, ok := m.clients[id.String()] |
| paddy@31 | 155 if !ok { |
| paddy@31 | 156 return Client{}, ErrClientNotFound |
| paddy@31 | 157 } |
| paddy@31 | 158 return c, nil |
| paddy@31 | 159 } |
| paddy@31 | 160 |
| paddy@57 | 161 func (m *memstore) saveClient(client Client) error { |
| paddy@31 | 162 m.clientLock.Lock() |
| paddy@31 | 163 defer m.clientLock.Unlock() |
| paddy@31 | 164 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 165 return ErrClientAlreadyExists |
| paddy@31 | 166 } |
| paddy@31 | 167 m.clients[client.ID.String()] = client |
| paddy@31 | 168 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 169 return nil |
| paddy@31 | 170 } |
| paddy@31 | 171 |
| paddy@57 | 172 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 173 m.clientLock.Lock() |
| paddy@39 | 174 defer m.clientLock.Unlock() |
| paddy@39 | 175 c, ok := m.clients[id.String()] |
| paddy@39 | 176 if !ok { |
| paddy@39 | 177 return ErrClientNotFound |
| paddy@39 | 178 } |
| paddy@39 | 179 c.ApplyChange(change) |
| paddy@39 | 180 m.clients[id.String()] = c |
| paddy@31 | 181 return nil |
| paddy@31 | 182 } |
| paddy@31 | 183 |
| paddy@57 | 184 func (m *memstore) deleteClient(id uuid.ID) error { |
| paddy@57 | 185 client, err := m.getClient(id) |
| paddy@31 | 186 if err != nil { |
| paddy@31 | 187 return err |
| paddy@31 | 188 } |
| paddy@31 | 189 m.clientLock.Lock() |
| paddy@31 | 190 defer m.clientLock.Unlock() |
| paddy@31 | 191 delete(m.clients, id.String()) |
| paddy@31 | 192 pos := -1 |
| paddy@31 | 193 for p, item := range m.profileClientLookup[client.OwnerID.String()] { |
| paddy@31 | 194 if item.Equal(id) { |
| paddy@31 | 195 pos = p |
| paddy@31 | 196 break |
| paddy@31 | 197 } |
| paddy@31 | 198 } |
| paddy@31 | 199 if pos >= 0 { |
| paddy@31 | 200 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...) |
| paddy@31 | 201 } |
| paddy@31 | 202 return nil |
| paddy@31 | 203 } |
| paddy@31 | 204 |
| paddy@57 | 205 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 206 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@31 | 207 if len(ids) > num+offset { |
| paddy@31 | 208 ids = ids[offset : num+offset] |
| paddy@31 | 209 } else if len(ids) > offset { |
| paddy@31 | 210 ids = ids[offset:] |
| paddy@31 | 211 } else { |
| paddy@31 | 212 return []Client{}, nil |
| paddy@31 | 213 } |
| paddy@31 | 214 clients := []Client{} |
| paddy@31 | 215 for _, id := range ids { |
| paddy@57 | 216 client, err := m.getClient(id) |
| paddy@31 | 217 if err != nil { |
| paddy@31 | 218 return []Client{}, err |
| paddy@31 | 219 } |
| paddy@31 | 220 clients = append(clients, client) |
| paddy@31 | 221 } |
| paddy@31 | 222 return clients, nil |
| paddy@31 | 223 } |
| paddy@41 | 224 |
| paddy@57 | 225 func (m *memstore) addEndpoint(client uuid.ID, endpoint Endpoint) error { |
| paddy@41 | 226 m.endpointLock.Lock() |
| paddy@41 | 227 defer m.endpointLock.Unlock() |
| paddy@41 | 228 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint) |
| paddy@41 | 229 return nil |
| paddy@41 | 230 } |
| paddy@41 | 231 |
| paddy@57 | 232 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 233 m.endpointLock.Lock() |
| paddy@41 | 234 defer m.endpointLock.Unlock() |
| paddy@41 | 235 pos := -1 |
| paddy@41 | 236 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 237 if item.ID.Equal(endpoint) { |
| paddy@41 | 238 pos = p |
| paddy@41 | 239 break |
| paddy@41 | 240 } |
| paddy@41 | 241 } |
| paddy@41 | 242 if pos >= 0 { |
| paddy@41 | 243 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 244 } |
| paddy@41 | 245 return nil |
| paddy@41 | 246 } |
| paddy@41 | 247 |
| paddy@58 | 248 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 249 m.endpointLock.RLock() |
| paddy@41 | 250 defer m.endpointLock.RUnlock() |
| paddy@41 | 251 for _, candidate := range m.endpoints[client.String()] { |
| paddy@58 | 252 if endpoint == candidate.URI.String() { |
| paddy@41 | 253 return true, nil |
| paddy@41 | 254 } |
| paddy@41 | 255 } |
| paddy@41 | 256 return false, nil |
| paddy@41 | 257 } |
| paddy@41 | 258 |
| paddy@57 | 259 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 260 m.endpointLock.RLock() |
| paddy@41 | 261 defer m.endpointLock.RUnlock() |
| paddy@41 | 262 return m.endpoints[client.String()], nil |
| paddy@41 | 263 } |
| paddy@54 | 264 |
| paddy@57 | 265 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) { |
| paddy@54 | 266 m.endpointLock.RLock() |
| paddy@54 | 267 defer m.endpointLock.RUnlock() |
| paddy@54 | 268 return int64(len(m.endpoints[client.String()])), nil |
| paddy@54 | 269 } |