Bugfixes and tests for getting grants.
Add tests for the grant confirmation part of the request to get a grant code.
When the request is denied, the redirect should have an access_denied error.
When the request is approved, the redirect should contain a code.
Fix numerous bugs in which the redirect URL didn't contain the parameters we
thought it would. Basically, anything that still used req.URL.Query().Set()
instead of copying the query, modifying it, and setting req.URL.RawQuery.
Fix a bug in which the redirectURL wasn't being properly set. Basically, when we
moved the redirectURI processing to the top of the file (c29c7df35905 for those
who forgot), we didn't update the reference to it lower in the file, where
redirectURI was being updated and we expected that to be reflected in the
processing.
The HTTP handler for getting grant codes is now completely tested except for
returning internal errors, which requires a new test harness be built to provoke
internal errors on demand. At this point, however, I'd like to continue on
implementing endpoints.
13 "code.secondbit.org/uuid"
22 func stripParam(param string, u *url.URL) {
25 u.RawQuery = q.Encode()
28 func TestGetGrantCodeSuccess(t *testing.T) {
30 store := NewMemstore()
31 testContext := Context{
32 template: template.Must(template.New(getGrantTemplateName).Parse("Get auth grant")),
40 Secret: "super secret!",
41 OwnerID: uuid.NewID(),
42 Name: "My test client",
43 Logo: "https://secondbit.org/logo.png",
44 Website: "https://secondbit.org",
47 uri, err := url.Parse("https://test.secondbit.org/redirect")
49 t.Fatal("Can't parse URL:", err)
57 err = testContext.SaveClient(client)
59 t.Fatal("Can't store client:", err)
61 err = testContext.AddEndpoint(client.ID, endpoint)
63 t.Fatal("Can't store endpoint:", err)
65 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
67 t.Fatal("Can't build request:", err)
69 for i := 0; i < 1<<3; i++ {
70 w := httptest.NewRecorder()
71 params := url.Values{}
72 // see OAuth 2.0 spec, section 4.1.1
73 params.Set("response_type", "code")
74 params.Set("client_id", client.ID.String())
76 params.Set("redirect_uri", endpoint.URI.String())
79 params.Set("scope", "testscope")
82 params.Set("state", "my super secure state string")
84 req.URL.RawQuery = params.Encode()
87 req.Header.Del("Content-Type")
88 GetGrantHandler(w, req, testContext)
89 if w.Code != http.StatusOK {
90 t.Errorf("Expected status code to be %d, got %d for %s", http.StatusOK, w.Code, req.URL.String())
92 if w.Body.String() != "Get auth grant" {
93 t.Errorf("Expected body to be `%s`, got `%s` for %s", "Get auth grant", w.Body.String(), req.URL.String())
96 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
97 w = httptest.NewRecorder()
99 data.Set("grant", "approved")
100 body := bytes.NewBufferString(data.Encode())
101 req.Body = ioutil.NopCloser(body)
102 GetGrantHandler(w, req, testContext)
103 if w.Code != http.StatusFound {
104 t.Errorf("Expected status code to be %d, got %d for %s", http.StatusFound, w.Code, req.URL.String())
106 redirectedTo := w.Header().Get("Location")
107 red, err := url.Parse(redirectedTo)
109 t.Fatalf(`Being redirected to a non-URL "%s" threw error "%s" for "%s"\n`, redirectedTo, err, req.URL.String())
111 t.Log("Redirected to", redirectedTo)
112 if red.Query().Get("code") == "" {
113 t.Fatalf(`Expected code param in redirect URL to be set, but it wasn't for %s`, req.URL.String())
115 if grant, err := testContext.GetGrant(red.Query().Get("code")); err != nil {
116 t.Fatalf(`Unexpected error "%s: retrieving the grant "%s" supplied in the redirect URL for %s`, err, grant, req.URL.String())
118 err = testContext.DeleteGrant(red.Query().Get("code"))
120 t.Log(`Unexpected error "%s" deleting grant "%s" for %s`, err, red.Query().Get("code"), req.URL.String())
122 stripParam("code", red)
123 if red.Query().Get("state") != params.Get("state") {
124 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s" for %s`, params.Get("state"), red.Query().Get("state"), req.URL.String())
126 stripParam("state", red)
127 if red.String() != endpoint.URI.String() {
128 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), red.String())
133 func TestGetGrantCodeInvalidClient(t *testing.T) {
135 store := NewMemstore()
136 testContext := Context{
137 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
145 Secret: "super secret!",
146 OwnerID: uuid.NewID(),
147 Name: "My test client",
150 err := testContext.SaveClient(client)
152 t.Fatal("Can't store client:", err)
154 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
156 t.Fatal("Can't build request:", err)
158 w := httptest.NewRecorder()
159 params := url.Values{}
160 params.Set("response_type", "code")
161 params.Set("redirect_uri", "https://test.secondbit.org/")
162 req.URL.RawQuery = params.Encode()
163 GetGrantHandler(w, req, testContext)
164 if w.Code != http.StatusBadRequest {
165 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
167 if w.Body.String() != "Client ID must be specified in the request." {
168 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "Client ID must be specified in the request.", w.Body.String())
170 w = httptest.NewRecorder()
171 params.Set("client_id", "Not an ID")
172 req.URL.RawQuery = params.Encode()
173 GetGrantHandler(w, req, testContext)
174 if w.Code != http.StatusBadRequest {
175 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
177 if w.Body.String() != "client_id is not a valid Client ID." {
178 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "client_id is not a valid Client ID.", w.Body.String())
180 w = httptest.NewRecorder()
181 params.Set("client_id", uuid.NewID().String())
182 req.URL.RawQuery = params.Encode()
183 GetGrantHandler(w, req, testContext)
184 if w.Code != http.StatusBadRequest {
185 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
187 if w.Body.String() != "The specified Client couldn’t be found." {
188 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The specified Client couldn’t be found.", w.Body.String())
192 func TestGetGrantCodeInvalidURI(t *testing.T) {
194 store := NewMemstore()
195 testContext := Context{
196 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
204 Secret: "super secret!",
205 OwnerID: uuid.NewID(),
206 Name: "My test client",
209 uri, err := url.Parse("https://test.secondbit.org/redirect")
211 t.Fatal("Can't parse URL:", err)
213 err = testContext.SaveClient(client)
215 t.Fatal("Can't store client:", err)
217 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
219 t.Fatal("Can't build request:", err)
221 w := httptest.NewRecorder()
222 params := url.Values{}
223 params.Set("response_type", "code")
224 params.Set("client_id", client.ID.String())
225 req.URL.RawQuery = params.Encode()
226 GetGrantHandler(w, req, testContext)
227 if w.Code != http.StatusBadRequest {
228 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
230 if w.Body.String() != "The redirect_uri specified is not valid." {
231 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
233 endpoint := Endpoint{
239 err = testContext.AddEndpoint(client.ID, endpoint)
241 t.Fatal("Can't store endpoint:", err)
243 w = httptest.NewRecorder()
244 params.Set("redirect_uri", "https://test.secondbit.org/wrong")
245 req.URL.RawQuery = params.Encode()
246 GetGrantHandler(w, req, testContext)
247 if w.Code != http.StatusBadRequest {
248 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
250 if w.Body.String() != "The redirect_uri specified is not valid." {
251 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
253 endpoint2 := Endpoint{
259 err = testContext.AddEndpoint(client.ID, endpoint2)
261 t.Fatal("Can't store endpoint:", err)
263 w = httptest.NewRecorder()
264 params.Set("redirect_uri", "")
265 req.URL.RawQuery = params.Encode()
266 GetGrantHandler(w, req, testContext)
267 if w.Code != http.StatusBadRequest {
268 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
270 if w.Body.String() != "The redirect_uri specified is not valid." {
271 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
273 w = httptest.NewRecorder()
274 params.Set("redirect_uri", "://not a URL")
275 req.URL.RawQuery = params.Encode()
276 GetGrantHandler(w, req, testContext)
277 if w.Code != http.StatusBadRequest {
278 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
280 if w.Body.String() != "The redirect_uri specified is not valid." {
281 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
285 func TestGetGrantCodeInvalidResponseType(t *testing.T) {
287 store := NewMemstore()
288 testContext := Context{
289 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
297 Secret: "super secret!",
298 OwnerID: uuid.NewID(),
299 Name: "My test client",
300 Logo: "https://secondbit.org/logo.png",
301 Website: "https://secondbit.org",
304 uri, err := url.Parse("https://test.secondbit.org/redirect")
306 t.Fatal("Can't parse URL:", err)
308 endpoint := Endpoint{
314 err = testContext.SaveClient(client)
316 t.Fatal("Can't store client:", err)
318 err = testContext.AddEndpoint(client.ID, endpoint)
320 t.Fatal("Can't store endpoint:", err)
322 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
324 t.Fatal("Can't build request:", err)
326 params := url.Values{}
327 params.Set("response_type", "totally not code")
328 params.Set("client_id", client.ID.String())
329 params.Set("redirect_uri", endpoint.URI.String())
330 params.Set("scope", "testscope")
331 params.Set("state", "my super secure state string")
332 req.URL.RawQuery = params.Encode()
333 w := httptest.NewRecorder()
334 GetGrantHandler(w, req, testContext)
335 if w.Code != http.StatusFound {
336 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
338 redirectedTo := w.Header().Get("Location")
339 red, err := url.Parse(redirectedTo)
341 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
343 if red.Query().Get("error") != "invalid_request" {
344 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
346 stripParam("error", red)
347 if red.Query().Get("state") != params.Get("state") {
348 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
350 stripParam("state", red)
351 if red.String() != endpoint.URI.String() {
352 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), red.String())
354 stripParam("response_type", req.URL)
355 w = httptest.NewRecorder()
356 GetGrantHandler(w, req, testContext)
357 if w.Code != http.StatusFound {
358 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
360 redirectedTo = w.Header().Get("Location")
361 red, err = url.Parse(redirectedTo)
363 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
365 if red.Query().Get("error") != "invalid_request" {
366 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
368 stripParam("error", red)
369 if red.Query().Get("state") != params.Get("state") {
370 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
372 stripParam("state", red)
373 if red.String() != endpoint.URI.String() {
374 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), red.String())
378 func TestGetGrantCodeDenied(t *testing.T) {
380 store := NewMemstore()
381 testContext := Context{
382 template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
390 Secret: "super secret!",
391 OwnerID: uuid.NewID(),
392 Name: "My test client",
393 Logo: "https://secondbit.org/logo.png",
394 Website: "https://secondbit.org",
397 uri, err := url.Parse("https://test.secondbit.org/redirect")
399 t.Fatal("Can't parse URL:", err)
401 endpoint := Endpoint{
407 err = testContext.SaveClient(client)
409 t.Fatal("Can't store client:", err)
411 err = testContext.AddEndpoint(client.ID, endpoint)
413 t.Fatal("Can't store endpoint:", err)
415 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
417 t.Fatal("Can't build request:", err)
419 params := url.Values{}
420 params.Set("response_type", "code")
421 params.Set("client_id", client.ID.String())
422 params.Set("redirect_uri", endpoint.URI.String())
423 params.Set("scope", "testscope")
424 params.Set("state", "my super secure state string")
426 data.Set("grant", "denied")
427 req.URL.RawQuery = params.Encode()
428 req.Body = ioutil.NopCloser(bytes.NewBufferString(data.Encode()))
430 w := httptest.NewRecorder()
431 GetGrantHandler(w, req, testContext)
432 if w.Code != http.StatusFound {
433 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
435 redirectedTo := w.Header().Get("Location")
436 red, err := url.Parse(redirectedTo)
438 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
440 if red.Query().Get("error") != "access_denied" {
441 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "access_denied", red.Query().Get("error"))
443 stripParam("error", red)
444 if red.Query().Get("state") != params.Get("state") {
445 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
447 stripParam("state", red)
448 if red.String() != endpoint.URI.String() {
449 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), red.String())