auth

Paddy 2015-01-18 Parent:fa8ee6a4507c Child:23c1a07c8a61

116:e000b1c24fc0 Go to Latest

auth/oauth2_test.go

Make all tests that deal with the store interfaces go through the Context. This is mainly important so that pre- and post- save/retrieval/deletion/whatever transforms can be done without doing them in every single implementation of the store. Change the Endpoint URI property to be a string, not a *url.URL. This makes testing easier, JSON responses cleaner, and is all around just a better strategy. Just because we turn it into a URL every now and then doesn't mean that's how we need to store it. Add JSON tags to the Client type and Endpoint type. Create normalizeURI and normalizeURIString methods to... well, normalize the Endpoint URIs. This makes it so that we can compare them, and forgive some arbitrary user behaviour (like slashes, etc.) Add a NormalizedURI property to the Endpoint type. This is where we store the NormalizedURI, which is what we'll be using when we want to check if an endpoint is valid or not. For the sake of tests and predictability, however, we always want to redirect to the URI, not the NormalizedURI. Add checks to the Client creation API endpoint to give better errors. Now leaving out the Type won't be considered an invalid type, it will be considered a missing parameter. An empty name will be reported as a missing parameter, a name with too few characters will be reported as an insufficient name, and a name with too many characters will be reported as an overflow name. We gather as many of these errors as apply before returning. Check if an Endpoint URI is absolute before adding it as an endpoint, or return an invalid value error if it is not. Always return the errors array when creating a client. We could succeed in creating one or more things and still have errors. We should return anything that's created _as well as_ any errors encountered. Add unit testing for our CreateClientHandler. Fix our oauth2 tests so that if there's an error in the body, it's in the test logs. This should help debugging significantly. Fix our oauth2 tests so that the Profile only requires 1 iteration for its password hashing. This means each time we want to validate a session, it doesn't add a full second to our test runs. This is a big speed improvement for our tests. Add test helper methods for comparing API errors, API responses, and filling in server-generated information in a response that it's impossible to have an expectation around (e.g., IDs) so that we can use our comparison helpers to check if a response is as we expect it. Fix a typo in our Context helpers that was reporting no sessionStore being set _only_ when a sessionStore was set. So yes, the opposite of what we wanted. Oops. This was discovered by passing all our tests through the context. methods instead of operating on the stores themselves.

History
paddy@52 1 package auth
paddy@52 2
paddy@52 3 import (
paddy@66 4 "bytes"
paddy@81 5 "encoding/json"
paddy@52 6 "html/template"
paddy@66 7 "io/ioutil"
paddy@52 8 "net/http"
paddy@52 9 "net/http/httptest"
paddy@53 10 "net/url"
paddy@52 11 "testing"
paddy@56 12 "time"
paddy@56 13
paddy@107 14 "code.secondbit.org/uuid.hg"
paddy@52 15 )
paddy@52 16
paddy@53 17 const (
paddy@53 18 scopeSet = 1 << iota
paddy@53 19 stateSet
paddy@53 20 uriSet
paddy@53 21 )
paddy@53 22
paddy@65 23 func stripParam(param string, u *url.URL) {
paddy@65 24 q := u.Query()
paddy@65 25 q.Del(param)
paddy@65 26 u.RawQuery = q.Encode()
paddy@65 27 }
paddy@65 28
paddy@87 29 func TestGetAuthorizationCodeCodeSuccess(t *testing.T) {
paddy@52 30 t.Parallel()
paddy@52 31 store := NewMemstore()
paddy@52 32 testContext := Context{
paddy@116 33 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ if not .error }}Get auth grant{{ else }}{{ .error }}{{ end }}")),
paddy@87 34 clients: store,
paddy@87 35 authCodes: store,
paddy@87 36 profiles: store,
paddy@87 37 tokens: store,
paddy@87 38 sessions: store,
paddy@52 39 }
paddy@56 40 client := Client{
paddy@56 41 ID: uuid.NewID(),
paddy@56 42 Secret: "super secret!",
paddy@56 43 OwnerID: uuid.NewID(),
paddy@56 44 Name: "My test client",
paddy@56 45 Logo: "https://secondbit.org/logo.png",
paddy@56 46 Website: "https://secondbit.org",
paddy@56 47 Type: "public",
paddy@56 48 }
paddy@56 49 endpoint := Endpoint{
paddy@56 50 ID: uuid.NewID(),
paddy@56 51 ClientID: client.ID,
paddy@116 52 URI: "https://test.secondbit.org/redirect",
paddy@56 53 Added: time.Now(),
paddy@56 54 }
paddy@116 55 err := testContext.SaveClient(client)
paddy@56 56 if err != nil {
paddy@56 57 t.Fatal("Can't store client:", err)
paddy@56 58 }
paddy@115 59 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
paddy@56 60 if err != nil {
paddy@56 61 t.Fatal("Can't store endpoint:", err)
paddy@56 62 }
paddy@85 63 profile := Profile{
paddy@85 64 ID: uuid.NewID(),
paddy@85 65 }
paddy@85 66 err = testContext.SaveProfile(profile)
paddy@85 67 if err != nil {
paddy@85 68 t.Fatal("Can't store profile:", err)
paddy@85 69 }
paddy@77 70 session := Session{
paddy@85 71 ID: "testsession",
paddy@85 72 Active: true,
paddy@85 73 ProfileID: profile.ID,
paddy@77 74 }
paddy@77 75 err = testContext.CreateSession(session)
paddy@77 76 if err != nil {
paddy@77 77 t.Fatal("Can't store session:", err)
paddy@77 78 }
paddy@52 79 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@52 80 if err != nil {
paddy@52 81 t.Fatal("Can't build request:", err)
paddy@52 82 }
paddy@77 83 cookie := &http.Cookie{
paddy@77 84 Name: authCookieName,
paddy@77 85 Value: session.ID,
paddy@77 86 }
paddy@77 87 req.AddCookie(cookie)
paddy@58 88 for i := 0; i < 1<<3; i++ {
paddy@53 89 w := httptest.NewRecorder()
paddy@53 90 params := url.Values{}
paddy@53 91 // see OAuth 2.0 spec, section 4.1.1
paddy@53 92 params.Set("response_type", "code")
paddy@56 93 params.Set("client_id", client.ID.String())
paddy@53 94 if i&uriSet != 0 {
paddy@116 95 params.Set("redirect_uri", endpoint.URI)
paddy@53 96 }
paddy@53 97 if i&scopeSet != 0 {
paddy@53 98 params.Set("scope", "testscope")
paddy@53 99 }
paddy@53 100 if i&stateSet != 0 {
paddy@53 101 params.Set("state", "my super secure state string")
paddy@53 102 }
paddy@53 103 req.URL.RawQuery = params.Encode()
paddy@66 104 req.Method = "GET"
paddy@66 105 req.Body = nil
paddy@66 106 req.Header.Del("Content-Type")
paddy@87 107 GetAuthorizationCodeHandler(w, req, testContext)
paddy@53 108 if w.Code != http.StatusOK {
paddy@116 109 t.Log("Response body:", w.Body.String())
paddy@53 110 t.Errorf("Expected status code to be %d, got %d for %s", http.StatusOK, w.Code, req.URL.String())
paddy@53 111 }
paddy@53 112 if w.Body.String() != "Get auth grant" {
paddy@53 113 t.Errorf("Expected body to be `%s`, got `%s` for %s", "Get auth grant", w.Body.String(), req.URL.String())
paddy@53 114 }
paddy@66 115 req.Method = "POST"
paddy@66 116 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@66 117 w = httptest.NewRecorder()
paddy@66 118 data := url.Values{}
paddy@66 119 data.Set("grant", "approved")
paddy@66 120 body := bytes.NewBufferString(data.Encode())
paddy@66 121 req.Body = ioutil.NopCloser(body)
paddy@87 122 GetAuthorizationCodeHandler(w, req, testContext)
paddy@66 123 if w.Code != http.StatusFound {
paddy@66 124 t.Errorf("Expected status code to be %d, got %d for %s", http.StatusFound, w.Code, req.URL.String())
paddy@66 125 }
paddy@66 126 redirectedTo := w.Header().Get("Location")
paddy@66 127 red, err := url.Parse(redirectedTo)
paddy@66 128 if err != nil {
paddy@66 129 t.Fatalf(`Being redirected to a non-URL "%s" threw error "%s" for "%s"\n`, redirectedTo, err, req.URL.String())
paddy@66 130 }
paddy@66 131 t.Log("Redirected to", redirectedTo)
paddy@66 132 if red.Query().Get("code") == "" {
paddy@66 133 t.Fatalf(`Expected code param in redirect URL to be set, but it wasn't for %s`, req.URL.String())
paddy@66 134 }
paddy@87 135 if _, err := testContext.GetAuthorizationCode(red.Query().Get("code")); err != nil {
paddy@67 136 t.Fatalf(`Unexpected error "%s: retrieving the grant "%s" supplied in the redirect URL for %s`, err, red.Query().Get("code"), req.URL.String())
paddy@66 137 }
paddy@87 138 err = testContext.DeleteAuthorizationCode(red.Query().Get("code"))
paddy@66 139 if err != nil {
paddy@82 140 t.Logf(`Unexpected error "%s" deleting grant "%s" for %s`, err, red.Query().Get("code"), req.URL.String())
paddy@66 141 }
paddy@66 142 stripParam("code", red)
paddy@66 143 if red.Query().Get("state") != params.Get("state") {
paddy@66 144 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())
paddy@66 145 }
paddy@66 146 stripParam("state", red)
paddy@116 147 if red.String() != endpoint.URI {
paddy@116 148 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
paddy@66 149 }
paddy@52 150 }
paddy@52 151 }
paddy@56 152
paddy@87 153 func TestGetAuthorizationCodeCodeInvalidClient(t *testing.T) {
paddy@62 154 t.Parallel()
paddy@62 155 store := NewMemstore()
paddy@62 156 testContext := Context{
paddy@87 157 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
paddy@87 158 clients: store,
paddy@87 159 authCodes: store,
paddy@87 160 profiles: store,
paddy@87 161 tokens: store,
paddy@87 162 sessions: store,
paddy@62 163 }
paddy@62 164 client := Client{
paddy@62 165 ID: uuid.NewID(),
paddy@62 166 Secret: "super secret!",
paddy@62 167 OwnerID: uuid.NewID(),
paddy@62 168 Name: "My test client",
paddy@62 169 Type: "public",
paddy@62 170 }
paddy@62 171 err := testContext.SaveClient(client)
paddy@62 172 if err != nil {
paddy@62 173 t.Fatal("Can't store client:", err)
paddy@62 174 }
paddy@77 175 session := Session{
paddy@77 176 ID: "testsession",
paddy@77 177 Active: true,
paddy@77 178 }
paddy@77 179 err = testContext.CreateSession(session)
paddy@77 180 if err != nil {
paddy@77 181 t.Fatal("Can't store session:", err)
paddy@77 182 }
paddy@62 183 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@62 184 if err != nil {
paddy@62 185 t.Fatal("Can't build request:", err)
paddy@62 186 }
paddy@62 187 w := httptest.NewRecorder()
paddy@62 188 params := url.Values{}
paddy@62 189 params.Set("response_type", "code")
paddy@62 190 params.Set("redirect_uri", "https://test.secondbit.org/")
paddy@62 191 req.URL.RawQuery = params.Encode()
paddy@77 192 cookie := &http.Cookie{
paddy@77 193 Name: authCookieName,
paddy@77 194 Value: session.ID,
paddy@77 195 }
paddy@77 196 req.AddCookie(cookie)
paddy@87 197 GetAuthorizationCodeHandler(w, req, testContext)
paddy@62 198 if w.Code != http.StatusBadRequest {
paddy@62 199 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@62 200 }
paddy@62 201 if w.Body.String() != "Client ID must be specified in the request." {
paddy@62 202 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "Client ID must be specified in the request.", w.Body.String())
paddy@62 203 }
paddy@62 204 w = httptest.NewRecorder()
paddy@62 205 params.Set("client_id", "Not an ID")
paddy@62 206 req.URL.RawQuery = params.Encode()
paddy@87 207 GetAuthorizationCodeHandler(w, req, testContext)
paddy@62 208 if w.Code != http.StatusBadRequest {
paddy@62 209 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@62 210 }
paddy@62 211 if w.Body.String() != "client_id is not a valid Client ID." {
paddy@62 212 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "client_id is not a valid Client ID.", w.Body.String())
paddy@62 213 }
paddy@62 214 w = httptest.NewRecorder()
paddy@62 215 params.Set("client_id", uuid.NewID().String())
paddy@62 216 req.URL.RawQuery = params.Encode()
paddy@87 217 GetAuthorizationCodeHandler(w, req, testContext)
paddy@62 218 if w.Code != http.StatusBadRequest {
paddy@62 219 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@62 220 }
paddy@62 221 if w.Body.String() != "The specified Client couldn&rsquo;t be found." {
paddy@62 222 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The specified Client couldn&rsquo;t be found.", w.Body.String())
paddy@62 223 }
paddy@62 224 }
paddy@62 225
paddy@87 226 func TestGetAuthorizationCodeCodeInvalidURI(t *testing.T) {
paddy@56 227 t.Parallel()
paddy@56 228 store := NewMemstore()
paddy@56 229 testContext := Context{
paddy@87 230 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
paddy@87 231 clients: store,
paddy@87 232 authCodes: store,
paddy@87 233 profiles: store,
paddy@87 234 tokens: store,
paddy@87 235 sessions: store,
paddy@56 236 }
paddy@56 237 client := Client{
paddy@56 238 ID: uuid.NewID(),
paddy@56 239 Secret: "super secret!",
paddy@56 240 OwnerID: uuid.NewID(),
paddy@56 241 Name: "My test client",
paddy@56 242 Type: "public",
paddy@56 243 }
paddy@116 244 err := testContext.SaveClient(client)
paddy@56 245 if err != nil {
paddy@56 246 t.Fatal("Can't store client:", err)
paddy@56 247 }
paddy@77 248 session := Session{
paddy@77 249 ID: "testsession",
paddy@77 250 Active: true,
paddy@77 251 }
paddy@77 252 err = testContext.CreateSession(session)
paddy@77 253 if err != nil {
paddy@77 254 t.Fatal("Can't store session:", err)
paddy@77 255 }
paddy@56 256 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@56 257 if err != nil {
paddy@56 258 t.Fatal("Can't build request:", err)
paddy@56 259 }
paddy@77 260 cookie := &http.Cookie{
paddy@77 261 Name: authCookieName,
paddy@77 262 Value: session.ID,
paddy@77 263 }
paddy@77 264 req.AddCookie(cookie)
paddy@56 265 w := httptest.NewRecorder()
paddy@56 266 params := url.Values{}
paddy@56 267 params.Set("response_type", "code")
paddy@56 268 params.Set("client_id", client.ID.String())
paddy@64 269 req.URL.RawQuery = params.Encode()
paddy@87 270 GetAuthorizationCodeHandler(w, req, testContext)
paddy@64 271 if w.Code != http.StatusBadRequest {
paddy@64 272 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@64 273 }
paddy@64 274 if w.Body.String() != "The redirect_uri specified is not valid." {
paddy@64 275 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
paddy@64 276 }
paddy@64 277 endpoint := Endpoint{
paddy@64 278 ID: uuid.NewID(),
paddy@64 279 ClientID: client.ID,
paddy@116 280 URI: "https://test.secondbit.org/redirect",
paddy@64 281 Added: time.Now(),
paddy@64 282 }
paddy@115 283 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
paddy@64 284 if err != nil {
paddy@64 285 t.Fatal("Can't store endpoint:", err)
paddy@64 286 }
paddy@64 287 w = httptest.NewRecorder()
paddy@56 288 params.Set("redirect_uri", "https://test.secondbit.org/wrong")
paddy@56 289 req.URL.RawQuery = params.Encode()
paddy@87 290 GetAuthorizationCodeHandler(w, req, testContext)
paddy@56 291 if w.Code != http.StatusBadRequest {
paddy@56 292 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@56 293 }
paddy@56 294 if w.Body.String() != "The redirect_uri specified is not valid." {
paddy@56 295 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
paddy@56 296 }
paddy@64 297 endpoint2 := Endpoint{
paddy@64 298 ID: uuid.NewID(),
paddy@64 299 ClientID: client.ID,
paddy@116 300 URI: "https://test.secondbit.org/redirect",
paddy@64 301 Added: time.Now(),
paddy@64 302 }
paddy@115 303 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint2})
paddy@60 304 if err != nil {
paddy@64 305 t.Fatal("Can't store endpoint:", err)
paddy@60 306 }
paddy@60 307 w = httptest.NewRecorder()
paddy@64 308 params.Set("redirect_uri", "")
paddy@64 309 req.URL.RawQuery = params.Encode()
paddy@87 310 GetAuthorizationCodeHandler(w, req, testContext)
paddy@64 311 if w.Code != http.StatusBadRequest {
paddy@64 312 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@64 313 }
paddy@64 314 if w.Body.String() != "The redirect_uri specified is not valid." {
paddy@64 315 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
paddy@64 316 }
paddy@64 317 w = httptest.NewRecorder()
paddy@64 318 params.Set("redirect_uri", "://not a URL")
paddy@60 319 req.URL.RawQuery = params.Encode()
paddy@87 320 GetAuthorizationCodeHandler(w, req, testContext)
paddy@60 321 if w.Code != http.StatusBadRequest {
paddy@60 322 t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@60 323 }
paddy@60 324 if w.Body.String() != "The redirect_uri specified is not valid." {
paddy@60 325 t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
paddy@60 326 }
paddy@56 327 }
paddy@65 328
paddy@87 329 func TestGetAuthorizationCodeCodeInvalidResponseType(t *testing.T) {
paddy@65 330 t.Parallel()
paddy@65 331 store := NewMemstore()
paddy@65 332 testContext := Context{
paddy@87 333 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
paddy@87 334 clients: store,
paddy@87 335 authCodes: store,
paddy@87 336 profiles: store,
paddy@87 337 tokens: store,
paddy@87 338 sessions: store,
paddy@65 339 }
paddy@65 340 client := Client{
paddy@65 341 ID: uuid.NewID(),
paddy@65 342 Secret: "super secret!",
paddy@65 343 OwnerID: uuid.NewID(),
paddy@65 344 Name: "My test client",
paddy@65 345 Logo: "https://secondbit.org/logo.png",
paddy@65 346 Website: "https://secondbit.org",
paddy@65 347 Type: "public",
paddy@65 348 }
paddy@65 349 endpoint := Endpoint{
paddy@65 350 ID: uuid.NewID(),
paddy@65 351 ClientID: client.ID,
paddy@116 352 URI: "https://test.secondbit.org/redirect",
paddy@65 353 Added: time.Now(),
paddy@65 354 }
paddy@116 355 err := testContext.SaveClient(client)
paddy@65 356 if err != nil {
paddy@65 357 t.Fatal("Can't store client:", err)
paddy@65 358 }
paddy@115 359 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
paddy@65 360 if err != nil {
paddy@65 361 t.Fatal("Can't store endpoint:", err)
paddy@65 362 }
paddy@77 363 session := Session{
paddy@77 364 ID: "testsession",
paddy@77 365 Active: true,
paddy@77 366 }
paddy@77 367 err = testContext.CreateSession(session)
paddy@77 368 if err != nil {
paddy@77 369 t.Fatal("Can't store session:", err)
paddy@77 370 }
paddy@65 371 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@65 372 if err != nil {
paddy@65 373 t.Fatal("Can't build request:", err)
paddy@65 374 }
paddy@77 375 cookie := &http.Cookie{
paddy@77 376 Name: authCookieName,
paddy@77 377 Value: session.ID,
paddy@77 378 }
paddy@77 379 req.AddCookie(cookie)
paddy@65 380 params := url.Values{}
paddy@65 381 params.Set("response_type", "totally not code")
paddy@65 382 params.Set("client_id", client.ID.String())
paddy@116 383 params.Set("redirect_uri", endpoint.URI)
paddy@65 384 params.Set("scope", "testscope")
paddy@65 385 params.Set("state", "my super secure state string")
paddy@65 386 req.URL.RawQuery = params.Encode()
paddy@65 387 w := httptest.NewRecorder()
paddy@87 388 GetAuthorizationCodeHandler(w, req, testContext)
paddy@65 389 if w.Code != http.StatusFound {
paddy@65 390 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
paddy@65 391 }
paddy@65 392 redirectedTo := w.Header().Get("Location")
paddy@65 393 red, err := url.Parse(redirectedTo)
paddy@65 394 if err != nil {
paddy@65 395 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
paddy@65 396 }
paddy@65 397 if red.Query().Get("error") != "invalid_request" {
paddy@65 398 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
paddy@65 399 }
paddy@65 400 stripParam("error", red)
paddy@65 401 if red.Query().Get("state") != params.Get("state") {
paddy@65 402 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
paddy@65 403 }
paddy@65 404 stripParam("state", red)
paddy@116 405 if red.String() != endpoint.URI {
paddy@116 406 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
paddy@65 407 }
paddy@65 408 stripParam("response_type", req.URL)
paddy@65 409 w = httptest.NewRecorder()
paddy@87 410 GetAuthorizationCodeHandler(w, req, testContext)
paddy@65 411 if w.Code != http.StatusFound {
paddy@65 412 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
paddy@65 413 }
paddy@65 414 redirectedTo = w.Header().Get("Location")
paddy@65 415 red, err = url.Parse(redirectedTo)
paddy@65 416 if err != nil {
paddy@65 417 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
paddy@65 418 }
paddy@65 419 if red.Query().Get("error") != "invalid_request" {
paddy@65 420 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
paddy@65 421 }
paddy@65 422 stripParam("error", red)
paddy@65 423 if red.Query().Get("state") != params.Get("state") {
paddy@65 424 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
paddy@65 425 }
paddy@65 426 stripParam("state", red)
paddy@116 427 if red.String() != endpoint.URI {
paddy@116 428 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
paddy@65 429 }
paddy@65 430 }
paddy@66 431
paddy@87 432 func TestGetAuthorizationCodeCodeDenied(t *testing.T) {
paddy@66 433 t.Parallel()
paddy@66 434 store := NewMemstore()
paddy@66 435 testContext := Context{
paddy@87 436 template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
paddy@87 437 clients: store,
paddy@87 438 authCodes: store,
paddy@87 439 profiles: store,
paddy@87 440 tokens: store,
paddy@87 441 sessions: store,
paddy@66 442 }
paddy@66 443 client := Client{
paddy@66 444 ID: uuid.NewID(),
paddy@66 445 Secret: "super secret!",
paddy@66 446 OwnerID: uuid.NewID(),
paddy@66 447 Name: "My test client",
paddy@66 448 Logo: "https://secondbit.org/logo.png",
paddy@66 449 Website: "https://secondbit.org",
paddy@66 450 Type: "public",
paddy@66 451 }
paddy@66 452 endpoint := Endpoint{
paddy@66 453 ID: uuid.NewID(),
paddy@66 454 ClientID: client.ID,
paddy@116 455 URI: "https://test.secondbit.org/redirect",
paddy@66 456 Added: time.Now(),
paddy@66 457 }
paddy@116 458 err := testContext.SaveClient(client)
paddy@66 459 if err != nil {
paddy@66 460 t.Fatal("Can't store client:", err)
paddy@66 461 }
paddy@115 462 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
paddy@66 463 if err != nil {
paddy@66 464 t.Fatal("Can't store endpoint:", err)
paddy@66 465 }
paddy@77 466 session := Session{
paddy@77 467 ID: "testsession",
paddy@77 468 Active: true,
paddy@77 469 }
paddy@77 470 err = testContext.CreateSession(session)
paddy@77 471 if err != nil {
paddy@77 472 t.Fatal("Can't store session:", err)
paddy@77 473 }
paddy@66 474 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@66 475 if err != nil {
paddy@66 476 t.Fatal("Can't build request:", err)
paddy@66 477 }
paddy@77 478 cookie := &http.Cookie{
paddy@77 479 Name: authCookieName,
paddy@77 480 Value: session.ID,
paddy@77 481 }
paddy@77 482 req.AddCookie(cookie)
paddy@66 483 params := url.Values{}
paddy@66 484 params.Set("response_type", "code")
paddy@66 485 params.Set("client_id", client.ID.String())
paddy@116 486 params.Set("redirect_uri", endpoint.URI)
paddy@66 487 params.Set("scope", "testscope")
paddy@66 488 params.Set("state", "my super secure state string")
paddy@66 489 data := url.Values{}
paddy@66 490 data.Set("grant", "denied")
paddy@66 491 req.URL.RawQuery = params.Encode()
paddy@66 492 req.Body = ioutil.NopCloser(bytes.NewBufferString(data.Encode()))
paddy@66 493 req.Method = "POST"
paddy@66 494 w := httptest.NewRecorder()
paddy@87 495 GetAuthorizationCodeHandler(w, req, testContext)
paddy@66 496 if w.Code != http.StatusFound {
paddy@66 497 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
paddy@66 498 }
paddy@66 499 redirectedTo := w.Header().Get("Location")
paddy@66 500 red, err := url.Parse(redirectedTo)
paddy@66 501 if err != nil {
paddy@66 502 t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
paddy@66 503 }
paddy@66 504 if red.Query().Get("error") != "access_denied" {
paddy@66 505 t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "access_denied", red.Query().Get("error"))
paddy@66 506 }
paddy@66 507 stripParam("error", red)
paddy@66 508 if red.Query().Get("state") != params.Get("state") {
paddy@66 509 t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
paddy@66 510 }
paddy@66 511 stripParam("state", red)
paddy@116 512 if red.String() != endpoint.URI {
paddy@116 513 t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
paddy@66 514 }
paddy@66 515 }
paddy@77 516
paddy@87 517 func TestGetAuthorizationCodeCodeLoginRedirect(t *testing.T) {
paddy@80 518 t.Parallel()
paddy@80 519 req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@80 520 if err != nil {
paddy@80 521 t.Fatal("Can't build request:", err)
paddy@80 522 }
paddy@80 523 w := httptest.NewRecorder()
paddy@87 524 GetAuthorizationCodeHandler(w, req, Context{template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .internal_error }}"))})
paddy@80 525 if w.Code != http.StatusInternalServerError {
paddy@80 526 t.Errorf("Expected status code to be %d, got %d", http.StatusInternalServerError, w.Code)
paddy@80 527 }
paddy@80 528 expectation := "Missing login URL."
paddy@80 529 if w.Body.String() != expectation {
paddy@80 530 t.Errorf(`Expected body to be "%s", got "%s"`, expectation, w.Body.String())
paddy@80 531 }
paddy@80 532 uri, err := url.Parse("https://client.secondbit.org/")
paddy@80 533 if err != nil {
paddy@82 534 t.Error("Unexpected error", err)
paddy@80 535 }
paddy@80 536 testContext := Context{
paddy@80 537 loginURI: uri,
paddy@80 538 }
paddy@80 539 w = httptest.NewRecorder()
paddy@87 540 GetAuthorizationCodeHandler(w, req, testContext)
paddy@80 541 if w.Code != http.StatusFound {
paddy@80 542 t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
paddy@80 543 }
paddy@80 544 redirectedTo := w.Header().Get("Location")
paddy@80 545 expectation = "https://client.secondbit.org/?from=https%3A%2F%2Ftest.auth.secondbit.org%2Foauth2%2Fgrant"
paddy@80 546 if redirectedTo != expectation {
paddy@80 547 t.Errorf("Expected to be redirected to '%s', was redirected to '%s'", expectation, redirectedTo)
paddy@80 548 }
paddy@80 549 }
paddy@80 550
paddy@78 551 func TestCheckCookie(t *testing.T) {
paddy@78 552 t.Parallel()
paddy@78 553 req, err := http.NewRequest("GET", "https://auth.secondbit.org", nil)
paddy@78 554 if err != nil {
paddy@78 555 t.Error("Unexpected error creating base request:", err)
paddy@78 556 }
paddy@78 557 store := NewMemstore()
paddy@78 558 testContext := Context{
paddy@78 559 sessions: store,
paddy@78 560 }
paddy@78 561 session, err := checkCookie(req, testContext)
paddy@78 562 if err != ErrNoSession {
paddy@78 563 t.Errorf("Expected ErrNoSession, got %s", err)
paddy@78 564 }
paddy@78 565 session = Session{
paddy@78 566 ID: "testsession",
paddy@78 567 Active: true,
paddy@78 568 }
paddy@78 569 err = testContext.CreateSession(session)
paddy@78 570 if err != nil {
paddy@78 571 t.Error("Unexpected error persisting session:", err)
paddy@78 572 }
paddy@78 573 invalidSession := Session{
paddy@78 574 ID: "testsession2",
paddy@78 575 Active: false,
paddy@78 576 }
paddy@78 577 err = testContext.CreateSession(invalidSession)
paddy@78 578 if err != nil {
paddy@78 579 t.Error("Unexpected error persisting session:", err)
paddy@78 580 }
paddy@78 581 result, err := checkCookie(req, testContext)
paddy@78 582 if err != ErrNoSession {
paddy@78 583 t.Errorf("Expected ErrNoSession, got %s", err)
paddy@78 584 }
paddy@78 585 req.AddCookie(&http.Cookie{
paddy@78 586 Name: "wrongcookie",
paddy@78 587 Value: "wrong value",
paddy@78 588 })
paddy@78 589 result, err = checkCookie(req, testContext)
paddy@78 590 if err != ErrNoSession {
paddy@78 591 t.Error("Expected ErrNoSession, got", err)
paddy@78 592 }
paddy@78 593 req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
paddy@78 594 if err != nil {
paddy@78 595 t.Error("Unexpected error creating base request:", err)
paddy@78 596 }
paddy@78 597 req.AddCookie(&http.Cookie{
paddy@78 598 Name: "Stillwrongcookie",
paddy@78 599 Value: session.ID,
paddy@78 600 })
paddy@78 601 result, err = checkCookie(req, testContext)
paddy@78 602 if err != ErrNoSession {
paddy@78 603 t.Error("Expected ErrNoSession, got", err)
paddy@78 604 }
paddy@78 605 req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
paddy@78 606 if err != nil {
paddy@78 607 t.Error("Unexpected error creating base request:", err)
paddy@78 608 }
paddy@78 609 req.AddCookie(&http.Cookie{
paddy@78 610 Name: authCookieName,
paddy@78 611 Value: "wrong value",
paddy@78 612 })
paddy@78 613 result, err = checkCookie(req, testContext)
paddy@78 614 if err != ErrInvalidSession {
paddy@78 615 t.Error("Expected ErrInvalidSession, got", err)
paddy@78 616 }
paddy@78 617 req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
paddy@78 618 if err != nil {
paddy@78 619 t.Error("Unexpected error creating base request:", err)
paddy@78 620 }
paddy@78 621 req.AddCookie(&http.Cookie{
paddy@78 622 Name: authCookieName,
paddy@78 623 Value: invalidSession.ID,
paddy@78 624 })
paddy@78 625 result, err = checkCookie(req, testContext)
paddy@78 626 if err != ErrInvalidSession {
paddy@78 627 t.Error("Expected ErrInvalidSession, got", err)
paddy@78 628 }
paddy@78 629 req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
paddy@78 630 if err != nil {
paddy@78 631 t.Error("Unexpected error creating base request:", err)
paddy@78 632 }
paddy@78 633 req.AddCookie(&http.Cookie{
paddy@78 634 Name: authCookieName,
paddy@78 635 Value: session.ID,
paddy@78 636 })
paddy@78 637 result, err = checkCookie(req, testContext)
paddy@78 638 if err != nil {
paddy@78 639 t.Error("Unexpected error:", err)
paddy@78 640 }
paddy@78 641 success, field, expectation, outcome := compareSessions(session, result)
paddy@78 642 if !success {
paddy@78 643 t.Errorf(`Expected field %s to be %v, but got %v`, field, expectation, outcome)
paddy@78 644 }
paddy@78 645 }
paddy@78 646
paddy@78 647 func TestBuildLoginRedirect(t *testing.T) {
paddy@78 648 t.Parallel()
paddy@78 649 req, err := http.NewRequest("GET", "https://client.secondbit.org/my/awesome/path?has=query&params=to&screw=this&all=up", nil)
paddy@78 650 if err != nil {
paddy@78 651 t.Error("Unexpected error creating base request:", err)
paddy@78 652 }
paddy@78 653 result := buildLoginRedirect(req, Context{})
paddy@78 654 if result != "" {
paddy@78 655 t.Error("Expected empty string as the result, got", result)
paddy@78 656 }
paddy@78 657 uri, err := url.Parse("https://auth.secondbit.org/login?query=string&other=param")
paddy@78 658 if err != nil {
paddy@78 659 t.Error("Unexpected error parsing URL:", err)
paddy@78 660 }
paddy@78 661 c := Context{loginURI: uri}
paddy@78 662 result = buildLoginRedirect(req, c)
paddy@78 663 expectation := "https://auth.secondbit.org/login?from=https%3A%2F%2Fclient.secondbit.org%2Fmy%2Fawesome%2Fpath%3Fhas%3Dquery%26params%3Dto%26screw%3Dthis%26all%3Dup&other=param&query=string"
paddy@78 664 if result != expectation {
paddy@78 665 t.Errorf(`Expected result string to be "%s", was "%s"`, expectation, result)
paddy@78 666 }
paddy@78 667 }
paddy@79 668
paddy@79 669 func TestAuthenticateHelper(t *testing.T) {
paddy@79 670 t.Parallel()
paddy@79 671 store := NewMemstore()
paddy@79 672 context := Context{
paddy@79 673 profiles: store,
paddy@79 674 }
paddy@79 675 profile := Profile{
paddy@79 676 ID: uuid.NewID(),
paddy@79 677 Name: "Test User",
paddy@116 678 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
paddy@116 679 Iterations: 1,
paddy@116 680 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
paddy@79 681 PassphraseScheme: 1,
paddy@79 682 Compromised: false,
paddy@79 683 LockedUntil: time.Time{},
paddy@79 684 PassphraseReset: "",
paddy@79 685 PassphraseResetCreated: time.Time{},
paddy@79 686 Created: time.Now(),
paddy@79 687 LastSeen: time.Time{},
paddy@79 688 }
paddy@79 689 login := Login{
paddy@79 690 Type: "email",
paddy@79 691 Value: "test@example.com",
paddy@79 692 ProfileID: profile.ID,
paddy@79 693 Created: time.Now(),
paddy@79 694 LastUsed: time.Time{},
paddy@79 695 }
paddy@79 696 err := context.SaveProfile(profile)
paddy@79 697 if err != nil {
paddy@79 698 t.Error("Error saving profile:", err)
paddy@79 699 }
paddy@79 700 err = context.AddLogin(login)
paddy@79 701 if err != nil {
paddy@79 702 t.Error("Error adding login:", err)
paddy@79 703 }
paddy@103 704 response, err := authenticate("test@example.com", "mysecurepassphrase", context)
paddy@79 705 if err != nil {
paddy@79 706 t.Error("Unexpected error:", err)
paddy@79 707 }
paddy@79 708 success, field, expectation, result := compareProfiles(profile, response)
paddy@79 709 if !success {
paddy@79 710 t.Errorf(`Expected field %s to be "%v", got "%v"`, field, expectation, result)
paddy@79 711 }
paddy@103 712 response, err = authenticate("test2@example.com", "mysecurepassphrase", context)
paddy@79 713 if err != ErrIncorrectAuth {
paddy@79 714 t.Error("Expected ErrIncorrectAuth, got", err)
paddy@79 715 }
paddy@79 716 response, err = authenticate("test@example.com", "not the right password", context)
paddy@79 717 if err != ErrIncorrectAuth {
paddy@79 718 t.Error("Expected ErrIncorrectAuth, got", err)
paddy@79 719 }
paddy@79 720 scheme := 1000
paddy@79 721 phrase := "doesn't really matter, the scheme doesn't exist"
paddy@79 722 change := ProfileChange{
paddy@79 723 PassphraseScheme: &scheme,
paddy@79 724 Passphrase: &phrase,
paddy@79 725 }
paddy@79 726 err = context.UpdateProfile(profile.ID, change)
paddy@79 727 if err != nil {
paddy@79 728 t.Error("Unexpected error:", err)
paddy@79 729 }
paddy@79 730 response, err = authenticate("test@example.com", "not the right password", context)
paddy@79 731 if err != ErrInvalidPassphraseScheme {
paddy@79 732 t.Error("Expected ErrIncorrectAuth, got", err)
paddy@79 733 }
paddy@79 734 }
paddy@81 735
paddy@81 736 func TestGetTokenHandler(t *testing.T) {
paddy@81 737 t.Parallel()
paddy@81 738 store := NewMemstore()
paddy@81 739 context := Context{
paddy@87 740 clients: store,
paddy@87 741 authCodes: store,
paddy@87 742 tokens: store,
paddy@81 743 }
paddy@81 744 client := Client{
paddy@81 745 ID: uuid.NewID(),
paddy@81 746 Secret: "sometimes I feel like I don't know what I'm doing",
paddy@81 747 OwnerID: uuid.NewID(),
paddy@81 748 Name: "A Super Awesome Client!",
paddy@81 749 Logo: "https://logos.secondbit.org/client.png",
paddy@81 750 Website: "https://client.secondbit.org/",
paddy@81 751 Type: "confidential",
paddy@81 752 }
paddy@87 753 authCode := AuthorizationCode{
paddy@81 754 Code: "testcode",
paddy@81 755 Created: time.Now(),
paddy@81 756 ExpiresIn: 600,
paddy@81 757 ClientID: client.ID,
paddy@81 758 Scope: "testscope",
paddy@81 759 RedirectURI: "https://client.secondbit.org/",
paddy@81 760 State: "teststate",
paddy@81 761 ProfileID: uuid.NewID(),
paddy@81 762 }
paddy@87 763 err := context.SaveAuthorizationCode(authCode)
paddy@81 764 if err != nil {
paddy@87 765 t.Error("Error saving auth code:", err)
paddy@81 766 }
paddy@81 767 err = context.SaveClient(client)
paddy@81 768 if err != nil {
paddy@81 769 t.Error("Error saving client:", err)
paddy@81 770 }
paddy@81 771 data := url.Values{}
paddy@81 772 data.Set("grant_type", "authorization_code")
paddy@87 773 data.Set("code", authCode.Code)
paddy@87 774 data.Set("redirect_uri", authCode.RedirectURI)
paddy@81 775 body := bytes.NewBufferString(data.Encode())
paddy@81 776 req, err := http.NewRequest("POST", "https://auth.secondbit.org/", ioutil.NopCloser(body))
paddy@81 777 if err != nil {
paddy@81 778 t.Error("Error constructing request:", err)
paddy@81 779 }
paddy@81 780 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@81 781 req.SetBasicAuth(client.ID.String(), client.Secret)
paddy@81 782 w := httptest.NewRecorder()
paddy@81 783 GetTokenHandler(w, req, context)
paddy@81 784 resp := tokenResponse{}
paddy@81 785 err = json.Unmarshal(w.Body.Bytes(), &resp)
paddy@81 786 if err != nil {
paddy@81 787 t.Error("Error unmarshalling response:", err)
paddy@85 788 t.Error("Response:", w.Body.String())
paddy@81 789 }
paddy@81 790 if resp.AccessToken == "" {
paddy@81 791 t.Error("Got blank access token back")
paddy@81 792 }
paddy@81 793 if resp.RefreshToken == "" {
paddy@81 794 t.Error("Got blank refresh token back")
paddy@81 795 }
paddy@81 796 if resp.TokenType == "" {
paddy@81 797 t.Error("Got blank token type back")
paddy@81 798 }
paddy@81 799 if resp.ExpiresIn == 0 {
paddy@81 800 t.Error("Got blank expires in back")
paddy@81 801 }
paddy@87 802 tokens, err := context.GetTokensByProfileID(authCode.ProfileID, 1, 0)
paddy@81 803 if err != nil {
paddy@81 804 t.Error("Error retrieving token:", err)
paddy@81 805 }
paddy@81 806 if len(tokens) != 1 {
paddy@85 807 t.Fatalf("Expected %d tokens, got %d", 1, len(tokens))
paddy@81 808 }
paddy@81 809 if tokens[0].AccessToken != resp.AccessToken {
paddy@81 810 t.Errorf(`Expected access token to be "%s", got "%s"`, tokens[0].AccessToken, resp.AccessToken)
paddy@81 811 }
paddy@81 812 if tokens[0].RefreshToken != resp.RefreshToken {
paddy@81 813 t.Errorf(`Expected refresh token to be "%s", got "%s"`, tokens[0].RefreshToken, resp.RefreshToken)
paddy@81 814 }
paddy@81 815 if tokens[0].ExpiresIn != resp.ExpiresIn {
paddy@81 816 t.Errorf(`Expected expires in to be %d, got %d`, tokens[0].ExpiresIn, resp.ExpiresIn)
paddy@81 817 }
paddy@81 818 if tokens[0].TokenType != resp.TokenType {
paddy@81 819 t.Errorf(`Expected token type to be %s, got %s`, tokens[0].TokenType, resp.TokenType)
paddy@81 820 }
paddy@81 821 }