auth

Paddy 2014-12-13 Parent:1dc4e152e3b0 Child:c03b5eb3179e

92:e81407f7ecb1 Go to Latest

auth/client.go

Remove old BUG. We implemented revoking tokens, so let's remove the BUG designation.

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