auth

Paddy 2015-05-17 Parent:581c60f8dd23 Child:b7e685839a1b

173:b0d1b3e39fc8 Go to Latest

auth/oauth2_test.go

Make client use our auth(n/z) scheme. Our auth(n/z) scheme can be loosely defined as "encrypted tokens that nginx transforms into headers" and "scopes for bypassing ACL". Our Go client, which is what we'll be using to have services communicate with each other, follows this paradigm now by auto-injecting the headers we'll need to identify ourselves. This will work behind our firewall, but will be useless for the rest of the world, which will need to go through the nginx bastion that can strip the headers and replace them with the headers appropriate to the token attached to the request. This did involve setting a static client ID as the client for our email_verification listener. Ideally, this would cause Client registration (using that ID) when the listener starts up, ignoring ErrClientAlreadyExists. I don't want to have to write the code that will allow us to bypass the Client ACL properly right now, though, so we're just going to have to remember to manually create that Client. Or not. I don't think it will do any harm (outside the OAuth flow) to be using a Client ID that doesn't actually point to a Client. I just think it'd be good for record-keeping purposes.

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