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