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