1Introduction to Wt::Auth 2======================== 3 4Koen Deforche <koen@emweb.be> + 5 6For Wt 4.0.0 (September 19, 2017) 7 8:doc: link:http://www.webtoolkit.eu/wt/doc/reference/html/ 9:tutorials: link:http://www.webtoolkit.eu/wt/doc/tutorial/ 10:cpp: pass:[C++] 11 12== Prerequisites 13 14In this tutorial, we use an example as a hands-on introduction to the Wt authentication 15module. This example is included in the Wt distribution, in 16https://github.com/emweb/wt/tree/master/examples/feature/auth1[examples/feature/auth1]. 17 18This introduction assumes that you have a reasonable understanding of 19Wt itself, in particular its stateful session model and the widget 20concept. If you haven't done so yet, you may want to go through the 21{tutorials}wt.html[Wt tutorial] first. 22 23== Introduction 24 25The authentication module implements the logic and widgets involved in 26getting users registered on your application, and letting them sign 27in. Note that this module is entirely optional, and simply implemented 28on top of http://www.webtoolkit.eu/wt[Wt]. 29 30The module implements 31http://en.wikipedia.org/wiki/Authentication[authentication]. Its main 32purpose is to securely authenticate a user to sign in to your 33application. From your application, you will interact with the 34authentication module using a 35{doc}classWt_1_1Auth_1_1Login.html[Wt::Auth::Login] object, which you 36typically hold in your application object. It indicates the user 37currently signed in (if any), and propagates authentication events. 38 39How you use this information for 40http://en.wikipedia.org/wiki/Authorization[authorization] or to 41customize the user experience is out of the scope of the 42module. Because of Wt's built-in security features, with strong 43session hijacking mitigation, this is as straight forward as one can 44conceive it. 45 46Currently, the module provides the following features, which can be 47separately enabled, configured or customized: 48 49* *Password authentication*, using 50http://stackoverflow.com/questions/549/the-definitive-guide-to-forms-based-website-authentication[best 51practices] including http://en.wikipedia.org/wiki/Salted_hash[salted 52hashing] with strong cryptographic hash functions (such as 53http://en.wikipedia.org/wiki/Bcrypt[bcrypt]) and password strength 54checking. 55 56* *Remember-me* functionality, again using best practices, by 57associating authentication tokens stored in cookies to a user. 58 59* *Verified email addresses* using the typical confirmation email 60process. 61 62* *Lost password functionality* that uses the verified email address 63to prompt a user to enter a new password. 64 65* Authentication using *3rd party Identity Providers*, currently using 66http://oauth.net/2/[OAuth 2], with support for multiple identities per 67user. Currently, only Google and Facebook are supported for authentication, 68but Wt may support other OAuth 2 providers, like the standardized OpenIDConnect, 69in the future. 70 71* *Registration* logic, which includes also the logic needed to merge 72new (federated login) identities into existing user profiles. For 73example, if a user previously registered using a user name and 74password, he may later also authenticate using for example his Google 75Account and this new identity is added to his existing account. 76 77The logic for these features is implemented separately from the 78user interface components, which can be customized or completely 79replaced with your own widgets. 80 81Next to traditional password-based authentication, we've been careful 82to have a design that accommodates 3rd party identity providers 83(http://en.wikipedia.org/wiki/Federated_identity[federated login]) 84such as Google, Twitter, Facebook, etc... and other authentication 85mechanisms in general (authenticating 86http://en.wikipedia.org/wiki/Reverse_proxy[reverse proxies], client 87SSL certificates, LDAP, token devices ...). 88 89//// 90FIXME: update to the current state of affairs 91 92Federated login is a compelling proposition, but has currently not had 93widespread success, because of usability issues and other problems, 94including privacy concerns. There are a number of exciting 95developments however which promise to resolve this: 96 97* http://openid.net/connect/[OpenIDConnect] is the much plagued 98http://openid.net/[OpenID] protocol redone, but this time on top of 99http://oauth.net/2/[OAuth2.0], and aims to be the "open and 100distributed" alternative to 101http://developers.facebook.com/docs/guides/web/[Facebook Connect]. In 102particular it solves the confusion of using URLs as identity, and 103instead has moved on to email addresses. There is an auto-discovery 104protocol that queries your email provider, and this should eliminate 105most privacy concerns (because you do trust your email provider, don't 106you ?). 107 108* http://accountchooser.com/[AccountChooser] aims to provide a 109consistent and universal method for users to sign in to 110websites. Together with the use of the email address-as-identity this 111might just work. 112 113* Implementing a usable federated login system is complex, and 114increasingly so. Not only is the logic daunting 115(http://sites.google.com/site/oauthgoog/UXFedLogin/loginlogic[account 116linking] is one example), it also requires a good mix of Ajax-y 117user interface components, server-side logic and state with a protocol 118that involves receiving and sending HTTP requests, and integration 119into a traditional password-based authentication module. Well, we are 120solving that ;-) 121 122[NOTE] 123OAuth2.0, OpenIDConnect, and AccountChooser are currently draft 124specifications. We plan to expand our support as these drafts turn 125into endorsed specifications. In particular, it would be _a plan 126coming together_ when we add an +AccountChooserWidget+ that 127complements the current 128{doc}classWt_1_1Auth_1_1AuthWidget.html[Wt::Auth::AuthWidget] 129and 130{doc}classWt_1_1Auth_1_1RegistrationWidget.html[Wt::Auth::RegistrationWidget] 131classes and which allows users to authenticate with any email provider 132using the OpenIDConnect discovery protocol. 133//// 134 135Obviously, the authentication logic needs to talk to a storage system, 136and it is designed to hook into a storage system using an abstract 137interface. A default implementation that leverages 138http://www.webtoolkit.eu/wt/doc/tutorial/dbo/tutorial.html[Wt::Dbo], 139Wt's ORM, is provided. 140 141== Module organization 142 143The following picture illustrates the main classes of the module. 144 145ifdef::basebackend-docbook[] 146image::img/auth.svg["Hello World",height=250,align="center"] 147endif::basebackend-docbook[] 148 149ifndef::basebackend-docbook[] 150image::img/auth.png[align="center"] 151endif::basebackend-docbook[] 152 153It uses a classical separation between Model classes and View classes 154(which are the widgets). 155 156There are three types of model classes: 157 158* *Service classes* are designed to be shared across all sessions 159(they do not have any state besides configuration). They contain logic 160which does not require transient state in a session. 161 162* *Session bound* model classes are usually kept in a session for the 163entire lifetime of a session (but don't need to be). 164 165* *Transient* model classes play an active role in the user-interface, 166and are instantiated in the context of certain view components. They 167implement logic which involves state while the user is progressing 168through the login and registration process. 169 170== Example 171 172We'll walk through a small example which is a basic application that 173uses the authentication module (included in the Wt distribution in 174https://github.com/kdeforche/wt/tree/master/examples/feature/auth1[examples/feature/auth1]). It 175is about 200 lines of C++ (which we'll discuss below), and has the 176following features: 177 178* Password-based authentication and registration 179* OAuth-2 login and registration, for Google and Facebook accounts 180* Password attempt throttling 181* Email verification and a lost password procedure 182* Remember-me tokens 183* And by virtue of Wt itself, falls back to plain HTML behavior if the 184browser does not support Ajax, strong security, spam resilience, 185etc... 186 187This example should help you to understand how to add authentication 188support to a new or existing Wt project. 189 190=== Setting up a user database 191 192We will be using the default implementation for an authentication 193database using +Wt::Dbo+, with the default persistence classes for 194authentication. This database implementation is found in 195{doc}classWt_1_1Auth_1_1Dbo_1_1UserDatabase.html[Wt::Auth::Dbo::UserDatabase], 196and it uses 197{doc}classWt_1_1Auth_1_1Dbo_1_1AuthInfo.html[Wt::Auth::Dbo::AuthInfo] 198as the persistence class for authentication information, which itself 199references two other persistence classes: 200 201* A user's *"identities"* are stored in a separate table. An identity 202uniquely identifies a user. Traditionally, a user would have only a 203single identity which is his login name (which could be his email 204address). But a user may accumulate more identities, corresponding to 205accounts with 3rd party identity providers. By allowing multiple 206identities, the user may identify using a choice of methods. 207* *Authentication tokens* are stored in a separate table. An 208authentication token usually corresponds to a "remember-me" cookie, 209and a user may have multiple "remember-me" cookies when using 210different computers. 211 212In addition, we define a +User+ type to which we can add the 213application data for a particular user (this could be address 214information, birth date, preferences, user role, etc...), and which we 215want to link up with the authentication system. 216 217The definition and persistence mapping for (a currently empty) User 218type is as given below: 219 220.User.h 221[source,cpp] 222---- 223#include <Wt/Dbo/Types.h> 224#include <Wt/WGlobal.h> 225 226namespace dbo = Wt::Dbo; 227 228class User; 229using AuthInfo = Wt::Auth::Dbo::AuthInfo<User>; 230 231class User { 232public: 233 template<class Action> 234 void persist(Action& a) 235 { 236 } 237}; 238 239DBO_EXTERN_TEMPLATES(User) 240---- 241 242We declare a type alias for +AuthInfo+, which links the authentication 243information persistence class to our custom +User+ information 244persistence class. 245 246Next, we define a session class, which encapsulates the connection to 247the database to store authentication information, and which also 248tracks the user currently logged in, in a web session. We choose to 249use the 250{doc}classWt_1_1Dbo_1_1Session.html[Wt::Dbo::Session] 251class as a base class (which could just as well be an embedded 252member). 253 254Later on, we'll see how each web session will instantiate its own 255persistence/authentication +Session+ object. 256 257.Session.h 258[source,cpp] 259---- 260#include <Wt/Auth/Login.h> 261#include <Wt/Auth/UserDatabase.h> 262 263#include <Wt/Dbo/Session.h> 264#include <Wt/Dbo/ptr.h> 265 266#include "User.h" 267 268namespace dbo = Wt::Dbo; 269 270using UserDatabase = Wt::Auth::Dbo::UserDatabase<AuthInfo>; 271 272class Session : public dbo::Session 273{ 274public: 275 Session(const std::string& sqliteDb); 276 277 Wt::Auth::AbstractUserDatabase& users(); 278 Wt::Auth::Login& login() { return login_; } 279 280 ... 281 282private: 283 std::unique_ptr<UserDatabase> users_; 284 Wt::Auth::Login login_; 285 286 ... 287}; 288---- 289 290Notice the type alias for +UserDatabase+, which states that we will be 291using the 292{doc}classWt_1_1Auth_1_1Dbo_1_1UserDatabase.html[Wt::Auth::Dbo::UserDatabase] 293implementation using +AuthInfo+, for which we declared a type alias 294earlier on. You are of course free to provide another implementation 295for 296{doc}classWt_1_1Auth_1_1AbstractUserDatabase.html[Wt::Auth::AbstractUserDatabase] 297which is not based on +Wt::Dbo+. 298 299We also embed a 300{doc}classWt_1_1Auth_1_1Login.html[Wt::Auth::Login] 301member here, which is a small model class that holds the current login 302information. The login/logout widgets will manipulate this login 303object, while the rest of our application will listen to login changes 304from this object to adapt to the user currently logged in. 305 306The +Session+ constructor sets up the database session. 307 308.Session.C (constructor) 309[source,cpp] 310---- 311#include "Session.h" 312#include "User.h" 313 314#include "Wt/Auth/Dbo/AuthInfo.h" 315 316#include "Wt/Dbo/backend/Sqlite3.h" 317 318using namespace Wt; 319 320Session::Session(const std::string& sqliteDb) 321{ 322 auto connection = std::make_unique<Dbo::backend::Sqlite3>(sqliteDb); 323 setConnection(std::move(connection_)); 324 325 mapClass<User>("user"); 326 mapClass<AuthInfo>("auth_info"); 327 mapClass<AuthInfo::AuthIdentityType>("auth_identity"); 328 mapClass<AuthInfo::AuthTokenType>("auth_token"); 329 330 try { 331 createTables(); 332 std::cerr << "Created database." << std::endl; 333 } catch (Wt::Dbo::Exception& e) { 334 std::cerr << e.what() << std::endl; 335 std::cerr << "Using existing database"; 336 } 337 338 users_ = std::make_unique<UserDatabase>(*this); 339} 340---- 341 342The example uses a SQLite3 database (a cuddly database convenient for 343development), and we map four persistence classes to tables. 344 345We then create the data schema if needed, which will automatically 346issue the following SQL: 347 348[source,sql] 349---- 350create table "user" ( 351 "id" integer primary key autoincrement, 352 "version" integer not null 353) 354 355create table "auth_info" ( 356 "id" integer primary key autoincrement, 357 "version" integer not null, 358 "user_id" bigint, 359 "password_hash" varchar(100) not null, 360 "password_method" varchar(20) not null, 361 "password_salt" varchar(20) not null, 362 "status" integer not null, 363 "failed_login_attempts" integer not null, 364 "last_login_attempt" text, 365 "email" varchar(256) not null, 366 "unverified_email" varchar(256) not null, 367 "email_token" varchar(64) not null, 368 "email_token_expires" text, 369 "email_token_role" integer not null, 370 constraint "fk_auth_info_user" foreign key 371 ("user_id") references "user" ("id") 372 on delete cascade deferrable initially deferred 373) 374 375create table "auth_token" ( 376 "id" integer primary key autoincrement, 377 "version" integer not null, 378 "auth_info_id" bigint, 379 "value" varchar(64) not null, 380 "expires" text, 381 constraint "fk_auth_token_auth_info" foreign key 382 ("auth_info_id") references "auth_info" ("id") 383 on delete cascade deferrable initially deferred 384) 385 386create table "auth_identity" ( 387 "id" integer primary key autoincrement, 388 "version" integer not null, 389 "auth_info_id" bigint, 390 "provider" varchar(64) not null, 391 "identity" varchar(512) not null, 392 constraint "fk_auth_identity_auth_info" foreign key 393 ("auth_info_id") references "auth_info" ("id") 394 on delete cascade defferable initially deferred 395) 396---- 397 398Notice the +auth_info+, +auth_token+ and +auth_identity+ tables that 399define the storage for our authentication system. 400 401=== Configuring authentication 402 403The service classes 404({doc}classWt_1_1Auth_1_1AuthService.html[Wt::Auth::AuthService], 405{doc}classWt_1_1Auth_1_1PasswordService.html[Wt::Auth::PasswordService], 406and 407{doc}classWt_1_1Auth_1_1OAuthService.html[Wt::Auth::OAuthService]), 408can be shared between sessions and contain the configuration and logic 409which does not require transient session state. 410 411A good location to add these service classes are inside a specialized 412{doc}classWt_1_1WServer.html[Wt::WServer] 413instance, of which you usually also have only one in a Wt process. You 414could also create a singleton for them. To keep the example simple, we 415will declare them simply as global variables (but within file scope): 416+myAuthService+, +myPasswordService+, and +myOAuthServices+. 417 418.Session.C (authentication services) 419[source,cpp] 420---- 421 422#include "Wt/Auth/AuthService.h" 423#include "Wt/Auth/HashFunction.h" 424#include "Wt/Auth/PasswordService.h" 425#include "Wt/Auth/PasswordStrengthValidator.h" 426#include "Wt/Auth/PasswordVerifier.h" 427#include "Wt/Auth/GoogleService.h" 428#include "Wt/Auth/FacebookService.h" 429 430namespace { 431 Wt::Auth::AuthService myAuthService; 432 Wt::Auth::PasswordService myPasswordService{myAuthService}; 433 std::vector<std::unique_ptr<Wt::Auth::OAuthService>> myOAuthServices; 434} 435 436void Session::configureAuth() 437{ 438 myAuthService.setAuthTokensEnabled(true, "logincookie"); 439 myAuthService.setEmailVerificationEnabled(true); 440 myAuthService.setEmailVerificationRequired(true); 441 442 std::unique_ptr<Wt::Auth::PasswordVerifier> verifier = 443 std::make_unique<Wt::Auth::PasswordVerifier>(); 444 verifier->addHashFunction(std::make_unique<Wt::Auth::BCryptHashFunction>(7)); 445 myPasswordService.setVerifier(std::move(verifier)); 446 myPasswordService.setAttemptThrottlingEnabled(true); 447 myPasswordService.setStrengthValidator(std::make_unique<Wt::Auth::PasswordStrengthValidator>()); 448 449 if (Wt::Auth::GoogleService::configured()) 450 myOAuthServices.push_back(std::make_unique<Wt::Auth::GoogleService>(myAuthService)); 451 452 if (Wt::Auth::FacebookService::configured()) 453 myOAuthServices.push_back(std::make_unique<Wt::Auth::FacebookService>(myAuthService)); 454 455 for (unsigned i = 0; i < myOAuthServices.size(); ++i) 456 myOAuthServices[i]->generateRedirectEndpoint(); 457} 458 459Wt::Auth::AbstractUserDatabase& Session::users() 460{ 461 return *users_; 462} 463 464const Wt::Auth::AuthService& Session::auth() 465{ 466 return myAuthService; 467} 468 469const Wt::Auth::PasswordService& Session::passwordAuth() 470{ 471 return myPasswordService; 472} 473 474const std::vector<const Wt::Auth::OAuthService *>& Session::oAuth() 475{ 476 std::vector<const Wt::Auth::OAuthService *> result; 477 for (auto &auth : myOAuthServices) { 478 result.push_back(auth.get()); 479 } 480 return result; 481} 482---- 483 484The {doc}classWt_1_1Auth_1_1AuthService.html[Wt::Auth::AuthService] 485is configured to support "remember-me" functionality, and email 486verification. 487 488The 489{doc}classWt_1_1Auth_1_1PasswordService.html[Wt::Auth::PasswordService] 490needs a hash function to safely store passwords. You can actually 491define more than one hash function, which is useful only if you want 492to migrate to a new hash function while still supporting existing 493passwords. When a user logs in, and he is not using the "preferred" 494hash function, his password will be rehashed with the preferred 495one. In this example, we will use 496http://en.wikipedia.org/wiki/Bcrypt[bcrypt] which is included as a 497{doc}classWt_1_1Auth_1_1HashFunction.html[hash 498function] in Wt::Auth. 499 500We also enable password attempt throttling: this mitigates brute force 501password guessing attempts. 502 503Finally, we also use two (but later, perhaps more) 504{doc}classWt_1_1Auth_1_1OAuthService.html[Wt::Auth::OAuthService] 505classes. You need one service per identity provider. In this case, 506we add Google and Facebook as identity providers. 507 508=== The user interface 509 510We create a specialized 511{doc}classWt_1_1WApplication.html[Wt::WApplication] 512which contains our authentication session, and instantiates a 513{doc}classWt_1_1Auth_1_1AuthWidget.html[Wt::Auth::AuthWidget]. This 514widget shows a login or logout form (depending on the login status), 515and also hooks into default forms for registration, lost passwords, 516and handling of email-sent tokens in URLs). 517 518.User interface 519[source,cpp] 520---- 521#include <Wt/WApplication.h> 522#include <Wt/WBootstrapTheme.h> 523#include <Wt/WContainerWidget.h> 524#include <Wt/WServer.h> 525 526#include <Wt/Auth/AuthWidget.h> 527#include <Wt/Auth/PasswordService.h> 528 529#include "model/Session.h" 530 531class AuthApplication : public Wt::WApplication 532{ 533public: 534 AuthApplication(const Wt::WEnvironment& env) 535 : Wt::WApplication(env), 536 session_(appRoot() + "auth.db") 537 { 538 session_.login().changed().connect(this, &AuthApplication::authEvent); 539 540 root()->addStyleClass("container"); 541 setTheme(std::make_shared<Wt::WBootstrapTheme>()); 542 543 useStyleSheet("css/style.css"); 544 545 std::unique_ptr<Wt::Auth::AuthWidget> authWidget = 546 std::make_unique<Wt::Auth::AuthWidget>(Session::auth(), session_.users(), session_.login()); 547 548 authWidget->model()->addPasswordAuth(&Session::passwordAuth()); 549 authWidget->model()->addOAuth(Session::oAuth()); 550 authWidget->setRegistrationEnabled(true); 551 552 authWidget->processEnvironment(); 553 554 root()->addWidget(std::move(authWidget)); 555 } 556 557 void authEvent() { 558 if (session_.login().loggedIn()) { 559 const Wt::Auth::User& u = session_.login().user(); 560 log("notice") 561 << "User " << u.id() 562 << " (" << u.identity(Wt::Auth::Identity::LoginName) << ")" 563 << " logged in."; 564 } else 565 log("notice") << "User logged out."; 566 } 567 568private: 569 Session session_; 570}; 571---- 572 573The last part is our main function where we setup the application server: 574 575.Application server setup 576[source,cpp] 577---- 578std::unique_ptr<Wt::WApplication> createApplication(const Wt::WEnvironment &env) 579{ 580 return std::make_unique<AuthApplication>(env); 581} 582 583int main(int argc, char **argv) 584{ 585 try { 586 Wt::WServer server{argc, argv, WTHTTP_CONFIGURATION}; 587 588 server.addEntryPoint(Wt::EntryPointType::Application, createApplication); 589 590 Session::configureAuth(); 591 592 server.run(); 593 } catch (Wt::WServer::Exception& e) { 594 std::cerr << e.what() << std::endl; 595 } catch (Wt::Dbo::Exception &e) { 596 std::cerr << "Dbo exception: " << e.what() << std::endl; 597 } catch (std::exception &e) { 598 std::cerr << "exception: " << e.what() << std::endl; 599 } 600} 601---- 602 603 604