fix(web): improve 2fa enrollment process (#1706)

* refactor(web): improve 2fa enrollment process

This PR will change some of the wording and colours for the 2FA processes in order to provide more clarity and address some accessibility issues for end users.

The following is a summary of the changes:

* One-Time Password ⭢ Time-based One-Time Password
* Security Key ⭢ Security Key - U2F

![Screenshot_2021-02-02-09-36-17](https://user-images.githubusercontent.com/3339418/107138185-17656100-6967-11eb-8fac-9e75c7a82d09.png)


* QRCode ⭢ QR Code

![Screenshot_2021-02-07-05-07-25](https://user-images.githubusercontent.com/3339418/107138196-29df9a80-6967-11eb-811f-d77c9bb0159e.png)

* `Not registered yet?` text to display `Lost device?` if a user has already registered a device of said type

![Screenshot_2021-02-02-10-24-54](https://user-images.githubusercontent.com/3339418/107138205-395ee380-6967-11eb-8826-83e1438dd146.png)

* Change button and text colour in e-mails that Authelia generates
* Change Authelia email footer to be more security conscious

![Screenshot_2021-02-07-04-51-40](https://user-images.githubusercontent.com/3339418/107138211-4085f180-6967-11eb-890b-9d931bd1ce76.png)

The docs have also been updated to clarify the 2fa device enrollment limitation which only allows users to register one of each device type concurrently.

Closes #1560.
pull/1717/head
Amir Zarrinkafsh 2021-02-12 16:59:42 +11:00 committed by GitHub
parent f188bfb1dc
commit 683c4a70bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 37 additions and 32 deletions

View File

@ -1,14 +1,14 @@
--- ---
layout: default layout: default
title: One-Time Password title: Time-based One-Time Password
parent: Configuration parent: Configuration
nav_order: 4 nav_order: 4
--- ---
# One-Time Password # Time-based One-Time Password
Authelia uses time based one-time passwords as the OTP method. You have Authelia uses time based one-time passwords as the OTP method. You have
the option to tune the settings of the TOTP generation and you can see a the option to tune the settings of the TOTP generation, and you can see a
full example of TOTP configuration below, as well as sections describing them. full example of TOTP configuration below, as well as sections describing them.
```yaml ```yaml

View File

@ -1,6 +1,6 @@
--- ---
layout: default layout: default
title: One-Time Password title: Time-based One-Time Password
nav_order: 1 nav_order: 1
parent: Second Factor parent: Second Factor
grand_parent: Features grand_parent: Features
@ -17,12 +17,11 @@ grand_parent: Features
After having successfully completed the first factor, select **One-Time Password method** After having successfully completed the first factor, select **One-Time Password method**
option and click on **Not registered yet?** link. This will send you an e-mail to confirm option and click on **Not registered yet?** link. This will e-mail you to confirm your identity.
your identity.
*NOTE: If you're testing **Authelia**, this e-mail has likely been sent to the mailbox available at https://mail.example.com:8080/* *NOTE: If you're testing **Authelia**, this e-mail has likely been sent to the mailbox available at https://mail.example.com:8080/*
Once this validation step is completed, a QRCode gets displayed. Once this validation step is completed, a QR Code gets displayed.
<p align="center"> <p align="center">
<img src="../../images/REGISTER-TOTP.png" width="400"> <img src="../../images/REGISTER-TOTP.png" width="400">
@ -34,5 +33,9 @@ From now on, you get tokens generated every 30 seconds that
you can use to validate the second factor in **Authelia**. you can use to validate the second factor in **Authelia**.
## Limitations
[Google Authenticator]: https://google-authenticator.com/ Users currently can only enroll a single TOTP device in **Authelia**.
Multiple single type device enrollment will be available when [this issue](https://github.com/authelia/authelia/issues/275) has been resolved.
[Google Authenticator]: https://google-authenticator.com/

View File

@ -56,6 +56,4 @@ Users must be enrolled via the Duo Admin panel, they cannot enroll a device from
It's likely that you have not configured **Authelia** correctly. Please read this It's likely that you have not configured **Authelia** correctly. Please read this
documentation again and be sure you had a look at [config.template.yml](https://github.com/authelia/authelia/blob/master/config.template.yml). documentation again and be sure you had a look at [config.template.yml](https://github.com/authelia/authelia/blob/master/config.template.yml).
[Duo]: https://duo.com/
[Duo]: https://duo.com/

View File

@ -44,6 +44,13 @@ by simply touching the token again when requested:
Easy, right?! Easy, right?!
## Limitations
Users currently can only enroll a single U2F device in **Authelia**.
Multiple single type device enrollment will be available when [this issue](https://github.com/authelia/authelia/issues/275) has been resolved.
## FAQ ## FAQ
### Why don't I have access to the *Security Key* option? ### Why don't I have access to the *Security Key* option?

View File

@ -6,6 +6,8 @@ import (
"time" "time"
"github.com/tebeka/selenium" "github.com/tebeka/selenium"
"github.com/authelia/authelia/internal/utils"
) )
type AvailableMethodsScenario struct { type AvailableMethodsScenario struct {
@ -48,16 +50,6 @@ func (s *AvailableMethodsScenario) SetupTest() {
s.verifyIsHome(ctx, s.T()) s.verifyIsHome(ctx, s.T())
} }
func IsStringInList(str string, list []string) bool {
for _, v := range list {
if v == str {
return true
}
}
return false
}
func (s *AvailableMethodsScenario) TestShouldCheckAvailableMethods() { func (s *AvailableMethodsScenario) TestShouldCheckAvailableMethods() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel() defer cancel()
@ -85,6 +77,6 @@ func (s *AvailableMethodsScenario) TestShouldCheckAvailableMethods() {
s.Assert().Len(optionsList, len(s.methods)) s.Assert().Len(optionsList, len(s.methods))
for _, m := range s.methods { for _, m := range s.methods {
s.Assert().True(IsStringInList(m, optionsList)) s.Assert().True(utils.IsStringInSlice(m, optionsList))
} }
} }

View File

@ -135,7 +135,7 @@ func (s *DuoPushSuite) TestDuoPushRedirectionURLSuite() {
func (s *DuoPushSuite) TestAvailableMethodsScenario() { func (s *DuoPushSuite) TestAvailableMethodsScenario() {
suite.Run(s.T(), NewAvailableMethodsScenario([]string{ suite.Run(s.T(), NewAvailableMethodsScenario([]string{
"ONE-TIME PASSWORD", "TIME-BASED ONE-TIME PASSWORD",
"PUSH NOTIFICATION", "PUSH NOTIFICATION",
})) }))
} }

View File

@ -180,7 +180,7 @@ func (s *StandaloneSuite) TestResetPasswordScenario() {
} }
func (s *StandaloneSuite) TestAvailableMethodsScenario() { func (s *StandaloneSuite) TestAvailableMethodsScenario() {
suite.Run(s.T(), NewAvailableMethodsScenario([]string{"ONE-TIME PASSWORD"})) suite.Run(s.T(), NewAvailableMethodsScenario([]string{"TIME-BASED ONE-TIME PASSWORD"}))
} }
func (s *StandaloneSuite) TestRedirectionURLScenario() { func (s *StandaloneSuite) TestRedirectionURLScenario() {

View File

@ -93,7 +93,7 @@ const emailHTMLContent = `
} }
a { a {
color: #0a8cce; color: #ffffff;
text-decoration: none; text-decoration: none;
text-decoration: none !important; text-decoration: none !important;
} }
@ -105,7 +105,7 @@ const emailHTMLContent = `
.button { .button {
padding: 15px 30px; padding: 15px 30px;
border-radius: 10px; border-radius: 10px;
background: rgb(204, 204, 255); background: rgb(25, 118, 210);
text-decoration: none; text-decoration: none;
} }
@ -395,7 +395,7 @@ const emailHTMLContent = `
<td align="center" valign="middle" <td align="center" valign="middle"
style="font-family: Helvetica, arial, sans-serif; font-size: 14px;color: #666666" style="font-family: Helvetica, arial, sans-serif; font-size: 14px;color: #666666"
st-content="postfooter"> st-content="postfooter">
Please ignore this email if you did not initiate the process. Please contact an administrator if you did not initiate the process.
</td> </td>
</tr> </tr>
<!-- Spacing --> <!-- Spacing -->

View File

@ -22,5 +22,5 @@ If you did not initiate the process your credentials might have been compromised
To setup your 2FA please visit the following URL: {{.url}} To setup your 2FA please visit the following URL: {{.url}}
Please ignore this email if you did not initiate the process. Please contact an administrator if you did not initiate the process.
` `

View File

@ -74,7 +74,7 @@ const RegisterOneTimePassword = function () {
const qrcodeFuzzyStyle = isLoading || hasErrored ? style.fuzzy : undefined; const qrcodeFuzzyStyle = isLoading || hasErrored ? style.fuzzy : undefined;
return ( return (
<LoginLayout title="Scan QRCode"> <LoginLayout title="Scan QR Code">
<div className={style.root}> <div className={style.root}>
<div className={style.googleAuthenticator}> <div className={style.googleAuthenticator}>
<Typography className={style.googleAuthenticatorText}>Need Google Authenticator?</Typography> <Typography className={style.googleAuthenticatorText}>Need Google Authenticator?</Typography>

View File

@ -15,6 +15,7 @@ export enum State {
export interface Props { export interface Props {
id: string; id: string;
title: string; title: string;
registered: boolean;
explanation: string; explanation: string;
state: State; state: State;
children: ReactNode; children: ReactNode;
@ -24,6 +25,7 @@ export interface Props {
const DefaultMethodContainer = function (props: Props) { const DefaultMethodContainer = function (props: Props) {
const style = useStyles(); const style = useStyles();
const registerMessage = props.registered ? "Lost your device?" : "Not registered yet?";
let container: ReactNode; let container: ReactNode;
let stateClass: string = ""; let stateClass: string = "";
@ -50,7 +52,7 @@ const DefaultMethodContainer = function (props: Props) {
</div> </div>
{props.onRegisterClick ? ( {props.onRegisterClick ? (
<Link component="button" id="register-link" onClick={props.onRegisterClick}> <Link component="button" id="register-link" onClick={props.onRegisterClick}>
Not registered yet? {registerMessage}
</Link> </Link>
) : null} ) : null}
</div> </div>

View File

@ -40,7 +40,7 @@ const MethodSelectionDialog = function (props: Props) {
{props.methods.has(SecondFactorMethod.TOTP) ? ( {props.methods.has(SecondFactorMethod.TOTP) ? (
<MethodItem <MethodItem
id="one-time-password-option" id="one-time-password-option"
method="One-Time Password" method="Time-based One-Time Password"
icon={pieChartIcon} icon={pieChartIcon}
onClick={() => props.onClick(SecondFactorMethod.TOTP)} onClick={() => props.onClick(SecondFactorMethod.TOTP)}
/> />
@ -48,7 +48,7 @@ const MethodSelectionDialog = function (props: Props) {
{props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported ? ( {props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported ? (
<MethodItem <MethodItem
id="security-key-option" id="security-key-option"
method="Security Key" method="Security Key - U2F"
icon={<FingerTouchIcon size={32} />} icon={<FingerTouchIcon size={32} />}
onClick={() => props.onClick(SecondFactorMethod.U2F)} onClick={() => props.onClick(SecondFactorMethod.U2F)}
/> />

View File

@ -84,6 +84,7 @@ const OneTimePasswordMethod = function (props: Props) {
id={props.id} id={props.id}
title="One-Time Password" title="One-Time Password"
explanation="Enter one-time password" explanation="Enter one-time password"
registered={props.registered}
state={methodState} state={methodState}
onRegisterClick={props.onRegisterClick} onRegisterClick={props.onRegisterClick}
> >

View File

@ -98,6 +98,7 @@ const PushNotificationMethod = function (props: Props) {
id={props.id} id={props.id}
title="Push Notification" title="Push Notification"
explanation="A notification has been sent to your smartphone" explanation="A notification has been sent to your smartphone"
registered={true}
state={methodState} state={methodState}
> >
<div className={style.icon}>{icon}</div> <div className={style.icon}>{icon}</div>

View File

@ -107,6 +107,7 @@ const SecurityKeyMethod = function (props: Props) {
id={props.id} id={props.id}
title="Security Key" title="Security Key"
explanation="Touch the token of your security key" explanation="Touch the token of your security key"
registered={props.registered}
state={methodState} state={methodState}
onRegisterClick={props.onRegisterClick} onRegisterClick={props.onRegisterClick}
> >