1 module vibeauth.router.oauth; 2 3 import vibe.http.router; 4 import vibe.data.json; 5 import vibe.inet.url; 6 7 import std.algorithm, std.base64, std..string, std.stdio, std.conv, std.array; 8 import std.datetime; 9 10 import vibeauth.users; 11 import vibeauth.router.baseAuthRouter; 12 import vibeauth.client; 13 import vibeauth.collection; 14 15 import vibeauth.router.accesscontrol; 16 17 /// OAuth2 comfiguration 18 struct OAuth2Configuration { 19 /// Route for generating tokens 20 string tokenPath = "/auth/token"; 21 22 /// Route for authorization 23 string authorizePath = "/auth/authorize"; 24 25 /// Route for authentication 26 string authenticatePath = "/auth/authenticate"; 27 28 /// Route for revoking tokens 29 string revokePath = "/auth/revoke"; 30 31 /// Custom style to be embeded into the html 32 string style; 33 } 34 35 /// Struct used for user authentication 36 struct AuthData { 37 /// 38 string username; 39 /// 40 string password; 41 /// 42 string refreshToken; 43 /// The authorization scopes 44 string[] scopes; 45 } 46 47 /// 48 interface IGrantAccess { 49 /// setter for the authentication data 50 void authData(AuthData authData); 51 52 /// setter for the user collection 53 void userCollection(UserCollection userCollection); 54 55 /// validate the auth data 56 bool isValid(); 57 58 /// get a Json response 59 Json get(); 60 } 61 62 /// Handle errors during token generation 63 final class UnknownGrantAccess : IGrantAccess { 64 /// Ignores the auth data 65 void authData(AuthData) {} 66 67 /// Ignore the user collection 68 void userCollection(UserCollection) {}; 69 70 /// All the requests are invalid 71 bool isValid() { 72 return false; 73 } 74 75 /// Get an error Json response 76 Json get() { 77 auto response = Json.emptyObject; 78 response["error"] = "Invalid `grant_type` value"; 79 80 return response; 81 } 82 } 83 84 /// Grant user access based on username and password strings 85 final class PasswordGrantAccess : IGrantAccess { 86 private { 87 AuthData data; 88 UserCollection collection; 89 } 90 91 /// setter for the authentication data 92 void authData(AuthData authData) { 93 this.data = authData; 94 } 95 96 /// setter for the user collection 97 void userCollection(UserCollection userCollection) { 98 this.collection = userCollection; 99 } 100 101 /// validate the authentication data 102 bool isValid() { 103 if(!collection.contains(data.username)) { 104 return false; 105 } 106 107 if(!collection[data.username].isValidPassword(data.password)) { 108 return false; 109 } 110 111 return true; 112 } 113 114 /// Get the token Json response object 115 Json get() { 116 auto response = Json.emptyObject; 117 118 if(!isValid) { 119 response["error"] = "Invalid password or username"; 120 return response; 121 } 122 123 auto accessToken = collection.createToken(data.username, Clock.currTime + 3601.seconds, data.scopes, "Bearer"); 124 auto refreshToken = collection.createToken(data.username, Clock.currTime + 30.weeks, data.scopes ~ [ "refresh" ], "Refresh"); 125 126 response["access_token"] = accessToken.name; 127 response["expires_in"] = (accessToken.expire - Clock.currTime).total!"seconds"; 128 response["token_type"] = accessToken.type; 129 response["refresh_token"] = refreshToken.name; 130 131 return response; 132 } 133 } 134 135 /// Grant user access based on a refresh token 136 final class RefreshTokenGrantAccess : IGrantAccess { 137 private { 138 AuthData data; 139 UserCollection collection; 140 User user; 141 } 142 143 /// setter for the authentication data 144 void authData(AuthData authData) { 145 this.data = authData; 146 cacheData; 147 } 148 149 /// setter for the user collection 150 void userCollection(UserCollection userCollection) { 151 this.collection = userCollection; 152 cacheData; 153 } 154 155 private void cacheData() { 156 if(collection is null || data.refreshToken == "") { 157 return; 158 } 159 160 user = collection.byToken(data.refreshToken); 161 data.scopes = user.getScopes(data.refreshToken).filter!(a => a != "refresh").array; 162 } 163 164 /// Validate the refresh token 165 bool isValid() { 166 if(data.refreshToken == "") { 167 return false; 168 } 169 170 return user.isValidToken(data.refreshToken, "refresh"); 171 } 172 173 /// Get the token Json response object 174 Json get() { 175 auto response = Json.emptyObject; 176 177 if(!isValid) { 178 response["error"] = "Invalid `refresh_token`"; 179 return response; 180 } 181 182 auto username = user.email(); 183 184 auto accessToken = collection.createToken(username, Clock.currTime + 3601.seconds, data.scopes, "Bearer"); 185 186 response["access_token"] = accessToken.name; 187 response["expires_in"] = (accessToken.expire - Clock.currTime).total!"seconds"; 188 response["token_type"] = accessToken.type; 189 190 return response; 191 } 192 } 193 194 /// Get the right access generator 195 IGrantAccess getAuthData(HTTPServerRequest req) { 196 AuthData data; 197 198 if("refresh_token" in req.form) { 199 data.refreshToken = req.form["refresh_token"]; 200 } 201 202 if("username" in req.form) { 203 data.username = req.form["username"]; 204 } 205 206 if("password" in req.form) { 207 data.password = req.form["password"]; 208 } 209 210 if("scope" in req.form) { 211 data.scopes = req.form["scope"].split(" "); 212 } 213 214 if("grant_type" in req.form) { 215 if(req.form["grant_type"] == "password") { 216 auto grant = new PasswordGrantAccess; 217 grant.authData = data; 218 219 return grant; 220 } 221 222 if(req.form["grant_type"] == "refresh_token") { 223 auto grant = new RefreshTokenGrantAccess; 224 grant.authData = data; 225 226 return grant; 227 } 228 } 229 230 return new UnknownGrantAccess; 231 } 232 233 /// OAuth2 autenticator 234 class OAuth2: BaseAuthRouter { 235 protected { 236 const OAuth2Configuration configuration; 237 ClientCollection clientCollection; 238 } 239 240 /// 241 this(UserCollection userCollection, ClientCollection clientCollection, const OAuth2Configuration configuration = OAuth2Configuration()) { 242 super(userCollection); 243 244 this.configuration = configuration; 245 this.clientCollection = clientCollection; 246 } 247 248 249 /// Handle the OAuth requests. Handles token creation, authorization 250 /// authentication and revocation 251 void tokenHandlers(HTTPServerRequest req, HTTPServerResponse res) { 252 253 try { 254 setAccessControl(res); 255 if(req.method == HTTPMethod.OPTIONS) { 256 return; 257 } 258 259 if(req.path == configuration.tokenPath) { 260 createToken(req, res); 261 } 262 263 if (req.path == configuration.authorizePath) { 264 authorize(req, res); 265 } 266 267 if(req.path == configuration.authenticatePath) { 268 authenticate(req, res); 269 } 270 271 if(req.path == configuration.revokePath) { 272 revoke(req, res); 273 } 274 } catch(Exception e) { 275 version(unittest) {} else debug stderr.writeln(e); 276 277 if(!res.headerWritten) { 278 res.writeJsonBody([ "error": e.msg ], 500); 279 } 280 } 281 } 282 283 override { 284 /// Auth handler that will fail if a successfull auth was not performed. 285 /// This handler is usefull for routes that want to hide information to the 286 /// public. 287 void mandatoryAuth(HTTPServerRequest req, HTTPServerResponse res) { 288 cleanRequest(req); 289 290 try { 291 setAccessControl(res); 292 if(req.method == HTTPMethod.OPTIONS) { 293 return; 294 } 295 296 if(!res.headerWritten && req.path != configuration.style && !isValidBearer(req)) { 297 respondUnauthorized(res); 298 } 299 } catch(Exception e) { 300 version(unittest) {} else debug stderr.writeln(e); 301 302 if(!res.headerWritten) { 303 res.writeJsonBody([ "error": e.msg ], 400); 304 } 305 } 306 } 307 308 /// Auth handler that fails only if the auth fields are present and are not valid. 309 /// This handler is usefull when a route should return different data when the user is 310 /// logged in 311 void permisiveAuth(HTTPServerRequest req, HTTPServerResponse res) { 312 cleanRequest(req); 313 314 if("Authorization" !in req.headers) { 315 return; 316 } 317 318 mandatoryAuth(req, res); 319 } 320 } 321 322 private { 323 /// Remove all dangerous fields from the request 324 void cleanRequest(HTTPServerRequest req) { 325 req.username = ""; 326 req.password = ""; 327 if("email" in req.context) { 328 req.context.remove("email"); 329 } 330 } 331 332 /// Validate the authorization token 333 bool isValidBearer(HTTPServerRequest req) { 334 auto pauth = "Authorization" in req.headers; 335 336 if(pauth && (*pauth).startsWith("Bearer ")) { 337 auto token = (*pauth)[7 .. $]; 338 339 try { 340 auto const user = collection.byToken(token); 341 req.username = user.id; 342 req.context["email"] = user.email; 343 344 } catch(UserNotFoundException exception) { 345 return false; 346 } 347 348 return true; 349 } 350 351 return false; 352 } 353 354 /// Handle the authorization step 355 void authorize(HTTPServerRequest req, HTTPServerResponse res) { 356 if("redirect_uri" !in req.query) { 357 showError(res, "Missing `redirect_uri` parameter"); 358 return; 359 } 360 361 if("client_id" !in req.query) { 362 showError(res, "Missing `client_id` parameter"); 363 return; 364 } 365 366 if("state" !in req.query) { 367 showError(res, "Missing `state` parameter"); 368 return; 369 } 370 371 auto const redirectUri = req.query["redirect_uri"]; 372 auto const clientId = req.query["client_id"]; 373 auto const state = req.query["state"]; 374 auto const style = configuration.style; 375 376 if(clientId !in clientCollection) { 377 showError(res, "Invalid `client_id` parameter"); 378 return; 379 } 380 381 string appTitle = clientCollection[clientId].name; 382 383 res.render!("loginForm.dt", appTitle, redirectUri, state, style); 384 } 385 386 387 /// Show an HTML error 388 void showError(HTTPServerResponse res, const string error) { 389 auto const style = configuration.style; 390 res.statusCode = 400; 391 res.render!("error.dt", error, style); 392 } 393 394 void authenticate(HTTPServerRequest req, HTTPServerResponse res) { 395 string email; 396 string password; 397 398 try { 399 email = req.form["email"]; 400 password = req.form["password"]; 401 } catch (Exception e) { 402 debug showError(res, e.to!string); 403 return; 404 } 405 406 if(!collection.contains(email) || !collection[email].isValidPassword(password)) { 407 showError(res, "Invalid email or password."); 408 return; 409 } 410 411 auto token = collection[email].createToken(Clock.currTime + 3601.seconds); 412 auto redirectUri = req.form["redirect_uri"] ~ "#access_token=" ~ token.name ~ "&state=" ~ req.form["state"]; 413 414 res.render!("redirect.dt", redirectUri); 415 } 416 417 /// Create token for the requested user 418 void createToken(HTTPServerRequest req, HTTPServerResponse res) { 419 auto grant = req.getAuthData; 420 421 grant.userCollection = collection; 422 res.statusCode = grant.isValid ? 200 : 401; 423 res.writeJsonBody(grant.get); 424 } 425 426 /// Revoke a previously created token using a POST request 427 void revoke(HTTPServerRequest req, HTTPServerResponse res) { 428 if(req.method != HTTPMethod.POST) { 429 return; 430 } 431 432 if("token" !in req.form) { 433 res.statusCode = 400; 434 res.writeJsonBody([ "error": "You must provide a `token` parameter." ]); 435 436 return; 437 } 438 439 auto const token = req.form["token"]; 440 collection.revoke(token); 441 442 res.statusCode = 200; 443 res.writeBody(""); 444 } 445 446 447 /// Write the unauthorized message to the server response 448 void respondUnauthorized(HTTPServerResponse res, string message = "Authorization required") { 449 res.statusCode = HTTPStatus.unauthorized; 450 res.writeJsonBody([ "error": message ]); 451 } 452 } 453 } 454 455 version(unittest) { 456 import fluentasserts.vibe.request; 457 import fluentasserts.vibe.json; 458 import fluent.asserts; 459 import vibeauth.token; 460 461 UserMemmoryCollection collection; 462 User user; 463 Client client; 464 ClientCollection clientCollection; 465 OAuth2 auth; 466 Token refreshToken; 467 Token bearerToken; 468 469 auto testRouter(bool requireLogin = true) { 470 auto router = new URLRouter(); 471 472 collection = new UserMemmoryCollection(["doStuff"]); 473 user = new User("user@gmail.com", "password"); 474 user.name = "John Doe"; 475 user.username = "test"; 476 user.id = 1; 477 478 collection.add(user); 479 480 refreshToken = collection.createToken("user@gmail.com", Clock.currTime + 3600.seconds, ["doStuff", "refresh"], "Refresh"); 481 bearerToken = collection.createToken("user@gmail.com", Clock.currTime + 3600.seconds, ["doStuff"], "Bearer"); 482 483 auto client = new Client(); 484 client.id = "CLIENT_ID"; 485 486 clientCollection = new ClientCollection([ client ]); 487 488 auth = new OAuth2(collection, clientCollection); 489 490 router.any("*", &auth.tokenHandlers); 491 492 if(requireLogin) { 493 router.any("*", &auth.mandatoryAuth); 494 } else { 495 router.any("*", &auth.permisiveAuth); 496 } 497 498 499 void handleRequest(HTTPServerRequest req, HTTPServerResponse res) { 500 res.statusCode = 200; 501 res.writeBody("Hello, World!"); 502 } 503 504 void showEmail(HTTPServerRequest req, HTTPServerResponse res) { 505 res.statusCode = 200; 506 res.writeBody(req.context["email"].get!string); 507 } 508 509 router.get("/sites", &handleRequest); 510 router.get("/email", &showEmail); 511 512 return router; 513 } 514 } 515 516 /// it should return 401 on missing auth 517 unittest { 518 testRouter.request.get("/sites").expectStatusCode(401).end(); 519 } 520 521 /// it should return 200 on valid credentials 522 unittest { 523 auto router = testRouter; 524 525 router 526 .request.get("/sites") 527 .header("Authorization", "Bearer " ~ bearerToken.name) 528 .expectStatusCode(200) 529 .end; 530 } 531 532 /// it should set the email on valid mandatory credentials 533 unittest { 534 auto router = testRouter; 535 536 router 537 .request.get("/email") 538 .header("Authorization", "Bearer " ~ bearerToken.name) 539 .expectStatusCode(200) 540 .end((Response response) => { 541 response.bodyString.should.equal("user@gmail.com"); 542 }); 543 } 544 545 /// it should return 200 on missing auth when it's not mandatory 546 unittest { 547 auto router = testRouter(false); 548 549 router 550 .request.get("/sites") 551 .expectStatusCode(200) 552 .end; 553 } 554 555 /// it should clear the username and email when auth it's not mandatory 556 unittest { 557 auto router = testRouter(false); 558 559 void setUser(HTTPServerRequest req, HTTPServerResponse res) { 560 req.username = "some user"; 561 req.password = "some password"; 562 req.context["email"] = "some random value"; 563 } 564 565 void showAuth(HTTPServerRequest req, HTTPServerResponse res) { 566 res.statusCode = 200; 567 string hasEmail = "email" in req.context ? "yes" : "no"; 568 res.writeBody(req.username ~ ":" ~ req.password ~ ":" ~ hasEmail); 569 } 570 571 router.any("*", &setUser); 572 router.any("*", &auth.permisiveAuth); 573 router.get("/misc", &showAuth); 574 575 router 576 .request.get("/misc") 577 .expectStatusCode(200) 578 .end((Response response) => { 579 response.bodyString.should.equal("::no"); 580 }); 581 } 582 583 /// it should return 200 on valid auth when it's not mandatory 584 unittest { 585 auto router = testRouter(false); 586 587 router 588 .request.get("/sites") 589 .header("Authorization", "Bearer " ~ bearerToken.name) 590 .expectStatusCode(200) 591 .end; 592 } 593 594 595 /// it should set the email on valid credentials when they are not mandatory 596 unittest { 597 auto router = testRouter(false); 598 599 router 600 .request.get("/email") 601 .header("Authorization", "Bearer " ~ bearerToken.name) 602 .expectStatusCode(200) 603 .end((Response response) => { 604 response.bodyString.should.equal("user@gmail.com"); 605 }); 606 } 607 608 /// it should return 401 on invalid auth when it's not mandatory 609 unittest { 610 auto router = testRouter(false); 611 612 router 613 .request.get("/sites") 614 .header("Authorization", "Bearer invalid") 615 .expectStatusCode(401) 616 .end; 617 } 618 619 /// it should return 401 on invalid credentials 620 unittest { 621 testRouter 622 .request.post("/auth/token") 623 .send(["grant_type": "password", "username": "invalid", "password": "invalid"]) 624 .expectStatusCode(401) 625 .end((Response response) => { 626 response.bodyJson.should.equal(`{ "error": "Invalid password or username" }`.parseJsonString); 627 }); 628 } 629 630 631 /// it should return tokens on valid email and password 632 unittest { 633 testRouter 634 .request 635 .post("/auth/token") 636 .send(["grant_type": "password", "username": "user@gmail.com", "password": "password"]) 637 .expectStatusCode(200) 638 .end((Response response) => { 639 response.bodyJson.keys.should.contain(["access_token", "expires_in", "refresh_token", "token_type"]); 640 641 user.isValidToken(response.bodyJson["access_token"].to!string).should.be.equal(true); 642 user.isValidToken(response.bodyJson["refresh_token"].to!string).should.be.equal(true); 643 644 response.bodyJson["token_type"].to!string.should.equal("Bearer"); 645 response.bodyJson["expires_in"].to!int.should.equal(3600); 646 }); 647 } 648 649 /// it should return tokens on valid username and password 650 unittest { 651 testRouter 652 .request 653 .post("/auth/token") 654 .send(["grant_type": "password", "username": "test", "password": "password"]) 655 .expectStatusCode(200) 656 .end((Response response) => { 657 response.bodyJson.keys.should.contain(["access_token", "expires_in", "refresh_token", "token_type"]); 658 659 user.isValidToken(response.bodyJson["access_token"].to!string).should.be.equal(true); 660 user.isValidToken(response.bodyJson["refresh_token"].to!string).should.be.equal(true); 661 662 response.bodyJson["token_type"].to!string.should.equal("Bearer"); 663 response.bodyJson["expires_in"].to!int.should.equal(3600); 664 }); 665 } 666 667 /// it should set the scope tokens on valid credentials 668 unittest { 669 testRouter 670 .request 671 .post("/auth/token") 672 .send(["grant_type": "password", "username": "user@gmail.com", "password": "password", "scope": "access1 access2"]) 673 .expectStatusCode(200) 674 .end((Response response) => { 675 user.isValidToken(response.bodyJson["refresh_token"].to!string, "refresh").should.equal(true); 676 user.isValidToken(response.bodyJson["refresh_token"].to!string, "other").should.equal(false); 677 678 user.isValidToken(response.bodyJson["access_token"].to!string, "access1").should.equal(true); 679 user.isValidToken(response.bodyJson["access_token"].to!string, "access2").should.equal(true); 680 user.isValidToken(response.bodyJson["access_token"].to!string, "other").should.equal(false); 681 }); 682 } 683 684 /// it should return a new access token on refresh token 685 unittest { 686 auto router = testRouter; 687 688 router 689 .request 690 .post("/auth/token") 691 .send(["grant_type": "refresh_token", "refresh_token": refreshToken.name ]) 692 .expectStatusCode(200) 693 .end((Response response) => { 694 response.bodyJson.keys.should.contain(["access_token", "expires_in", "token_type"]); 695 696 user.isValidToken(response.bodyJson["access_token"].to!string).should.be.equal(true); 697 user.isValidToken(response.bodyJson["access_token"].to!string, "doStuff").should.be.equal(true); 698 user.isValidToken(response.bodyJson["access_token"].to!string, "refresh").should.be.equal(false); 699 700 response.bodyJson["token_type"].to!string.should.equal("Bearer"); 701 response.bodyJson["expires_in"].to!int.should.equal(3600); 702 }); 703 } 704 705 /// it should be able to not block the requests without login 706 unittest { 707 auto router = testRouter(false); 708 709 router 710 .request 711 .get("/path") 712 .expectStatusCode(404) 713 .end(); 714 } 715 716 /// it should return 404 for GET on revocation path 717 unittest { 718 auto router = testRouter(false); 719 720 router 721 .request 722 .get("/auth/revoke") 723 .expectStatusCode(404) 724 .end(); 725 } 726 727 /// it should return 400 for POST on revocation path with missing token 728 unittest { 729 auto router = testRouter(false); 730 731 router 732 .request 733 .post("/auth/revoke") 734 .expectStatusCode(400) 735 .end((Response response) => { 736 response.bodyJson.should.equal("{ 737 \"error\": \"You must provide a `token` parameter.\" 738 }".parseJsonString); 739 }); 740 }