mirror of
				https://github.com/mail-in-a-box/mailinabox.git
				synced 2025-10-31 19:00:54 +00:00 
			
		
		
		
	Reorganize MFA front-end and add label column
This commit is contained in:
		
							parent
							
								
									a8ea456b49
								
							
						
					
					
						commit
						b80f225691
					
				| @ -128,6 +128,12 @@ def me(): | ||||
| 	try: | ||||
| 		email, privs = auth_service.authenticate(request, env) | ||||
| 	except ValueError as e: | ||||
| 		if "missing-totp-token" in str(e): | ||||
| 			return json_response({ | ||||
| 				"status": "missing-totp-token", | ||||
| 				"reason": str(e), | ||||
| 			}) | ||||
| 		else: | ||||
| 			# Log the failed login | ||||
| 			log_failed_login(request) | ||||
| 			return json_response({ | ||||
| @ -408,11 +414,12 @@ def mfa_get_status(): | ||||
| def totp_post_enable(): | ||||
| 	secret = request.form.get('secret') | ||||
| 	token = request.form.get('token') | ||||
| 	label = request.form.get('label') | ||||
| 	if type(token) != str: | ||||
| 		return json_response({ "error": 'bad_input' }, 400) | ||||
| 	try: | ||||
| 		validate_totp_secret(secret) | ||||
| 		enable_mfa(request.user_email, "totp", secret, token, env) | ||||
| 		enable_mfa(request.user_email, "totp", secret, token, label, env) | ||||
| 	except ValueError as e: | ||||
| 		return str(e) | ||||
| 	return "OK" | ||||
|  | ||||
| @ -15,13 +15,13 @@ def get_user_id(email, c): | ||||
| 
 | ||||
| def get_mfa_state(email, env): | ||||
| 	c = open_database(env) | ||||
| 	c.execute('SELECT id, type, secret, mru_token FROM mfa WHERE user_id=?', (get_user_id(email, c),)) | ||||
| 	c.execute('SELECT id, type, secret, mru_token, label FROM mfa WHERE user_id=?', (get_user_id(email, c),)) | ||||
| 	return [ | ||||
| 		{ "id": r[0], "type": r[1], "secret": r[2], "mru_token": r[3] } | ||||
| 		{ "id": r[0], "type": r[1], "secret": r[2], "mru_token": r[3], "label": r[4] } | ||||
| 		for r in c.fetchall() | ||||
| 	] | ||||
| 
 | ||||
| def enable_mfa(email, type, secret, token, env): | ||||
| def enable_mfa(email, type, secret, token, label, env): | ||||
| 	if type == "totp": | ||||
| 		validate_totp_secret(secret) | ||||
| 		# Sanity check with the provide current token. | ||||
| @ -32,7 +32,7 @@ def enable_mfa(email, type, secret, token, env): | ||||
| 		raise ValueError("Invalid MFA type.") | ||||
| 
 | ||||
| 	conn, c = open_database(env, with_connection=True) | ||||
| 	c.execute('INSERT INTO mfa (user_id, type, secret) VALUES (?, ?, ?)', (get_user_id(email, c), type, secret)) | ||||
| 	c.execute('INSERT INTO mfa (user_id, type, secret, label) VALUES (?, ?, ?, ?)', (get_user_id(email, c), type, secret, label)) | ||||
| 	conn.commit() | ||||
| 
 | ||||
| def set_mru_token(email, token, env): | ||||
|  | ||||
| @ -93,16 +93,18 @@ | ||||
|                 <li class="dropdown-header">Advanced Pages</li> | ||||
|                 <li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li> | ||||
|                 <li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li> | ||||
|                 <li><a href="#two_factor_auth" onclick="return show_panel(this);">Two-Factor Authentication</a></li> | ||||
|                 <li><a href="/admin/munin" target="_blank">Munin Monitoring</a></li> | ||||
|               </ul> | ||||
|             </li> | ||||
|             <li class="dropdown"> | ||||
|               <a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail <b class="caret"></b></a> | ||||
|               <a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail & Users <b class="caret"></b></a> | ||||
|               <ul class="dropdown-menu"> | ||||
|                 <li><a href="#mail-guide" onclick="return show_panel(this);">Instructions</a></li> | ||||
|                 <li><a href="#users" onclick="return show_panel(this);">Users</a></li> | ||||
|                 <li><a href="#aliases" onclick="return show_panel(this);">Aliases</a></li> | ||||
|                 <li class="divider"></li> | ||||
|                 <li class="dropdown-header">Your Account</li> | ||||
|                 <li><a href="#mfa" onclick="return show_panel(this);">Two-Factor Authentication</a></li> | ||||
|               </ul> | ||||
|             </li> | ||||
|             <li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li> | ||||
| @ -132,8 +134,8 @@ | ||||
|       {% include "custom-dns.html" %} | ||||
|       </div> | ||||
| 
 | ||||
|       <div id="panel_two_factor_auth" class="admin_panel"> | ||||
|       {% include "two-factor-auth.html" %} | ||||
|       <div id="panel_mfa" class="admin_panel"> | ||||
|       {% include "mfa.html" %} | ||||
|       </div> | ||||
| 
 | ||||
|       <div id="panel_login" class="admin_panel"> | ||||
|  | ||||
| @ -61,6 +61,13 @@ sudo tools/mail.py user make-admin me@{{hostname}}</pre> | ||||
|         <input name="password" type="password" class="form-control" id="loginPassword" placeholder="Password"> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="form-group" id="loginOtp"> | ||||
|       <label for="loginOtpInput" class="col-sm-3 control-label">Code</label> | ||||
|       <div class="col-sm-9"> | ||||
|           <input type="text" class="form-control" id="loginOtpInput" placeholder="6-digit code"> | ||||
|           <div class="help-block" style="margin-top: 5px; font-size: 90%">Enter the six-digit code generated by your two factor authentication app.</div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="form-group"> | ||||
|       <div class="col-sm-offset-3 col-sm-9"> | ||||
|         <div class="checkbox"> | ||||
| @ -70,12 +77,6 @@ sudo tools/mail.py user make-admin me@{{hostname}}</pre> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="form-group" id="loginOtp"> | ||||
|         <div class="col-sm-offset-3 col-sm-9"> | ||||
|           <label for="loginOtpInput" class="control-label">Two-Factor Code</label> | ||||
|           <input type="text" class="form-control" id="loginOtpInput" placeholder="6-digit code"> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="form-group"> | ||||
|       <div class="col-sm-offset-3 col-sm-9"> | ||||
|         <button type="submit" class="btn btn-default">Sign in</button> | ||||
| @ -111,13 +112,18 @@ function do_login() { | ||||
|     // This API call always succeeds. It returns a JSON object indicating | ||||
|     // whether the request was authenticated or not. | ||||
|     if (response.status != 'ok') { | ||||
|       if (response.status === 'missing_token' && !$('#loginForm').hasClass('is-twofactor')) { | ||||
|       if (response.status === 'missing-totp-token' || (response.status === 'invalid' && response.reason == 'invalid-totp-token')) { | ||||
|         $('#loginForm').addClass('is-twofactor'); | ||||
|         if (response.reason === "invalid-totp-token") { | ||||
|           show_modal_error("Login Failed", "Incorrect two factor authentication token."); | ||||
|         } else { | ||||
|           setTimeout(() => { | ||||
|               $('#loginOtpInput').focus(); | ||||
|           }); | ||||
|         } | ||||
|       } else { | ||||
|         $('#loginForm').removeClass('is-twofactor'); | ||||
| 
 | ||||
|         // Show why the login failed. | ||||
|         show_modal_error("Login Failed", response.reason) | ||||
| 
 | ||||
|  | ||||
| @ -33,38 +33,65 @@ | ||||
| 
 | ||||
| <h2>Two-Factor Authentication</h2> | ||||
| 
 | ||||
| <p>When two-factor authentication is enabled, you will be prompted to enter a six digit code from an | ||||
| authenticator app (usually on your phone) when you log into this control panel.</p> | ||||
| 
 | ||||
| <div class="panel panel-danger"> | ||||
| <div class="panel-heading"> | ||||
| Enabling two-factor authentication does not protect access to your email | ||||
| </div> | ||||
| <div class="panel-body"> | ||||
| Enabling two-factor authentication on this page only limits access to this control panel. Remember that most websites allow you to | ||||
| reset your password by checking your email, so anyone with access to your email can typically take over | ||||
| your other accounts. Additionally, if your email address or any alias that forwards to your email | ||||
| address is a typical domain control validation address (e.g admin@, administrator@, postmaster@, hostmaster@, | ||||
| webmaster@, abuse@), extra care should be taken to protect the account. <strong>Always use a strong password, | ||||
| and ensure every administrator account for this control panel does the same.</strong> | ||||
| </div> | ||||
| </div> | ||||
| 
 | ||||
| <div class="twofactor"> | ||||
|     <div class="loading-indicator">Loading...</div> | ||||
| 
 | ||||
|     <form id="totp-setup"> | ||||
|         <p>After enabling two-factor authentication, any login to the admin panel will require you to enter a time-limited 6-digit number from an authenticator app after entering your normal credentials.</p> | ||||
|         <h3>Setup Instructions</h3> | ||||
| 
 | ||||
|         <div class="form-group"> | ||||
|             <h3>Setup Instructions</h3> | ||||
|             <p>1. Scan the QR code or enter the secret into an authenticator app (e.g. Google Authenticator)</p> | ||||
|             <p>1. Install <a href="https://freeotp.github.io/">FreeOTP</a> or <a href="https://www.pcworld.com/article/3225913/what-is-two-factor-authentication-and-which-2fa-apps-are-best.html">any | ||||
|             other two-factor authentication app</a> that supports TOTP.</p> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="form-group"> | ||||
|             <p style="margin-bottom: 0">2. Scan the QR code in the app or directly enter the secret into the app:</p> | ||||
|             <div id="totp-setup-qr"></div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="form-group"> | ||||
|             <label for="otp">2. Enter the code displayed in the Authenticator app</label> | ||||
|             <p>You will have to log into the admin panel again after enabling two-factor authentication.</p> | ||||
|             <label for="otp-label" style="font-weight: normal">3. Optionally, give your device a label so that you can remember what device you set it up on:</label> | ||||
|             <input type="text" id="totp-setup-label" class="form-control" placeholder="my phone" /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="form-group"> | ||||
|             <label for="otp" style="font-weight: normal">4. Use the app to generate your first six-digit code and enter it here:</label> | ||||
|             <input type="text" id="totp-setup-token" class="form-control" placeholder="6-digit code" /> | ||||
|         </div> | ||||
| 
 | ||||
|         <input type="hidden" id="totp-setup-secret" /> | ||||
| 
 | ||||
|         <div class="form-group"> | ||||
|             <button id="totp-setup-submit" disabled type="submit" class="btn">Enable two-factor authentication</button> | ||||
|             <p>When you click Enable Two-Factor Authentication, you will be logged out of the control panel and will have to log in | ||||
|             again, now using your two-factor authentication app.</p> | ||||
|             <button id="totp-setup-submit" disabled type="submit" class="btn">Enable Two-Factor Authentication</button> | ||||
|         </div> | ||||
|     </form> | ||||
| 
 | ||||
|     <form id="disable-2fa"> | ||||
|         <div class="form-group"> | ||||
|             <p>Two-factor authentication is active for your account. You can disable it by clicking below button.</p> | ||||
|             <p>Two-factor authentication is active for your account<span id="mfa-device-label"></span>.</p> | ||||
|             <p>You will have to log into the admin panel again after disabling two-factor authentication.</p> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|             <button type="submit" class="btn btn-danger">Disable two-factor authentication</button> | ||||
|             <button type="submit" class="btn btn-danger">Disable Two-Factor Authentication</button> | ||||
|         </div> | ||||
|     </form> | ||||
| 
 | ||||
| @ -80,6 +107,7 @@ | ||||
|         totpSetupForm: document.getElementById('totp-setup'), | ||||
|         totpSetupToken: document.getElementById('totp-setup-token'), | ||||
|         totpSetupSecret: document.getElementById('totp-setup-secret'), | ||||
|         totpSetupLabel: document.getElementById('totp-setup-label'), | ||||
|         totpQr: document.getElementById('totp-setup-qr'), | ||||
|         totpSetupSubmit: document.querySelector('#totp-setup-submit'), | ||||
|         wrapper: document.querySelector('.twofactor') | ||||
| @ -101,30 +129,29 @@ | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function render_totp_setup(res) { | ||||
|         function render_qr_code(encoded) { | ||||
|     function render_totp_setup(provisioned_totp) { | ||||
|         var img = document.createElement('img'); | ||||
|             img.src = encoded; | ||||
|         img.src = "data:image/png;base64," + provisioned_totp.qr_code_base64; | ||||
| 
 | ||||
|         var code = document.createElement('div'); | ||||
|             code.innerHTML = `Secret: ${res.totp_secret}`; | ||||
|         code.innerHTML = `Secret: ${provisioned_totp.secret}`; | ||||
| 
 | ||||
|         el.totpQr.appendChild(img); | ||||
|         el.totpQr.appendChild(code); | ||||
|         } | ||||
| 
 | ||||
|         el.totpSetupToken.addEventListener('input', update_setup_disabled); | ||||
|         el.totpSetupForm.addEventListener('submit', do_enable_totp); | ||||
| 
 | ||||
|         el.totpSetupSecret.setAttribute('value', res.totp_secret); | ||||
|         render_qr_code(res.totp_qr); | ||||
|         el.totpSetupSecret.setAttribute('value', provisioned_totp.secret); | ||||
| 
 | ||||
|         el.wrapper.classList.add('disabled'); | ||||
|     } | ||||
| 
 | ||||
|     function render_disable() { | ||||
|     function render_disable(mfa) { | ||||
|         el.disableForm.addEventListener('submit', do_disable); | ||||
|         el.wrapper.classList.add('enabled'); | ||||
|         if (mfa.label) | ||||
|           $("#mfa-device-label").text(" on device '" + mfa.label + "'"); | ||||
|     } | ||||
| 
 | ||||
|     function hide_error() { | ||||
| @ -154,7 +181,7 @@ | ||||
|         el.totpQr.innerHTML = ''; | ||||
|     } | ||||
| 
 | ||||
|     function show_two_factor_auth() { | ||||
|     function show_mfa() { | ||||
|         reset_view(); | ||||
| 
 | ||||
|         api( | ||||
| @ -163,8 +190,17 @@ | ||||
|             {}, | ||||
|             function(res) { | ||||
|                 el.wrapper.classList.add('loaded'); | ||||
|                 var isTotpEnabled = res.type === 'totp' | ||||
|                 return isTotpEnabled ? render_disable(res) : render_totp_setup(res); | ||||
| 
 | ||||
|                 var has_mfa = false; | ||||
|                 res.enabled_mfa.forEach(function(mfa) { | ||||
|                     if (mfa.type == "totp") { | ||||
|                         render_disable(mfa); | ||||
|                         has_mfa = true; | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 if (!has_mfa) | ||||
|                   render_totp_setup(res.new_mfa.totp); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| @ -174,9 +210,9 @@ | ||||
|         hide_error(); | ||||
| 
 | ||||
|         api( | ||||
|             '/mfa/totp/disable', | ||||
|             '/mfa/disable', | ||||
|             'POST', | ||||
|             {}, | ||||
|             { type: 'totp' }, | ||||
|             function() { | ||||
|                 do_logout(); | ||||
|             } | ||||
| @ -194,7 +230,8 @@ | ||||
|             'POST', | ||||
|             { | ||||
|                 token: $(el.totpSetupToken).val(), | ||||
|                 secret: $(el.totpSetupSecret).val() | ||||
|                 secret: $(el.totpSetupSecret).val(), | ||||
|                 label: $(el.totpSetupLabel).val() | ||||
|             }, | ||||
|             function(res) { | ||||
|                 do_logout(); | ||||
| @ -22,7 +22,7 @@ if [ ! -f $db_path ]; then | ||||
| 	echo Creating new user database: $db_path; | ||||
| 	echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path; | ||||
| 	echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path; | ||||
| 	echo "CREATE TABLE IF NOT EXISTS mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 $db_path; | ||||
| 	echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 $db_path; | ||||
| fi | ||||
| 
 | ||||
| # ### User Authentication | ||||
|  | ||||
| @ -184,7 +184,7 @@ def migration_12(env): | ||||
| def migration_13(env): | ||||
| 	# Add the "mfa" table for configuring MFA for login to the control panel. | ||||
| 	db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') | ||||
| 	shell("check_call", ["sqlite3", db, "CREATE TABLE IF NOT EXISTS mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);"]) | ||||
| 	shell("check_call", ["sqlite3", db, "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);"]) | ||||
| 
 | ||||
| ########################################################### | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user