auth

Paddy 2015-01-18 Parent:fa8ee6a4507c Child:d30a3a12d387

116:e000b1c24fc0 Go to Latest

auth/authcode_test.go

Make all tests that deal with the store interfaces go through the Context. This is mainly important so that pre- and post- save/retrieval/deletion/whatever transforms can be done without doing them in every single implementation of the store. Change the Endpoint URI property to be a string, not a *url.URL. This makes testing easier, JSON responses cleaner, and is all around just a better strategy. Just because we turn it into a URL every now and then doesn't mean that's how we need to store it. Add JSON tags to the Client type and Endpoint type. Create normalizeURI and normalizeURIString methods to... well, normalize the Endpoint URIs. This makes it so that we can compare them, and forgive some arbitrary user behaviour (like slashes, etc.) Add a NormalizedURI property to the Endpoint type. This is where we store the NormalizedURI, which is what we'll be using when we want to check if an endpoint is valid or not. For the sake of tests and predictability, however, we always want to redirect to the URI, not the NormalizedURI. Add checks to the Client creation API endpoint to give better errors. Now leaving out the Type won't be considered an invalid type, it will be considered a missing parameter. An empty name will be reported as a missing parameter, a name with too few characters will be reported as an insufficient name, and a name with too many characters will be reported as an overflow name. We gather as many of these errors as apply before returning. Check if an Endpoint URI is absolute before adding it as an endpoint, or return an invalid value error if it is not. Always return the errors array when creating a client. We could succeed in creating one or more things and still have errors. We should return anything that's created _as well as_ any errors encountered. Add unit testing for our CreateClientHandler. Fix our oauth2 tests so that if there's an error in the body, it's in the test logs. This should help debugging significantly. Fix our oauth2 tests so that the Profile only requires 1 iteration for its password hashing. This means each time we want to validate a session, it doesn't add a full second to our test runs. This is a big speed improvement for our tests. Add test helper methods for comparing API errors, API responses, and filling in server-generated information in a response that it's impossible to have an expectation around (e.g., IDs) so that we can use our comparison helpers to check if a response is as we expect it. Fix a typo in our Context helpers that was reporting no sessionStore being set _only_ when a sessionStore was set. So yes, the opposite of what we wanted. Oops. This was discovered by passing all our tests through the context. methods instead of operating on the stores themselves.

History
1 package auth
3 import (
4 "bytes"
5 "io/ioutil"
6 "net/http"
7 "net/http/httptest"
8 "net/url"
9 "strings"
10 "testing"
11 "time"
13 "code.secondbit.org/uuid.hg"
14 )
16 var authCodeStores = []authorizationCodeStore{NewMemstore()}
18 func compareAuthorizationCodes(authCode1, authCode2 AuthorizationCode) (success bool, field string, authCode1val, authCode2val interface{}) {
19 if authCode1.Code != authCode2.Code {
20 return false, "code", authCode1.Code, authCode2.Code
21 }
22 if !authCode1.Created.Equal(authCode2.Created) {
23 return false, "created", authCode1.Created, authCode2.Created
24 }
25 if authCode1.ExpiresIn != authCode2.ExpiresIn {
26 return false, "expires in", authCode1.ExpiresIn, authCode2.ExpiresIn
27 }
28 if !authCode1.ClientID.Equal(authCode2.ClientID) {
29 return false, "client ID", authCode1.ClientID, authCode2.ClientID
30 }
31 if authCode1.Scope != authCode2.Scope {
32 return false, "scope", authCode1.Scope, authCode2.Scope
33 }
34 if authCode1.RedirectURI != authCode2.RedirectURI {
35 return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
36 }
37 if authCode1.State != authCode2.State {
38 return false, "state", authCode1.State, authCode2.State
39 }
40 if !authCode1.ProfileID.Equal(authCode2.ProfileID) {
41 return false, "profile ID", authCode1.ProfileID, authCode2.ProfileID
42 }
43 if authCode1.Used != authCode2.Used {
44 return false, "used", authCode1.Used, authCode2.Used
45 }
46 return true, "", nil, nil
47 }
49 func TestAuthorizationCodeStore(t *testing.T) {
50 t.Parallel()
51 authCode := AuthorizationCode{
52 Code: "code",
53 Created: time.Now(),
54 ExpiresIn: 180,
55 ClientID: uuid.NewID(),
56 Scope: "scope",
57 RedirectURI: "redirectURI",
58 State: "state",
59 }
60 for _, store := range authCodeStores {
61 context := Context{authCodes: store}
62 err := context.SaveAuthorizationCode(authCode)
63 if err != nil {
64 t.Errorf("Error saving auth code to %T: %s", store, err)
65 }
66 err = context.SaveAuthorizationCode(authCode)
67 if err != ErrAuthorizationCodeAlreadyExists {
68 t.Errorf("Expected ErrAuthorizationCodeAlreadyExists from %T, got %+v", store, err)
69 }
70 retrieved, err := context.GetAuthorizationCode(authCode.Code)
71 if err != nil {
72 t.Errorf("Error retrieving auth code from %T: %s", store, err)
73 }
74 match, field, expectation, result := compareAuthorizationCodes(authCode, retrieved)
75 if !match {
76 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
77 }
78 err = context.UseAuthorizationCode(authCode.Code)
79 if err != nil {
80 t.Errorf("Error retrieving auth code from %T: %s", store, err)
81 }
82 retrieved, err = context.GetAuthorizationCode(authCode.Code)
83 if err != nil {
84 t.Errorf("Error retrieving auth code from %T: %s", store, err)
85 }
86 authCode.Used = true
87 match, field, expectation, result = compareAuthorizationCodes(authCode, retrieved)
88 if !match {
89 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
90 }
91 err = context.DeleteAuthorizationCode(authCode.Code)
92 if err != nil {
93 t.Errorf("Error removing auth code from %T: %s", store, err)
94 }
95 retrieved, err = context.GetAuthorizationCode(authCode.Code)
96 if err != ErrAuthorizationCodeNotFound {
97 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v and %+v", store, retrieved, err)
98 }
99 err = context.DeleteAuthorizationCode(authCode.Code)
100 if err != ErrAuthorizationCodeNotFound {
101 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
102 }
103 err = context.UseAuthorizationCode(authCode.Code)
104 if err != ErrAuthorizationCodeNotFound {
105 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
106 }
107 }
108 }
110 func TestAuthCodeGrantValidate(t *testing.T) {
111 t.Parallel()
112 store := NewMemstore()
113 testContext := Context{
114 clients: store,
115 authCodes: store,
116 profiles: store,
117 tokens: store,
118 sessions: store,
119 }
120 client := Client{
121 ID: uuid.NewID(),
122 Secret: "super secret!",
123 OwnerID: uuid.NewID(),
124 Name: "My test client",
125 Logo: "https://secondbit.org/logo.png",
126 Website: "https://secondbit.org/",
127 Type: "public",
128 }
129 endpoint := Endpoint{
130 ID: uuid.NewID(),
131 ClientID: client.ID,
132 URI: "https://test.secondbit.org/redirect",
133 Added: time.Now(),
134 }
135 err := testContext.SaveClient(client)
136 if err != nil {
137 t.Fatal("Can't store client:", err)
138 }
139 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
140 if err != nil {
141 t.Fatal("Can't store endpoint:", err)
142 }
143 code := AuthorizationCode{
144 Code: "myauthcode",
145 Created: time.Now(),
146 ExpiresIn: 180,
147 ClientID: uuid.NewID(),
148 Scope: "scope",
149 RedirectURI: "redirectURI",
150 State: "state",
151 }
152 err = testContext.SaveAuthorizationCode(code)
153 if err != nil {
154 t.Fatal("Can't add auth code:", err)
155 }
156 code2 := code
157 code2.Code = "otherauthcode"
158 code2.ClientID = client.ID
159 err = testContext.SaveAuthorizationCode(code2)
160 if err != nil {
161 t.Fatal("Can't add second auth code:", err)
162 }
163 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
164 if err != nil {
165 t.Fatal("Can't build request:", err)
166 }
167 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
168 w := httptest.NewRecorder()
169 params := url.Values{}
170 body := bytes.NewBufferString(params.Encode())
171 req.Body = ioutil.NopCloser(body)
172 scope, profileID, valid := authCodeGrantValidate(w, req, testContext)
173 if valid {
174 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
175 }
176 if w.Code != http.StatusBadRequest {
177 t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
178 }
179 expectedBody := `{"error":"invalid_request"}`
180 if strings.TrimSpace(w.Body.String()) != expectedBody {
181 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
182 }
184 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
185 if err != nil {
186 t.Fatal("Can't build request:", err)
187 }
188 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
189 w = httptest.NewRecorder()
190 params = url.Values{}
191 params.Set("code", "notmycode")
192 body = bytes.NewBufferString(params.Encode())
193 req.Body = ioutil.NopCloser(body)
194 err = req.ParseForm()
195 if err != nil {
196 t.Log(err)
197 }
198 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
199 if valid {
200 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
201 }
202 if w.Code != http.StatusUnauthorized {
203 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
204 }
205 expectedBody = `{"error":"invalid_client"}`
206 if expectedBody != strings.TrimSpace(w.Body.String()) {
207 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
208 }
210 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
211 if err != nil {
212 t.Fatal("Can't build request:", err)
213 }
214 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
215 req.SetBasicAuth(client.ID.String(), client.Secret)
216 w = httptest.NewRecorder()
217 params = url.Values{}
218 params.Set("code", "notmycode")
219 body = bytes.NewBufferString(params.Encode())
220 req.Body = ioutil.NopCloser(body)
221 err = req.ParseForm()
222 if err != nil {
223 t.Log(err)
224 }
225 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
226 if valid {
227 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
228 }
229 if w.Code != http.StatusBadRequest {
230 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
231 }
232 expectedBody = `{"error":"invalid_grant"}`
233 if expectedBody != strings.TrimSpace(w.Body.String()) {
234 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
235 }
237 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
238 if err != nil {
239 t.Fatal("Can't build request:", err)
240 }
241 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
242 req.SetBasicAuth(client.ID.String(), client.Secret)
243 w = httptest.NewRecorder()
244 params = url.Values{}
245 params.Set("code", code.Code)
246 params.Set("redirect_uri", "not my redirectURI")
247 body = bytes.NewBufferString(params.Encode())
248 req.Body = ioutil.NopCloser(body)
249 err = req.ParseForm()
250 if err != nil {
251 t.Log(err)
252 }
253 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
254 if valid {
255 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
256 }
257 if w.Code != http.StatusBadRequest {
258 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
259 }
260 expectedBody = `{"error":"invalid_grant"}`
261 if expectedBody != strings.TrimSpace(w.Body.String()) {
262 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
263 }
265 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
266 if err != nil {
267 t.Fatal("Can't build request:", err)
268 }
269 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
270 req.SetBasicAuth(client.ID.String(), client.Secret)
271 w = httptest.NewRecorder()
272 params = url.Values{}
273 params.Set("code", code.Code)
274 params.Set("redirect_uri", code.RedirectURI)
275 body = bytes.NewBufferString(params.Encode())
276 req.Body = ioutil.NopCloser(body)
277 err = req.ParseForm()
278 if err != nil {
279 t.Log(err)
280 }
281 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
282 if valid {
283 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
284 }
285 if w.Code != http.StatusBadRequest {
286 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
287 }
288 expectedBody = `{"error":"invalid_grant"}`
289 if expectedBody != strings.TrimSpace(w.Body.String()) {
290 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
291 }
293 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
294 if err != nil {
295 t.Fatal("Can't build request:", err)
296 }
297 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
298 req.SetBasicAuth(client.ID.String(), client.Secret)
299 w = httptest.NewRecorder()
300 params = url.Values{}
301 params.Set("code", code2.Code)
302 params.Set("redirect_uri", code2.RedirectURI)
303 body = bytes.NewBufferString(params.Encode())
304 req.Body = ioutil.NopCloser(body)
305 err = req.ParseForm()
306 if err != nil {
307 t.Log(err)
308 }
309 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
310 if !valid {
311 t.Fatalf("Expected valid auth code, was not valid.")
312 }
313 }
315 func TestAuthCodeGrantInvalidate(t *testing.T) {
316 t.Parallel()
317 store := NewMemstore()
318 testContext := Context{
319 clients: store,
320 authCodes: store,
321 profiles: store,
322 tokens: store,
323 sessions: store,
324 }
325 code := AuthorizationCode{
326 Code: "myauthcode",
327 Created: time.Now(),
328 ExpiresIn: 180,
329 ClientID: uuid.NewID(),
330 Scope: "scope",
331 RedirectURI: "redirectURI",
332 State: "state",
333 }
334 err := testContext.SaveAuthorizationCode(code)
335 if err != nil {
336 t.Fatal("Can't add auth code:", err)
337 }
338 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
339 if err != nil {
340 t.Fatal("Can't build request:", err)
341 }
342 err = authCodeGrantInvalidate(req, testContext)
343 if err != ErrAuthorizationCodeNotFound {
344 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
345 }
346 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
347 if err != nil {
348 t.Fatal("Can't build request:", err)
349 }
350 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
351 params := url.Values{}
352 params.Set("code", "notmycode")
353 body := bytes.NewBufferString(params.Encode())
354 req.Body = ioutil.NopCloser(body)
355 err = authCodeGrantInvalidate(req, testContext)
356 if err != ErrAuthorizationCodeNotFound {
357 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
358 }
359 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
360 if err != nil {
361 t.Fatal("Can't build request:", err)
362 }
363 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
364 params.Set("code", code.Code)
365 body = bytes.NewBufferString(params.Encode())
366 req.Body = ioutil.NopCloser(body)
367 err = authCodeGrantInvalidate(req, testContext)
368 if err != nil {
369 t.Error("Error invalidating auth code:", err)
370 }
371 authCode, err := testContext.GetAuthorizationCode(code.Code)
372 if err != nil {
373 t.Error("Error retrieving auth code:", err)
374 }
375 if !authCode.Used {
376 t.Error("Expected auth code to be used, was not.")
377 }
378 }