Break out scopes and events.
This repo has gotten unwieldy, and there are portions of it that need to be
imported by a large number of other packages.
For example, scopes will be used in almost every API we write. Rather than
importing the entirety of this codebase into every API we write, I've opted to
move the scope logic out into a scopes package, with a subpackage for the
defined types, which is all most projects actually want to import.
We also define some event type constants, and importing those shouldn't require
a project to import all our dependencies, either. So I made an events subpackage
that just holds those constants.
This package has become a little bit of a red-headed stepchild and is do for a
refactor, but I'm trying to put that off as long as I can.
The refactoring of our scopes stuff has left a bug wherein a token can be
granted for scopes that don't exist. I'm going to need to revisit that, and also
how to limit scopes to only be granted to the users that should be able to
request them. But that's a battle for another day.
14 "code.secondbit.org/scopes.hg/types"
15 "code.secondbit.org/uuid.hg"
19 if os.Getenv("PG_TEST_DB") != "" {
20 p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
24 authCodeStores = append(authCodeStores, &p)
28 var authCodeStores = []authorizationCodeStore{NewMemstore()}
30 func compareAuthorizationCodes(authCode1, authCode2 AuthorizationCode) (success bool, field string, authCode1val, authCode2val interface{}) {
31 if authCode1.Code != authCode2.Code {
32 return false, "code", authCode1.Code, authCode2.Code
34 if !authCode1.Created.Equal(authCode2.Created) {
35 return false, "created", authCode1.Created, authCode2.Created
37 if authCode1.ExpiresIn != authCode2.ExpiresIn {
38 return false, "expires in", authCode1.ExpiresIn, authCode2.ExpiresIn
40 if !authCode1.ClientID.Equal(authCode2.ClientID) {
41 return false, "client ID", authCode1.ClientID, authCode2.ClientID
43 if len(authCode1.Scopes) != len(authCode2.Scopes) {
44 return false, "scopes", authCode1.Scopes, authCode2.Scopes
46 for pos, scope := range authCode1.Scopes {
47 if scope != authCode2.Scopes[pos] {
48 return false, "scopes", authCode1.Scopes, authCode2.Scopes
51 if authCode1.RedirectURI != authCode2.RedirectURI {
52 return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
54 if authCode1.State != authCode2.State {
55 return false, "state", authCode1.State, authCode2.State
57 if !authCode1.ProfileID.Equal(authCode2.ProfileID) {
58 return false, "profile ID", authCode1.ProfileID, authCode2.ProfileID
60 if authCode1.Used != authCode2.Used {
61 return false, "used", authCode1.Used, authCode2.Used
63 return true, "", nil, nil
66 func TestAuthorizationCodeStore(t *testing.T) {
68 authCode := AuthorizationCode{
70 Created: time.Now().Round(time.Millisecond),
72 ClientID: uuid.NewID(),
73 Scopes: scopeTypes.StringsToScopes([]string{"scope"}),
74 RedirectURI: "redirectURI",
77 for _, store := range authCodeStores {
78 context := Context{authCodes: store}
79 err := context.SaveAuthorizationCode(authCode)
81 t.Errorf("Error saving auth code to %T: %s", store, err)
83 err = context.SaveAuthorizationCode(authCode)
84 if err != ErrAuthorizationCodeAlreadyExists {
85 t.Errorf("Expected ErrAuthorizationCodeAlreadyExists from %T, got %+v", store, err)
87 retrieved, err := context.GetAuthorizationCode(authCode.Code)
89 t.Errorf("Error retrieving auth code from %T: %s", store, err)
91 match, field, expectation, result := compareAuthorizationCodes(authCode, retrieved)
93 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
95 err = context.UseAuthorizationCode(authCode.Code)
97 t.Errorf("Error retrieving auth code from %T: %s", store, err)
99 retrieved, err = context.GetAuthorizationCode(authCode.Code)
101 t.Errorf("Error retrieving auth code from %T: %s", store, err)
104 match, field, expectation, result = compareAuthorizationCodes(authCode, retrieved)
106 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
108 err = context.DeleteAuthorizationCode(authCode.Code)
110 t.Errorf("Error removing auth code from %T: %s", store, err)
112 retrieved, err = context.GetAuthorizationCode(authCode.Code)
113 if err != ErrAuthorizationCodeNotFound {
114 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v and %+v", store, retrieved, err)
116 err = context.DeleteAuthorizationCode(authCode.Code)
117 if err != ErrAuthorizationCodeNotFound {
118 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
120 err = context.UseAuthorizationCode(authCode.Code)
121 if err != ErrAuthorizationCodeNotFound {
122 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
127 func TestAuthCodeGrantValidate(t *testing.T) {
129 store := NewMemstore()
130 testContext := Context{
139 Secret: "super secret!",
140 OwnerID: uuid.NewID(),
141 Name: "My test client",
142 Logo: "https://secondbit.org/logo.png",
143 Website: "https://secondbit.org/",
146 endpoint := Endpoint{
149 URI: "https://test.secondbit.org/redirect",
150 Added: time.Now().Round(time.Millisecond),
152 err := testContext.SaveClient(client)
154 t.Fatal("Can't store client:", err)
156 err = testContext.AddEndpoints([]Endpoint{endpoint})
158 t.Fatal("Can't store endpoint:", err)
160 code := AuthorizationCode{
162 Created: time.Now().Round(time.Millisecond),
164 ClientID: uuid.NewID(),
165 Scopes: scopeTypes.StringsToScopes([]string{"scope"}),
166 RedirectURI: "redirectURI",
169 err = testContext.SaveAuthorizationCode(code)
171 t.Fatal("Can't add auth code:", err)
174 code2.Code = "otherauthcode"
175 code2.ClientID = client.ID
176 err = testContext.SaveAuthorizationCode(code2)
178 t.Fatal("Can't add second auth code:", err)
180 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
182 t.Fatal("Can't build request:", err)
184 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
185 w := httptest.NewRecorder()
186 params := url.Values{}
187 body := bytes.NewBufferString(params.Encode())
188 req.Body = ioutil.NopCloser(body)
189 scope, profileID, valid := authCodeGrantValidate(w, req, testContext)
191 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
193 if w.Code != http.StatusBadRequest {
194 t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
196 expectedBody := `{"error":"invalid_request"}`
197 if strings.TrimSpace(w.Body.String()) != expectedBody {
198 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
201 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
203 t.Fatal("Can't build request:", err)
205 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
206 w = httptest.NewRecorder()
207 params = url.Values{}
208 params.Set("code", "notmycode")
209 body = bytes.NewBufferString(params.Encode())
210 req.Body = ioutil.NopCloser(body)
211 err = req.ParseForm()
215 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
217 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
219 if w.Code != http.StatusUnauthorized {
220 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
222 expectedBody = `{"error":"invalid_client"}`
223 if expectedBody != strings.TrimSpace(w.Body.String()) {
224 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
227 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
229 t.Fatal("Can't build request:", err)
231 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
232 req.SetBasicAuth(client.ID.String(), client.Secret)
233 w = httptest.NewRecorder()
234 params = url.Values{}
235 params.Set("code", "notmycode")
236 body = bytes.NewBufferString(params.Encode())
237 req.Body = ioutil.NopCloser(body)
238 err = req.ParseForm()
242 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
244 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
246 if w.Code != http.StatusBadRequest {
247 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
249 expectedBody = `{"error":"invalid_grant"}`
250 if expectedBody != strings.TrimSpace(w.Body.String()) {
251 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
254 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
256 t.Fatal("Can't build request:", err)
258 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
259 req.SetBasicAuth(client.ID.String(), client.Secret)
260 w = httptest.NewRecorder()
261 params = url.Values{}
262 params.Set("code", code.Code)
263 params.Set("redirect_uri", "not my redirectURI")
264 body = bytes.NewBufferString(params.Encode())
265 req.Body = ioutil.NopCloser(body)
266 err = req.ParseForm()
270 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
272 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
274 if w.Code != http.StatusBadRequest {
275 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
277 expectedBody = `{"error":"invalid_grant"}`
278 if expectedBody != strings.TrimSpace(w.Body.String()) {
279 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
282 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
284 t.Fatal("Can't build request:", err)
286 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
287 req.SetBasicAuth(client.ID.String(), client.Secret)
288 w = httptest.NewRecorder()
289 params = url.Values{}
290 params.Set("code", code.Code)
291 params.Set("redirect_uri", code.RedirectURI)
292 body = bytes.NewBufferString(params.Encode())
293 req.Body = ioutil.NopCloser(body)
294 err = req.ParseForm()
298 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
300 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
302 if w.Code != http.StatusBadRequest {
303 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
305 expectedBody = `{"error":"invalid_grant"}`
306 if expectedBody != strings.TrimSpace(w.Body.String()) {
307 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
310 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
312 t.Fatal("Can't build request:", err)
314 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
315 req.SetBasicAuth(client.ID.String(), client.Secret)
316 w = httptest.NewRecorder()
317 params = url.Values{}
318 params.Set("code", code2.Code)
319 params.Set("redirect_uri", code2.RedirectURI)
320 body = bytes.NewBufferString(params.Encode())
321 req.Body = ioutil.NopCloser(body)
322 err = req.ParseForm()
326 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
328 t.Fatalf("Expected valid auth code, was not valid.")
332 func TestAuthCodeGrantInvalidate(t *testing.T) {
334 store := NewMemstore()
335 testContext := Context{
342 code := AuthorizationCode{
344 Created: time.Now().Round(time.Millisecond),
346 ClientID: uuid.NewID(),
347 Scopes: scopeTypes.StringsToScopes([]string{"scope"}),
348 RedirectURI: "redirectURI",
351 err := testContext.SaveAuthorizationCode(code)
353 t.Fatal("Can't add auth code:", err)
355 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
357 t.Fatal("Can't build request:", err)
359 err = authCodeGrantInvalidate(req, testContext)
360 if err != ErrAuthorizationCodeNotFound {
361 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
363 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
365 t.Fatal("Can't build request:", err)
367 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
368 params := url.Values{}
369 params.Set("code", "notmycode")
370 body := bytes.NewBufferString(params.Encode())
371 req.Body = ioutil.NopCloser(body)
372 err = authCodeGrantInvalidate(req, testContext)
373 if err != ErrAuthorizationCodeNotFound {
374 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
376 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
378 t.Fatal("Can't build request:", err)
380 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
381 params.Set("code", code.Code)
382 body = bytes.NewBufferString(params.Encode())
383 req.Body = ioutil.NopCloser(body)
384 err = authCodeGrantInvalidate(req, testContext)
386 t.Error("Error invalidating auth code:", err)
388 authCode, err := testContext.GetAuthorizationCode(code.Code)
390 t.Error("Error retrieving auth code:", err)
393 t.Error("Expected auth code to be used, was not.")