auth

Paddy 2014-10-26 Parent:e45bfa2abc00 Child:1dc4e152e3b0

58:b3cd7765a7c8 Go to Latest

auth/client.go

Require full URLs for Endpoints. The spec says that we SHOULD require full URLs for redirection, but we _can_ offer the ability to set a URL as a "partial URL" if we really must. I see no particular reason to do this, so I've simplified the code by pulling that option out. This means that URLs (as long as they're normalized, which I've filed a bug in the codebase to do) can be checked using simple string comparison, which makes the likelihood of bugs across clientStorage implementations a lot lower.

History
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 }