1 /*
2  * Copyright (C) 2011 Emweb bv, Herent, Belgium.
3  *
4  * See the LICENSE file for terms of use.
5  */
6 
7 #include "Wt/Auth/AbstractUserDatabase.h"
8 #include "Wt/Auth/AuthModel.h"
9 #include "Wt/Auth/AuthService.h"
10 #include "Wt/Auth/LostPasswordWidget.h"
11 #include "Wt/Auth/OAuthWidget.h"
12 #include "Wt/Auth/PasswordPromptDialog.h"
13 #include "Wt/Auth/RegistrationWidget.h"
14 #include "Wt/Auth/UpdatePasswordWidget.h"
15 
16 #include "Wt/Auth/OAuthService.h"
17 
18 #ifdef WT_HAS_SAML
19 #include "Wt/Auth/Saml/Process.h"
20 #include "Wt/Auth/Saml/Service.h"
21 #include "Wt/Auth/Saml/Widget.h"
22 #endif // WT_HAS_SAML
23 
24 #include "Wt/WApplication.h"
25 #include "Wt/WAnchor.h"
26 #include "Wt/WCheckBox.h"
27 #include "Wt/WContainerWidget.h"
28 #include "Wt/WDialog.h"
29 #include "Wt/WEnvironment.h"
30 #include "Wt/WImage.h"
31 #include "Wt/WLineEdit.h"
32 #include "Wt/WLogger.h"
33 #include "Wt/WMessageBox.h"
34 #include "Wt/WPushButton.h"
35 #include "Wt/WText.h"
36 #include "Wt/WTheme.h"
37 
38 #include "Login.h"
39 #include "AuthWidget.h"
40 #include "web/WebUtils.h"
41 
42 #include "Wt/WDllDefs.h"
43 
44 #include <memory>
45 
46 namespace Wt {
47 
48 LOGGER("Auth.AuthWidget");
49 
50   namespace Auth {
51 
AuthWidget(const AuthService & baseAuth,AbstractUserDatabase & users,Login & login)52 AuthWidget::AuthWidget(const AuthService& baseAuth,
53 		       AbstractUserDatabase& users, Login& login)
54   : WTemplateFormView(WString::Empty),
55     model_(std::make_shared<AuthModel>(baseAuth, users)),
56     login_(login)
57 {
58   init();
59 }
60 
AuthWidget(Login & login)61 AuthWidget::AuthWidget(Login& login)
62   : WTemplateFormView(WString::Empty),
63     login_(login)
64 {
65   init();
66 }
67 
~AuthWidget()68 AuthWidget::~AuthWidget()
69 {
70   dialog_.reset();
71   messageBox_.reset();
72 }
73 
init()74 void AuthWidget::init()
75 {
76   setWidgetIdMode(TemplateWidgetIdMode::SetObjectName);
77 
78   registrationEnabled_ = false;
79   created_ = false;
80 
81   WApplication *app = WApplication::instance();
82   app->internalPathChanged().connect(this, &AuthWidget::onPathChange);
83   app->theme()->apply(this, this, AuthWidgets);
84 
85   login_.changed().connect(this, &AuthWidget::onLoginChange);
86 }
87 
setModel(std::unique_ptr<AuthModel> model)88 void AuthWidget::setModel(std::unique_ptr<AuthModel> model)
89 {
90   model_ = std::move(model);
91 }
92 
setRegistrationEnabled(bool enabled)93 void AuthWidget::setRegistrationEnabled(bool enabled)
94 {
95   registrationEnabled_ = enabled;
96 }
97 
setInternalBasePath(const std::string & basePath)98 void AuthWidget::setInternalBasePath(const std::string& basePath)
99 {
100   basePath_ = Utils::append(Utils::prepend(basePath, '/'), '/');;
101 }
102 
onPathChange(const std::string & path)103 void AuthWidget::onPathChange(const std::string& path)
104 {
105   handleRegistrationPath(path);
106 }
107 
handleRegistrationPath(const std::string & path)108 bool AuthWidget::handleRegistrationPath(const std::string& path)
109 {
110   if (!basePath_.empty()) {
111     WApplication *app = WApplication::instance();
112 
113     if (app->internalPathMatches(basePath_)) {
114       std::string ap = app->internalSubPath(basePath_);
115 
116       if (ap == "register/") {
117 	registerNewUser();
118 	return true;
119       }
120     }
121   }
122 
123   return false;
124 }
125 
registerNewUser()126 void AuthWidget::registerNewUser()
127 {
128   registerNewUser(Identity::Invalid);
129 }
130 
registerNewUser(const Identity & oauth)131 void AuthWidget::registerNewUser(const Identity& oauth)
132 {
133   showDialog(tr("Wt.Auth.registration"), createRegistrationView(oauth));
134 }
135 
showDialog(const WString & title,std::unique_ptr<WWidget> contents)136 WDialog *AuthWidget::showDialog(const WString& title,
137 				std::unique_ptr<WWidget> contents)
138 {
139   if (contents) {
140     dialog_.reset(new WDialog(title));
141     dialog_->contents()->addWidget(std::move(contents));
142     dialog_->contents()->childrenChanged()
143       .connect(this, &AuthWidget::closeDialog);
144 
145     dialog_->footer()->hide();
146 
147     if (!WApplication::instance()->environment().ajax()) {
148       /*
149        * try to center it better, we need to set the half width and
150        * height as negative margins.
151        */
152       dialog_->setMargin(WLength("-21em"), Side::Left); // .Wt-form width
153       dialog_->setMargin(WLength("-200px"), Side::Top); // ???
154     }
155 
156     dialog_->show();
157   }
158 
159   return dialog_.get();
160 }
161 
closeDialog()162 void AuthWidget::closeDialog()
163 {
164   if (dialog_) {
165 #ifdef WT_TARGET_JAVA
166     delete dialog_.release();
167 #endif
168     dialog_.reset();
169   } else {
170 #ifdef WT_TARGET_JAVA
171     delete messageBox_.release();
172 #endif
173     messageBox_.reset();
174   }
175 
176   /* Reset internal path */
177   if (!basePath_.empty()) {
178     WApplication *app = WApplication::instance();
179     if (app->internalPathMatches(basePath_)) {
180       std::string ap = app->internalSubPath(basePath_);
181 
182       if (ap == "register/") {
183         app->setInternalPath(basePath_, false);
184       }
185     }
186   }
187 }
188 
createRegistrationModel()189 std::unique_ptr<RegistrationModel> AuthWidget::createRegistrationModel()
190 {
191   auto result = std::unique_ptr<RegistrationModel>
192     (new RegistrationModel(*model_->baseAuth(), model_->users(), login_));
193 
194   if (model_->passwordAuth())
195     result->addPasswordAuth(model_->passwordAuth());
196 
197   result->addOAuth(model_->oAuth());
198 #ifdef WT_HAS_SAML
199   result->addSaml(model_->saml());
200 #endif // WT_HAS_SAML
201   return result;
202 }
203 
createRegistrationView(const Identity & id)204 std::unique_ptr<WWidget> AuthWidget::createRegistrationView(const Identity& id)
205 {
206   auto model = createRegistrationModel();
207 
208   if (id.isValid())
209     model->registerIdentified(id);
210 
211   std::unique_ptr<RegistrationWidget> w(new RegistrationWidget(this));
212   w->setModel(std::move(model));
213   return std::move(w);
214 }
215 
handleLostPassword()216 void AuthWidget::handleLostPassword()
217 {
218   showDialog(tr("Wt.Auth.lostpassword"), createLostPasswordView());
219 }
220 
createLostPasswordView()221 std::unique_ptr<WWidget> AuthWidget::createLostPasswordView()
222 {
223   return std::unique_ptr<WWidget>
224     (new LostPasswordWidget(model_->users(), *model_->baseAuth()));
225 }
226 
letUpdatePassword(const User & user,bool promptPassword)227 void AuthWidget::letUpdatePassword(const User& user, bool promptPassword)
228 {
229   std::unique_ptr<WWidget> updatePasswordView = createUpdatePasswordView(user, promptPassword);
230 
231   UpdatePasswordWidget *defaultUpdatePasswordWidget =
232       dynamic_cast<UpdatePasswordWidget*>(updatePasswordView.get());
233 
234   showDialog(tr("Wt.Auth.updatepassword"), std::move(updatePasswordView));
235 
236   if (defaultUpdatePasswordWidget) {
237     defaultUpdatePasswordWidget->updated().connect(this, &AuthWidget::closeDialog);
238     defaultUpdatePasswordWidget->canceled().connect(this, &AuthWidget::closeDialog);
239   }
240 }
241 
242 std::unique_ptr<WWidget> AuthWidget
createUpdatePasswordView(const User & user,bool promptPassword)243 ::createUpdatePasswordView(const User& user, bool promptPassword)
244 {
245   return std::unique_ptr<WWidget>
246     (new UpdatePasswordWidget
247      (user, createRegistrationModel(),
248       promptPassword ? model_ : std::shared_ptr<AuthModel>()));
249 }
250 
createPasswordPromptDialog(Login & login)251 std::unique_ptr<WDialog> AuthWidget::createPasswordPromptDialog(Login& login)
252 {
253   return std::make_unique<PasswordPromptDialog>(login, model_);
254 }
255 
logout()256 void AuthWidget::logout()
257 {
258   model_->logout(login_);
259 }
260 
displayError(const WString & m)261 void AuthWidget::displayError(const WString& m)
262 {
263   messageBox_.reset(new WMessageBox(tr("Wt.Auth.error"), m,
264 				    Icon::None, StandardButton::Ok));
265   messageBox_->buttonClicked().connect(this, &AuthWidget::closeDialog);
266   messageBox_->show();
267 }
268 
displayInfo(const WString & m)269 void AuthWidget::displayInfo(const WString& m)
270 {
271   messageBox_.reset(new WMessageBox(tr("Wt.Auth.notice"), m,
272 				    Icon::None, StandardButton::Ok));
273   messageBox_->buttonClicked().connect(this, &AuthWidget::closeDialog);
274   messageBox_->show();
275 }
276 
render(WFlags<RenderFlag> flags)277 void AuthWidget::render(WFlags<RenderFlag> flags)
278 {
279   if (!created_) {
280     create();
281     created_ = true;
282   }
283 
284   WTemplateFormView::render(flags);
285 }
286 
create()287 void AuthWidget::create()
288 {
289   if (created_)
290     return;
291 
292   onLoginChange();
293 
294   created_ = true;
295 }
296 
onLoginChange()297 void AuthWidget::onLoginChange()
298 {
299   if (!(isRendered() || created_))
300     return;
301 
302   clear();
303 
304   if (login_.loggedIn()) {
305 #ifndef WT_TARGET_JAVA
306     if (created_) // do not do this if onLoginChange() is called from create()
307       WApplication::instance()->changeSessionId();
308 #endif // WT_TARGET_JAVA
309 
310     createLoggedInView();
311   } else {
312     if (login_.state() != LoginState::Disabled) {
313       if (model_->baseAuth()->authTokensEnabled()) {
314 	WApplication::instance()->removeCookie
315 	  (model_->baseAuth()->authTokenCookieName());
316       }
317 
318       model_->reset();
319       createLoginView();
320     } else {
321 	  createLoginView();
322 	}
323   }
324 }
325 
createLoginView()326 void AuthWidget::createLoginView()
327 {
328   setTemplateText(tr("Wt.Auth.template.login"));
329 
330   createPasswordLoginView();
331   createOAuthLoginView();
332 #ifdef WT_HAS_SAML
333   createSamlLoginView();
334 #endif // WT_HAS_SAML_
335 }
336 
createPasswordLoginView()337 void AuthWidget::createPasswordLoginView()
338 {
339   updatePasswordLoginView();
340 }
341 
createFormWidget(WFormModel::Field field)342 std::unique_ptr<WWidget> AuthWidget::createFormWidget(WFormModel::Field field)
343 {
344   std::unique_ptr<WFormWidget> result;
345 
346   if (field == AuthModel::LoginNameField) {
347     result.reset(new WLineEdit());
348     result->setFocus(true);
349   } else if (field == AuthModel::PasswordField) {
350     WLineEdit *p = new WLineEdit();
351     p->enterPressed().connect(this, &AuthWidget::attemptPasswordLogin);
352     p->setEchoMode(EchoMode::Password);
353     result.reset(p);
354   } else if (field == AuthModel::RememberMeField) {
355     result.reset(new WCheckBox());
356   }
357 
358   return std::move(result);
359 }
360 
updatePasswordLoginView()361 void AuthWidget::updatePasswordLoginView()
362 {
363   if (model_->passwordAuth()) {
364     setCondition("if:passwords", true);
365 
366     updateView(model_.get());
367 
368     WInteractWidget *login = resolve<WInteractWidget *>("login");
369 
370     if (!login) {
371       login = bindWidget("login", std::make_unique<WPushButton>(tr("Wt.Auth.login")));
372       login->clicked().connect(this, &AuthWidget::attemptPasswordLogin);
373 
374       model_->configureThrottling(login);
375 
376       if (model_->baseAuth()->emailVerificationEnabled()) {
377 	WText *text =
378 	  bindWidget("lost-password",
379 		     std::make_unique<WText>(tr("Wt.Auth.lost-password")));
380 	text->clicked().connect(this, &AuthWidget::handleLostPassword);
381       } else
382 	bindEmpty("lost-password");
383 
384       if (registrationEnabled_) {
385 	if (!basePath_.empty()) {
386 	  bindWidget("register",
387 		     std::make_unique<WAnchor>
388 		     (WLink(LinkType::InternalPath, basePath_ + "register"),
389 		      tr("Wt.Auth.register")));
390 	} else {
391 	  WText *t =
392 	    bindWidget("register",
393 		       std::make_unique<WText>(tr("Wt.Auth.register")));
394 	  t->clicked().connect(this, &AuthWidget::registerNewUser);
395 	}
396       } else
397 	bindEmpty("register");
398 
399       if (model_->baseAuth()->emailVerificationEnabled()
400 	  && registrationEnabled_)
401 	bindString("sep", " | ");
402       else
403 	bindEmpty("sep");
404     }
405 
406     model_->updateThrottling(login);
407   } else {
408     bindEmpty("lost-password");
409     bindEmpty("sep");
410     bindEmpty("register");
411     bindEmpty("login");
412   }
413 }
414 
createOAuthLoginView()415 void AuthWidget::createOAuthLoginView()
416 {
417   if (!model_->oAuth().empty()) {
418     setCondition("if:oauth", true);
419 
420     WContainerWidget *icons
421       = bindWidget("icons", std::make_unique<WContainerWidget>());
422     icons->setInline(isInline());
423 
424     for (unsigned i = 0; i < model_->oAuth().size(); ++i) {
425       const OAuthService *service = model_->oAuth()[i];
426 
427       OAuthWidget *w
428 	= icons->addWidget(std::make_unique<OAuthWidget>(*service));
429       w->authenticated().connect(this, &AuthWidget::oAuthDone);
430     }
431   }
432 }
433 
434 #ifdef WT_HAS_SAML
createSamlLoginView()435 void AuthWidget::createSamlLoginView()
436 {
437   if (!model_->saml().empty()) {
438     setCondition("if:oauth", true);
439 
440     WContainerWidget *icons = resolve<WContainerWidget *>("icons");
441     if (!icons) {
442       icons = bindWidget("icons", std::make_unique<WContainerWidget>());
443       icons->setInline(isInline());
444     }
445 
446     for (const Saml::Service *saml : model()->saml()) {
447       Saml::Widget *w = icons->addNew<Saml::Widget>(*saml);
448       w->authenticated().connect(this, &AuthWidget::samlDone);
449     }
450   }
451 }
452 #endif // WT_HAS_SAML
453 
oAuthDone(OAuthProcess * oauth,const Identity & identity)454 void AuthWidget::oAuthDone(OAuthProcess *oauth, const Identity& identity)
455 {
456   /*
457    * FIXME: perhaps consider moving this to the model with signals or
458    * by passing the Login object ?
459    */
460   if (identity.isValid()) {
461     LOG_SECURE(oauth->service().name() << ": identified: as "
462 	       << identity.id() << ", "
463 	       << identity.name() << ", " << identity.email());
464 
465     std::unique_ptr<AbstractUserDatabase::Transaction>
466       t(model_->users().startTransaction());
467 
468     User user = model_->baseAuth()->identifyUser(identity, model_->users());
469     if (user.isValid())
470       model_->loginUser(login_, user);
471     else
472       registerNewUser(identity);
473 
474     if (t.get())
475       t->commit();
476   } else {
477     LOG_SECURE(oauth->service().name() << ": error: " << oauth->error());
478     displayError(oauth->error());
479   }
480 }
481 
482 #ifdef WT_HAS_SAML
samlDone(Saml::Process * process,const Identity & identity)483 void AuthWidget::samlDone(Saml::Process *process, const Identity &identity)
484 {
485   if (identity.isValid()) {
486     LOG_SECURE(process->service().name() << ": identified: as "
487                                        << identity.id() << ", "
488                                        << identity.name() << ", " << identity.email());
489 
490     std::unique_ptr<AbstractUserDatabase::Transaction>
491       t(model_->users().startTransaction());
492 
493     User user = model_->baseAuth()->identifyUser(identity, model_->users());
494     if (user.isValid())
495       model_->loginUser(login_, user);
496     else
497       registerNewUser(identity);
498 
499     if (t.get())
500       t->commit();
501   } else {
502     LOG_SECURE(process->service().name() << ": error: " << process->error());
503     displayError(process->error());
504   }
505 }
506 #endif // WT_HAS_SAML
507 
attemptPasswordLogin()508 void AuthWidget::attemptPasswordLogin()
509 {
510   updateModel(model_.get());
511 
512   if (model_->validate()) {
513     if (!model_->login(login_))
514       updatePasswordLoginView();
515   } else
516     updatePasswordLoginView();
517 }
518 
createLoggedInView()519 void AuthWidget::createLoggedInView()
520 {
521   setTemplateText(tr("Wt.Auth.template.logged-in"));
522 
523   bindString("user-name", login_.user().identity(Identity::LoginName));
524 
525   WPushButton *logout
526     = bindWidget("logout",
527 		 std::make_unique<WPushButton>(tr("Wt.Auth.logout")));
528   logout->clicked().connect(this, &AuthWidget::logout);
529 }
530 
processEnvironment()531 void AuthWidget::processEnvironment()
532 {
533   const WEnvironment& env = WApplication::instance()->environment();
534 
535   if (registrationEnabled_)
536     if (handleRegistrationPath(env.internalPath()))
537       return;
538 
539   std::string emailToken
540     = model_->baseAuth()->parseEmailToken(env.internalPath());
541 
542   if (!emailToken.empty()) {
543     EmailTokenResult result = model_->processEmailToken(emailToken);
544     switch (result.state()) {
545     case EmailTokenState::Invalid:
546       displayError(tr("Wt.Auth.error-invalid-token"));
547       break;
548     case EmailTokenState::Expired:
549       displayError(tr("Wt.Auth.error-token-expired"));
550       break;
551     case EmailTokenState::UpdatePassword:
552       letUpdatePassword(result.user(), false);
553       break;
554     case EmailTokenState::EmailConfirmed:
555       displayInfo(tr("Wt.Auth.info-email-confirmed"));
556       User user = result.user();
557       model_->loginUser(login_, user);
558     }
559 
560     /*
561      * In progressive bootstrap mode, this would cause a redirect w/o
562      * session ID, losing the dialog.
563      */
564     if (WApplication::instance()->environment().ajax())
565       WApplication::instance()->setInternalPath("/");
566 
567     return;
568   }
569 
570   User user = model_->processAuthToken();
571   model_->loginUser(login_, user, LoginState::Weak);
572 }
573 
574   }
575 }
576