Refactor verifyClient, implement refresh tokens.
Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient
out of each of the GrantType's validation functions and into the access token
endpoint, where it will be called before the GrantType's validation function.
Yay, less code repetition. And seeing as we always want to verify the client,
that seems like a good way to prevent things like 118a69954621 from happening.
This did, however, force us to add an AllowsPublic property to the GrantType, so
the token endpoint knows whether or not a public Client is valid for any given
GrantType.
We also implemented the refresh token grant type, which required adding ClientID
and RefreshRevoked as properties on the Token type. We need ClientID because we
need to constrain refresh tokens to the client that issued them. We also should
probably keep track of which tokens belong to which clients, just as a general
rule of thumb. RefreshRevoked had to be created, next to Revoked, because the
AccessToken could be revoked and the RefreshToken still valid, or vice versa.
Notably, when you issue a new refresh token, the old one is revoked, but the
access token is still valid. It remains to be seen whether this is a good way to
track things or not. The number of duplicated properties lead me to believe our
type is not a great representation of the underlying concepts.
13 "code.secondbit.org/uuid.hg"
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
22 if !authCode1.Created.Equal(authCode2.Created) {
23 return false, "created", authCode1.Created, authCode2.Created
25 if authCode1.ExpiresIn != authCode2.ExpiresIn {
26 return false, "expires in", authCode1.ExpiresIn, authCode2.ExpiresIn
28 if !authCode1.ClientID.Equal(authCode2.ClientID) {
29 return false, "client ID", authCode1.ClientID, authCode2.ClientID
31 if authCode1.Scope != authCode2.Scope {
32 return false, "scope", authCode1.Scope, authCode2.Scope
34 if authCode1.RedirectURI != authCode2.RedirectURI {
35 return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
37 if authCode1.State != authCode2.State {
38 return false, "state", authCode1.State, authCode2.State
40 if !authCode1.ProfileID.Equal(authCode2.ProfileID) {
41 return false, "profile ID", authCode1.ProfileID, authCode2.ProfileID
43 if authCode1.Used != authCode2.Used {
44 return false, "used", authCode1.Used, authCode2.Used
46 return true, "", nil, nil
49 func TestAuthorizationCodeStore(t *testing.T) {
51 authCode := AuthorizationCode{
55 ClientID: uuid.NewID(),
57 RedirectURI: "redirectURI",
60 for _, store := range authCodeStores {
61 context := Context{authCodes: store}
62 err := context.SaveAuthorizationCode(authCode)
64 t.Errorf("Error saving auth code to %T: %s", store, err)
66 err = context.SaveAuthorizationCode(authCode)
67 if err != ErrAuthorizationCodeAlreadyExists {
68 t.Errorf("Expected ErrAuthorizationCodeAlreadyExists from %T, got %+v", store, err)
70 retrieved, err := context.GetAuthorizationCode(authCode.Code)
72 t.Errorf("Error retrieving auth code from %T: %s", store, err)
74 match, field, expectation, result := compareAuthorizationCodes(authCode, retrieved)
76 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
78 err = context.UseAuthorizationCode(authCode.Code)
80 t.Errorf("Error retrieving auth code from %T: %s", store, err)
82 retrieved, err = context.GetAuthorizationCode(authCode.Code)
84 t.Errorf("Error retrieving auth code from %T: %s", store, err)
87 match, field, expectation, result = compareAuthorizationCodes(authCode, retrieved)
89 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
91 err = context.DeleteAuthorizationCode(authCode.Code)
93 t.Errorf("Error removing auth code from %T: %s", store, err)
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)
99 err = context.DeleteAuthorizationCode(authCode.Code)
100 if err != ErrAuthorizationCodeNotFound {
101 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
103 err = context.UseAuthorizationCode(authCode.Code)
104 if err != ErrAuthorizationCodeNotFound {
105 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
110 func TestAuthCodeGrantValidate(t *testing.T) {
112 store := NewMemstore()
113 testContext := Context{
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/",
129 endpoint := Endpoint{
132 URI: "https://test.secondbit.org/redirect",
135 err := testContext.SaveClient(client)
137 t.Fatal("Can't store client:", err)
139 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
141 t.Fatal("Can't store endpoint:", err)
143 code := AuthorizationCode{
147 ClientID: uuid.NewID(),
149 RedirectURI: "redirectURI",
152 err = testContext.SaveAuthorizationCode(code)
154 t.Fatal("Can't add auth code:", err)
157 code2.Code = "otherauthcode"
158 code2.ClientID = client.ID
159 err = testContext.SaveAuthorizationCode(code2)
161 t.Fatal("Can't add second auth code:", err)
163 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
165 t.Fatal("Can't build request:", err)
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)
174 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
176 if w.Code != http.StatusBadRequest {
177 t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
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()))
184 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
186 t.Fatal("Can't build request:", err)
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()
198 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
200 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
202 if w.Code != http.StatusUnauthorized {
203 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
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()))
210 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
212 t.Fatal("Can't build request:", err)
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()
225 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
227 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
229 if w.Code != http.StatusBadRequest {
230 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
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()))
237 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
239 t.Fatal("Can't build request:", err)
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()
253 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
255 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
257 if w.Code != http.StatusBadRequest {
258 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
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()))
265 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
267 t.Fatal("Can't build request:", err)
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()
281 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
283 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
285 if w.Code != http.StatusBadRequest {
286 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
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()))
293 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
295 t.Fatal("Can't build request:", err)
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()
309 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
311 t.Fatalf("Expected valid auth code, was not valid.")
315 func TestAuthCodeGrantInvalidate(t *testing.T) {
317 store := NewMemstore()
318 testContext := Context{
325 code := AuthorizationCode{
329 ClientID: uuid.NewID(),
331 RedirectURI: "redirectURI",
334 err := testContext.SaveAuthorizationCode(code)
336 t.Fatal("Can't add auth code:", err)
338 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
340 t.Fatal("Can't build request:", err)
342 err = authCodeGrantInvalidate(req, testContext)
343 if err != ErrAuthorizationCodeNotFound {
344 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
346 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
348 t.Fatal("Can't build request:", err)
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)
359 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
361 t.Fatal("Can't build request:", err)
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)
369 t.Error("Error invalidating auth code:", err)
371 authCode, err := testContext.GetAuthorizationCode(code.Code)
373 t.Error("Error retrieving auth code:", err)
376 t.Error("Expected auth code to be used, was not.")