Stub out sessions.
Stop using the Login type when getting profile by Login, removing Logins,
or recording Login use. The Login value has to be unique, anyways, and we don't
actually know the Login type when getting a profile by Login. That's sort of the
point.
Create the concept of Sessions and a sessionStore type to manage our
authentication sessions with the server. As per OWASP, we're basically just
going to use a transparent, SHA256-generated random string as an ID, and store
it client-side and server-side and just pass it back and forth.
Add the ProfileID to the Grant type, because we need to remember who granted
access. That's sort of important.
Set a defaultGrantExpiration constant to an hour, so we have that one constant
when creating new Grants.
Create a helper that pulls the session ID out of an auth cookie, checks it
against the sessionStore, and returns the Session if it's valid.
Create a helper that pulls the username and password out of a basic auth header.
Create a helper that authenticates a user's login and passphrase, checking them
against the profileStore securely.
Stub out how the cookie checking is going to work for getting grant approval.
Fix the stored Grant RedirectURI to be the passed in redirect URI, not the
RedirectURI that we ultimately redirect to. This is in accordance with the spec.
Store the profile ID from our session in the created Grant.
Stub out a GetTokenHandler that will allow users to exchange a Grant for a
Token.
Set a constant for the current passphrase scheme, which we will increment for
each revision to the passphrase scheme, for backwards compatibility.
Change the Profile iterations property to an int, not an int64, to match the
code.secondbit.org/pass library (which is matching the PBKDF2 library).
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 _, 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, red.Query().Get("code"), 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())