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