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
parent
f188bfb1dc
commit
683c4a70bf
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
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/
|
[Google Authenticator]: https://google-authenticator.com/
|
|
@ -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/
|
|
@ -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?
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
|
@ -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.
|
||||||
`
|
`
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in New Issue