Use postgres arrays for scope associations.
Use the new pqarrays library I wrote to store Scope associations for Tokens and
AuthorizationCodes, instead of using our hacky and abstraction-breaking
many-to-many code.
We also created the authStore.deleteAuthorizationCodesByProfileID method, to
clear out the AuthorizationCodes that belong to a Profile (used when the Profile
is deleted). So we added the implementation for memstore and for our postgres
store.
Call Context.DeleteAuthorizationCodesByProfileID when deleting a Profile to
clean up after it.
Rename sortedScopes to Scopes, which we use pqarrays.StringArray's methods on to
fulfill the sql.Scanner and driver.Valuer interfaces. This lets us store Scopes
in postgres arrays.
Create a stringsToScopes helper function that creates Scope objects, with their
IDs filled by the strings specified.
Update our GrantType.Validate function signature to return Scopes instead of
[]string.
Create a Scopes.Strings() helper method that returns a []string of the IDs of
the Scopes.
Update our SQL init file to use the new postgres array definition, instead of
the many-to-many definition.
14 "code.secondbit.org/uuid.hg"
18 if os.Getenv("PG_TEST_DB") != "" {
19 p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
23 authCodeStores = append(authCodeStores, &p)
27 var authCodeStores = []authorizationCodeStore{NewMemstore()}
29 func compareAuthorizationCodes(authCode1, authCode2 AuthorizationCode) (success bool, field string, authCode1val, authCode2val interface{}) {
30 if authCode1.Code != authCode2.Code {
31 return false, "code", authCode1.Code, authCode2.Code
33 if !authCode1.Created.Equal(authCode2.Created) {
34 return false, "created", authCode1.Created, authCode2.Created
36 if authCode1.ExpiresIn != authCode2.ExpiresIn {
37 return false, "expires in", authCode1.ExpiresIn, authCode2.ExpiresIn
39 if !authCode1.ClientID.Equal(authCode2.ClientID) {
40 return false, "client ID", authCode1.ClientID, authCode2.ClientID
42 if len(authCode1.Scopes) != len(authCode2.Scopes) {
43 return false, "scopes", authCode1.Scopes, authCode2.Scopes
45 for pos, scope := range authCode1.Scopes {
46 if scope != authCode2.Scopes[pos] {
47 return false, "scopes", authCode1.Scopes, authCode2.Scopes
50 if authCode1.RedirectURI != authCode2.RedirectURI {
51 return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
53 if authCode1.State != authCode2.State {
54 return false, "state", authCode1.State, authCode2.State
56 if !authCode1.ProfileID.Equal(authCode2.ProfileID) {
57 return false, "profile ID", authCode1.ProfileID, authCode2.ProfileID
59 if authCode1.Used != authCode2.Used {
60 return false, "used", authCode1.Used, authCode2.Used
62 return true, "", nil, nil
65 func TestAuthorizationCodeStore(t *testing.T) {
67 authCode := AuthorizationCode{
69 Created: time.Now().Round(time.Millisecond),
71 ClientID: uuid.NewID(),
72 Scopes: stringsToScopes([]string{"scope"}),
73 RedirectURI: "redirectURI",
76 for _, store := range authCodeStores {
77 context := Context{authCodes: store}
78 err := context.SaveAuthorizationCode(authCode)
80 t.Errorf("Error saving auth code to %T: %s", store, err)
82 err = context.SaveAuthorizationCode(authCode)
83 if err != ErrAuthorizationCodeAlreadyExists {
84 t.Errorf("Expected ErrAuthorizationCodeAlreadyExists from %T, got %+v", store, err)
86 retrieved, err := context.GetAuthorizationCode(authCode.Code)
88 t.Errorf("Error retrieving auth code from %T: %s", store, err)
90 match, field, expectation, result := compareAuthorizationCodes(authCode, retrieved)
92 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
94 err = context.UseAuthorizationCode(authCode.Code)
96 t.Errorf("Error retrieving auth code from %T: %s", store, err)
98 retrieved, err = context.GetAuthorizationCode(authCode.Code)
100 t.Errorf("Error retrieving auth code from %T: %s", store, err)
103 match, field, expectation, result = compareAuthorizationCodes(authCode, retrieved)
105 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
107 err = context.DeleteAuthorizationCode(authCode.Code)
109 t.Errorf("Error removing auth code from %T: %s", store, err)
111 retrieved, err = context.GetAuthorizationCode(authCode.Code)
112 if err != ErrAuthorizationCodeNotFound {
113 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v and %+v", store, retrieved, err)
115 err = context.DeleteAuthorizationCode(authCode.Code)
116 if err != ErrAuthorizationCodeNotFound {
117 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
119 err = context.UseAuthorizationCode(authCode.Code)
120 if err != ErrAuthorizationCodeNotFound {
121 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
126 func TestAuthCodeGrantValidate(t *testing.T) {
128 store := NewMemstore()
129 testContext := Context{
138 Secret: "super secret!",
139 OwnerID: uuid.NewID(),
140 Name: "My test client",
141 Logo: "https://secondbit.org/logo.png",
142 Website: "https://secondbit.org/",
145 endpoint := Endpoint{
148 URI: "https://test.secondbit.org/redirect",
149 Added: time.Now().Round(time.Millisecond),
151 err := testContext.SaveClient(client)
153 t.Fatal("Can't store client:", err)
155 err = testContext.AddEndpoints([]Endpoint{endpoint})
157 t.Fatal("Can't store endpoint:", err)
159 code := AuthorizationCode{
161 Created: time.Now().Round(time.Millisecond),
163 ClientID: uuid.NewID(),
164 Scopes: stringsToScopes([]string{"scope"}),
165 RedirectURI: "redirectURI",
168 err = testContext.SaveAuthorizationCode(code)
170 t.Fatal("Can't add auth code:", err)
173 code2.Code = "otherauthcode"
174 code2.ClientID = client.ID
175 err = testContext.SaveAuthorizationCode(code2)
177 t.Fatal("Can't add second auth code:", err)
179 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
181 t.Fatal("Can't build request:", err)
183 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
184 w := httptest.NewRecorder()
185 params := url.Values{}
186 body := bytes.NewBufferString(params.Encode())
187 req.Body = ioutil.NopCloser(body)
188 scope, profileID, valid := authCodeGrantValidate(w, req, testContext)
190 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
192 if w.Code != http.StatusBadRequest {
193 t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
195 expectedBody := `{"error":"invalid_request"}`
196 if strings.TrimSpace(w.Body.String()) != expectedBody {
197 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
200 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
202 t.Fatal("Can't build request:", err)
204 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
205 w = httptest.NewRecorder()
206 params = url.Values{}
207 params.Set("code", "notmycode")
208 body = bytes.NewBufferString(params.Encode())
209 req.Body = ioutil.NopCloser(body)
210 err = req.ParseForm()
214 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
216 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
218 if w.Code != http.StatusUnauthorized {
219 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
221 expectedBody = `{"error":"invalid_client"}`
222 if expectedBody != strings.TrimSpace(w.Body.String()) {
223 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
226 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
228 t.Fatal("Can't build request:", err)
230 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
231 req.SetBasicAuth(client.ID.String(), client.Secret)
232 w = httptest.NewRecorder()
233 params = url.Values{}
234 params.Set("code", "notmycode")
235 body = bytes.NewBufferString(params.Encode())
236 req.Body = ioutil.NopCloser(body)
237 err = req.ParseForm()
241 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
243 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
245 if w.Code != http.StatusBadRequest {
246 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
248 expectedBody = `{"error":"invalid_grant"}`
249 if expectedBody != strings.TrimSpace(w.Body.String()) {
250 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
253 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
255 t.Fatal("Can't build request:", err)
257 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
258 req.SetBasicAuth(client.ID.String(), client.Secret)
259 w = httptest.NewRecorder()
260 params = url.Values{}
261 params.Set("code", code.Code)
262 params.Set("redirect_uri", "not my redirectURI")
263 body = bytes.NewBufferString(params.Encode())
264 req.Body = ioutil.NopCloser(body)
265 err = req.ParseForm()
269 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
271 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
273 if w.Code != http.StatusBadRequest {
274 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
276 expectedBody = `{"error":"invalid_grant"}`
277 if expectedBody != strings.TrimSpace(w.Body.String()) {
278 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
281 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
283 t.Fatal("Can't build request:", err)
285 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
286 req.SetBasicAuth(client.ID.String(), client.Secret)
287 w = httptest.NewRecorder()
288 params = url.Values{}
289 params.Set("code", code.Code)
290 params.Set("redirect_uri", code.RedirectURI)
291 body = bytes.NewBufferString(params.Encode())
292 req.Body = ioutil.NopCloser(body)
293 err = req.ParseForm()
297 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
299 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
301 if w.Code != http.StatusBadRequest {
302 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
304 expectedBody = `{"error":"invalid_grant"}`
305 if expectedBody != strings.TrimSpace(w.Body.String()) {
306 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
309 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
311 t.Fatal("Can't build request:", err)
313 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
314 req.SetBasicAuth(client.ID.String(), client.Secret)
315 w = httptest.NewRecorder()
316 params = url.Values{}
317 params.Set("code", code2.Code)
318 params.Set("redirect_uri", code2.RedirectURI)
319 body = bytes.NewBufferString(params.Encode())
320 req.Body = ioutil.NopCloser(body)
321 err = req.ParseForm()
325 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
327 t.Fatalf("Expected valid auth code, was not valid.")
331 func TestAuthCodeGrantInvalidate(t *testing.T) {
333 store := NewMemstore()
334 testContext := Context{
341 code := AuthorizationCode{
343 Created: time.Now().Round(time.Millisecond),
345 ClientID: uuid.NewID(),
346 Scopes: stringsToScopes([]string{"scope"}),
347 RedirectURI: "redirectURI",
350 err := testContext.SaveAuthorizationCode(code)
352 t.Fatal("Can't add auth code:", err)
354 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
356 t.Fatal("Can't build request:", err)
358 err = authCodeGrantInvalidate(req, testContext)
359 if err != ErrAuthorizationCodeNotFound {
360 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
362 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
364 t.Fatal("Can't build request:", err)
366 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
367 params := url.Values{}
368 params.Set("code", "notmycode")
369 body := bytes.NewBufferString(params.Encode())
370 req.Body = ioutil.NopCloser(body)
371 err = authCodeGrantInvalidate(req, testContext)
372 if err != ErrAuthorizationCodeNotFound {
373 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
375 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
377 t.Fatal("Can't build request:", err)
379 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
380 params.Set("code", code.Code)
381 body = bytes.NewBufferString(params.Encode())
382 req.Body = ioutil.NopCloser(body)
383 err = authCodeGrantInvalidate(req, testContext)
385 t.Error("Error invalidating auth code:", err)
387 authCode, err := testContext.GetAuthorizationCode(code.Code)
389 t.Error("Error retrieving auth code:", err)
392 t.Error("Expected auth code to be used, was not.")