auth

Paddy 2015-12-14 Parent:581c60f8dd23

181:b7e685839a1b Go to Latest

auth/oauth2_test.go

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.

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