Merge branch 'fix-webauthn-1411' into v2.0

This commit is contained in:
Maxime Besson 2022-02-16 10:38:09 +01:00
commit af7abe8d19
86 changed files with 3402 additions and 186 deletions

27
COPYING
View File

@ -39,6 +39,10 @@ Copyright: 2005-2019, Xavier Guimard <yadd@debian.org>
2018-2019, Christophe Maudoux <chrmdx@gmail.com>
License: GPL-2+
Files: lemonldap-ng-portal/site/htdocs/static/bootstrap/webauthn.png
Copyright: James Cullum <https://github.com/JamesCullum>
License: WebAuthnLogoLicense
Files: lemonldap-ng-portal/site/htdocs/static/common/js/portal.js
Copyright: 2005-2019, Xavier Guimard <yadd@debian.org>
2006-2019, Clement Oudot <clem.oudot@gmail.com>
@ -1268,3 +1272,26 @@ License: BSD-3-clause
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License: WebAuthnLogoLicense
How to Use These Logos
.
Do these awesome things:
.
* Use the WebAuthn logo to link to WebAuthn specs or webauthn.org
* Use the WebAuthn logo to show that your product or project has built-in WebAuthn integration
* Use the WebAuthn logo in a blog post or news article about WebAuthn
.
Please don't do these things:
.
x Use the WebAuthn logo for your applications icon
x Create a modified version of the WebAuthn logo
x Integrate the WebAuthn logo into your logo
x Use any WebAuthn artwork without permission
x Sell any WebAuthn artwork without permission
x Change the colors, dimensions or add your own text/images
.
Please contact me
.
* If you want to use artwork not included in this repository
* If you want to use these images in a video/mainstream media

27
debian/copyright vendored
View File

@ -39,6 +39,10 @@ Copyright: 2005-2019, Xavier Guimard <yadd@debian.org>
2018-2019, Christophe Maudoux <chrmdx@gmail.com>
License: GPL-2+
Files: lemonldap-ng-portal/site/htdocs/static/bootstrap/webauthn.png
Copyright: James Cullum <https://github.com/JamesCullum>
License: WebAuthnLogoLicense
Files: lemonldap-ng-portal/site/htdocs/static/common/js/portal.js
Copyright: 2005-2019, Xavier Guimard <yadd@debian.org>
2006-2019, Clement Oudot <clem.oudot@gmail.com>
@ -1271,3 +1275,26 @@ License: BSD-3-clause
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License: WebAuthnLogoLicense
How to Use These Logos
.
Do these awesome things:
.
* Use the WebAuthn logo to link to WebAuthn specs or webauthn.org
* Use the WebAuthn logo to show that your product or project has built-in WebAuthn integration
* Use the WebAuthn logo in a blog post or news article about WebAuthn
.
Please don't do these things:
.
x Use the WebAuthn logo for your applications icon
x Create a modified version of the WebAuthn logo
x Integrate the WebAuthn logo into your logo
x Use any WebAuthn artwork without permission
x Sell any WebAuthn artwork without permission
x Change the colors, dimensions or add your own text/images
.
Please contact me
.
* If you want to use artwork not included in this repository
* If you want to use these images in a video/mainstream media

View File

@ -1269,7 +1269,7 @@
"type" : {
"type" : "string",
"description" : "The type of token in use",
"example" : "TOTP, U2F, UBK (Yubikey)"
"example" : "TOTP, U2F, UBK (Yubikey), WebAuthn"
},
"name" : {
"type" : "string",

View File

@ -41,4 +41,5 @@ Authentication, users and password databases
radius2f
rest2f
yubikey2f
webauthn2f
sfextra

View File

@ -23,6 +23,7 @@ complete authentication module with 2FA :
- :doc:`External 2F<external2f>` *(to call an external command)*
- :doc:`REST<rest2f>` *(Remote REST app)*
- :doc:`RADIUS<radius2f>` *(Remote RADIUS server)*
- :doc:`WebAuthn<webauthn2f>` *(Web Authentication API)*
The E-Mail, External and REST 2F modules
:doc:`may be declared multiple times<sfextra>` with different sets of

View File

@ -187,6 +187,7 @@ Second factor (:doc:`documentation<secondfactor>`) Authenticat
:doc:`Radius Second Factor<radius2f>` |new| [3]_
:doc:`REST Second Factor<rest2f>` |new| ✔
:doc:`Yubikey<yubikey2f>` |new| ✔
:doc:`WebAuthn<webauthn2f>` |new| ✔
:doc:`Additional second factors<sfextra>` |new| [4]_
==================================================================== ==============

View File

@ -34,6 +34,16 @@ Security
* **CVE-2021-40874**: RESTServer pwdConfirm always returns true with Combination + Kerberos (see `issue 2612 <https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2612>`__)
U2F deprecation in Chrome 98
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Chrome 98 `removed U2F support by default <https://developer.chrome.com/blog/deps-rems-95/#deprecate-u2f-api-cryptotoken>`__. You can enable them back temporarily in ``chrome://flags`` by setting *Enable the U2F Security Key API* to *Enabled* and *Enable a permission prompt for the U2F Security Key API* to *Disabled*
LemonLDAP::NG provides a newer alternative: :doc:`webauthn2f`, which is compatible with U2F security keys. Please read :ref:`migrateu2ftowebauthn` for instructions on how to convert U2F secrets to WebAuthn.
After migration, you will need to disable U2F from the configuration and enable WebAuthn instead, in *General Parameters* » *Second Factors* » *WebAuthn*
Weak encryption used for password-protected SAML keys
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1,75 @@
WebAuthn as a second factor
===========================
`Web Authentication <https://www.w3.org/TR/webauthn/>`__ , shortened as WebAuthn, is a standard method by which a web browser can authenticate to an application (*Relying Party*, in our case, this is LemonLDAP::NG) through the use of an *Authenticator*, which can be a hardware token (USB, NFC...) or provided by the user's device itself (TPM).
.. versionadded:: 2.0.14
Currently, we only implement WebAuthn as a second factor. Passwordless,
first-factor authentication will be added in a later release.
Implementation status
~~~~~~~~~~~~~~~~~~~~~
Currently, we implement:
* Device registration without attestation validation (attestation type: *None*)
* Authentication as a second factor with the registered device
Requirements
~~~~~~~~~~~~
You need to install the `Authen::WebAuthn` CPAN module for WebAuthn to work on
your LemonLDAP::NG installation. If there is no package for it in your
distribution, you can install it with:
```
cpanm Authen::WebAuthn
```
Configuration
~~~~~~~~~~~~~
- **Activation**: set it to "on"
- **User verification**: Whether or not LemonLDAP::NG requires the user to
authenticate to their second factor device. Usually by entering a PIN code.
*Warning*: The *Required* option is not supported by older U2F security keys.
- **Self registration**: set it to "on" if users are authorized to
register their keys
- **Relying Party display name**: How the LemonLDAP::NG server will appear in
the web browser messages displayed to the user
- **Allow users to remove WebAuthn**: If enabled, users can unregister their WebAuthn device.
- **Authentication level**: you can overwrite here auth level for
WebAuthn registered users. Leave it blank keeps auth level provided by
first authentication module *(default: 2 for user/password based
modules)*. **It is recommended to set an higher value here if you
want to give access to some apps only for enrolled users**
- **Label** (Optional): label that should be displayed to the user on
the choice screen
- **Logo** (Optional): logo file *(in static/<skin> directory)*
.. _migrateu2ftowebauthn:
Migrating existing U2F devices
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
WebAuthn is compatible with both FIDO and FIDO2 standards. Which means this
module lets you use any U2F-compatible device you already own.
You can use the ``lemonldap-ng-sessions`` tool to migrate existing U2F devices to the WebAuthn plugin ::
# For one user
lemonldap-ng-sessions secondfactors migrateu2f dwho
# For all users
lemonldap-ng-sessions secondfactors migrateu2f --all
Once you are satisfied with WebAuthn, you can remove existing U2F devices and
disable the U2F second factor module ::
# For one user
lemonldap-ng-sessions secondfactors delType dwho U2F
# For all users
lemonldap-ng-sessions secondfactors delType --all U2F

View File

@ -1417,7 +1417,7 @@ components:
type:
type: string
description: "The type of token in use"
example: "TOTP, U2F, UBK (Yubikey)"
example: "TOTP, U2F, UBK (Yubikey), WebAuthn"
name:
type: string
description: "A user-set description of the token"

View File

@ -84,7 +84,6 @@ sub _search {
}
return $res;
}
sub search {
@ -264,12 +263,86 @@ sub _del_psession_special {
if ($deleted) {
$data->{$specialKeyName} = to_json( [@new] );
}
# TODO should this be in the if???
$psession->update($data);
}
sub _migrateu2f_device {
my ( $self, $device ) = @_;
my $credential_id = $device->{_keyHandle};
my $_userKey = $device->{_userKey};
eval { require Authen::WebAuthn };
if ($@) {
die "Missing Authen::WebAuthn dependency: $@";
}
my $credential_pubkey =
Authen::WebAuthn::convert_raw_ecc_to_cose($_userKey);
return {
type => "WebAuthn",
name => "$device->{name}",
_credentialId => "$credential_id",
_credentialPublicKey => "$credential_pubkey",
_signCount => 0,
epoch => "$device->{epoch}",
};
}
sub _migrateu2f {
my $self = shift;
my $target = shift;
my $psession = $self->_get_psession($target);
my $data = $psession->data;
my $migrated = 0;
my $_2fDevices = $data->{_2fDevices} || "[]";
$_2fDevices = from_json($_2fDevices);
die "Expecting JSON array in _2fDevices"
unless ref($_2fDevices) eq "ARRAY";
my @new_2fDevices = @{$_2fDevices};
my @u2f_devices = grep { $_->{type} eq "U2F" } @{$_2fDevices};
my %migrated_devices;
for my $u2f_device (@u2f_devices) {
my $migrated_device = $self->_migrateu2f_device($u2f_device);
$migrated_devices{ $migrated_device->{_credentialId} } =
$migrated_device;
}
for my $migrated_device ( keys %migrated_devices ) {
# If credentialId is not already present
unless (
grep {
$_->{type} eq "WebAuthn"
and $_->{_credentialId} eq $migrated_device
} @new_2fDevices
)
{
push @new_2fDevices, $migrated_devices{$migrated_device};
$migrated = 1;
}
}
if ($migrated) {
$data->{_2fDevices} = to_json( [@new_2fDevices] );
$psession->update($data);
}
}
sub consents_get {
my $self = shift;
my $target = shift;
my $self = shift;
my $target = shift;
return 0 unless $target;
my $o = $self->stdout;
my $consents = $self->_get_psession_special( $target, '_oidcConsents',
sub { $_[0]->{rp} } );
@ -278,8 +351,10 @@ sub consents_get {
}
sub secondfactors_get {
my $self = shift;
my $target = shift;
my $self = shift;
my $target = shift;
return 0 unless $target;
my $o = $self->stdout;
my $consents = $self->_get_psession_special( $target, '_2fDevices',
sub { genId2F( $_[0] ) } );
@ -290,8 +365,11 @@ sub secondfactors_get {
sub consents_delete {
my $self = shift;
my $target = shift;
my @ids = @_;
return unless @ids;
return 0 unless $target;
my @ids = @_;
return 0 unless @ids;
$self->_del_psession_special( $target, '_oidcConsents',
sub { $_[0]->{rp} }, @ids );
return 0;
@ -300,23 +378,66 @@ sub consents_delete {
sub secondfactors_delete {
my $self = shift;
my $target = shift;
my @ids = @_;
return unless @ids;
return 0 unless $target;
my @ids = @_;
return 0 unless @ids;
$self->_del_psession_special( $target, '_2fDevices',
sub { genId2F( $_[0] ) }, @ids );
return 0;
}
sub _get_psession_targets {
my ( $self, @args ) = @_;
if ( $self->opts->{where} or $self->opts->{all} ) {
$self->opts->{persistent} = 1;
if ( $self->opts->{all} ) {
delete $self->opts->{where};
}
my $res = $self->_search();
return ( map { $res->{$_}->{_session_uid} } keys %{$res} );
}
else {
return @args;
}
}
sub secondfactors_delType {
my $self = shift;
my $target = shift;
my @types = @_;
return unless @types;
$self->_del_psession_special( $target, '_2fDevices', sub { $_[0]->{type} },
@types );
my $self = shift;
my $target;
unless ( $self->opts->{where} or $self->opts->{all} ) {
$target = shift;
}
my @types = @_;
return 0 unless @types;
my @targets = $self->_get_psession_targets($target);
for my $target (@targets) {
$self->_del_psession_special( $target, '_2fDevices',
sub { $_[0]->{type} }, @types );
}
return 0;
}
sub secondfactors_migrateu2f {
my ( $self, @ids ) = @_;
my $result = 0;
my @sessions = $self->_get_psession_targets(@ids);
for my $id (@sessions) {
if ( !$self->_migrateu2f($id) ) {
$result = 1;
}
}
return $result;
}
sub setKey {
my $self = shift;
my $id = shift;
@ -380,8 +501,8 @@ sub run {
# Subcommands and target
elsif ( $action =~ /^(?:secondfactors|consents)$/ ) {
my $subcommand = shift;
unless ( $subcommand and @_ ) {
die "Missing subcommand and target for $action";
unless ($subcommand) {
die "Missing subcommand $action";
}
my $func = "${action}_${subcommand}";
if ( $self->can($func) ) {

View File

@ -31,7 +31,7 @@ use constant DEFAULTCONFBACKENDOPTIONS => (
);
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|(?:(?:d(?:emo|bi)|webID)ExportedVa|exported(?:Heade|Va)|issuerDBGetParamete)r|f(?:indUser(?:Exclud|Search)ingAttribute|acebookExportedVar)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|macro)s|o(?:idc(?:S(?:ervice(?:DynamicRegistrationEx(?:portedVar|traClaim)s|MetaDataAuthnContext)|torageOptions)|RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar|ScopeRule|Macro)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node))|penIdExportedVars)|c(?:as(?:A(?:ppMetaData(?:(?:ExportedVar|Option|Macro)s|Node)|ttributes)|S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions))|(?:ustom(?:Plugins|Add)Param|heckUserHiddenHeader|ombModule)s)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option|Macro)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars|fExtra)|a(?:(?:daptativeAuthenticationLevelR|ut(?:hChoiceMod|oSigninR))ules|pplicationList)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|v(?:hostOptions|irtualHost)|S(?:MTPTLSOpts|SLVarIf))$/;
our $arrayParameters = qr/^mySessionAuthorizedRWKeys$/;
our $boolKeys = qr/^(?:s(?:aml(?:IDP(?:MetaDataOptions(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|IsPassiv)e|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Force(?:Authn|UTF8)|StoreSAMLToken|RelayStateURL)|SSODescriptorWantAuthnRequestsSigned)|S(?:P(?:MetaDataOptions(?:(?:CheckS[LS]OMessageSignatur|OneTimeUs)e|EnableIDPInitiatedURL|ForceUTF8)|SSODescriptor(?:WantAssertion|AuthnRequest)sSigned)|erviceUseCertificateInResponse)|DiscoveryProtocol(?:Activation|IsPassive)|CommonDomainCookieActivation|UseQueryStringSpecific|MetadataForceUTF8)|t(?:ayConnectedBypassFG|orePassword)|f(?:RemovedUseNotif|OnlyUpgrade)|kip(?:Upgrade|Renew)Confirmation|oap(?:Session|Config)Server|laveDisplayLogo|howLanguages|slByAjax)|o(?:idc(?:RPMetaDataOptions(?:A(?:llow(?:(?:ClientCredentials|Password)Grant|Offline)|ccessToken(?:Claims|JWT))|Re(?:freshToken|quirePKCE)|LogoutSessionRequired|IDTokenForceClaims|BypassConsent|Public)|ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration|OnlyDeclaredScopes)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:sS(?:rvMetaDataOptions(?:Gateway|Renew)|trictMatching)|ptcha_(?:register|login|mail)_enabled)|heck(?:DevOps(?:D(?:isplayNormalizedHeaders|ownload)|CheckSessionAttributes)?|State|User|XSS)|o(?:ntextSwitching(?:Allowed2fModifications|StopWithLogout)|mpactConf|rsEnabled)|rowdsec|da)|p(?:ortal(?:Display(?:Re(?:freshMyRights|setPassword|gister)|CertificateResetByMail|GeneratePassword|PasswordPolicy)|E(?:rrorOn(?:ExpiredSession|MailNotFound)|nablePasswordDisplay)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|ForceAuthn|AntiFrame)|roxy(?:AuthServiceImpersonation|UseSoap))|l(?:dap(?:(?:G(?:roup(?:DecodeSearchedValu|Recursiv)|etUserBeforePasswordChang)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl|ITDS)|oginHistoryEnabled)|n(?:o(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?|sExplorer)?|y(?:Deleted|Other))|AjaxHook)|ewLocationWarning)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|re(?:st(?:(?:Password|Session|Config|Auth)Server|ExportSecretKeys)|freshSessions)|br(?:uteForceProtection(?:IncrementalTempo)?|owsersDontStorePassword)|d(?:is(?:ablePersistentStorage|playSessionId)|biDynamicHashEnabled)|to(?:tp2f(?:UserCanRemoveKey|EncryptSecret)|kenUseGlobalStorage)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|g(?:roupsBeforeMacros|lobalLogoutTimer)|a(?:voidAssignment|ctiveTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|(?:wsdlServ|findUs)er)$/;
our $boolKeys = qr/^(?:s(?:aml(?:IDP(?:MetaDataOptions(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|IsPassiv)e|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Force(?:Authn|UTF8)|StoreSAMLToken|RelayStateURL)|SSODescriptorWantAuthnRequestsSigned)|S(?:P(?:MetaDataOptions(?:(?:CheckS[LS]OMessageSignatur|OneTimeUs)e|EnableIDPInitiatedURL|ForceUTF8)|SSODescriptor(?:WantAssertion|AuthnRequest)sSigned)|erviceUseCertificateInResponse)|DiscoveryProtocol(?:Activation|IsPassive)|CommonDomainCookieActivation|UseQueryStringSpecific|MetadataForceUTF8)|t(?:ayConnectedBypassFG|orePassword)|f(?:RemovedUseNotif|OnlyUpgrade)|kip(?:Upgrade|Renew)Confirmation|oap(?:Session|Config)Server|laveDisplayLogo|howLanguages|slByAjax)|o(?:idc(?:RPMetaDataOptions(?:A(?:llow(?:(?:ClientCredentials|Password)Grant|Offline)|ccessToken(?:Claims|JWT))|Re(?:freshToken|quirePKCE)|LogoutSessionRequired|IDTokenForceClaims|BypassConsent|Public)|ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration|OnlyDeclaredScopes)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:sS(?:rvMetaDataOptions(?:Gateway|Renew)|trictMatching)|ptcha_(?:register|login|mail)_enabled)|heck(?:DevOps(?:D(?:isplayNormalizedHeaders|ownload)|CheckSessionAttributes)?|State|User|XSS)|o(?:ntextSwitching(?:Allowed2fModifications|StopWithLogout)|mpactConf|rsEnabled)|rowdsec|da)|p(?:ortal(?:Display(?:Re(?:freshMyRights|setPassword|gister)|CertificateResetByMail|GeneratePassword|PasswordPolicy)|E(?:rrorOn(?:ExpiredSession|MailNotFound)|nablePasswordDisplay)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|ForceAuthn|AntiFrame)|roxy(?:AuthServiceImpersonation|UseSoap))|l(?:dap(?:(?:G(?:roup(?:DecodeSearchedValu|Recursiv)|etUserBeforePasswordChang)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl|ITDS)|oginHistoryEnabled)|n(?:o(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?|sExplorer)?|y(?:Deleted|Other))|AjaxHook)|ewLocationWarning)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|re(?:st(?:(?:Password|Session|Config|Auth)Server|ExportSecretKeys)|freshSessions)|br(?:uteForceProtection(?:IncrementalTempo)?|owsersDontStorePassword)|d(?:is(?:ablePersistentStorage|playSessionId)|biDynamicHashEnabled)|to(?:tp2f(?:UserCanRemoveKey|EncryptSecret)|kenUseGlobalStorage)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|w(?:ebauthn2fUserCanRemoveKey|sdlServer)|g(?:roupsBeforeMacros|lobalLogoutTimer)|a(?:voidAssignment|ctiveTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|findUser)$/;
our @sessionTypes = ( 'remoteGlobal', 'global', 'localSession', 'persistent', 'saml', 'oidc', 'cas' );

View File

@ -17,8 +17,9 @@ sub defaultValues {
},
'authChoiceParam' => 'lmAuth',
'authentication' => 'Demo',
'available2F' => 'UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,Yubikey,Radius',
'available2FSelfRegistration' => 'TOTP,U2F,Yubikey',
'available2F' =>
'UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius',
'available2FSelfRegistration' => 'TOTP,U2F,WebAuthn,Yubikey',
'bruteForceProtectionLockTimes' => '15, 30, 60, 300, 600',
'bruteForceProtectionMaxAge' => 300,
'bruteForceProtectionMaxFailed' => 3,
@ -385,14 +386,18 @@ sub defaultValues {
'useRedirectOnError' => 1,
'useSafeJail' => 1,
'utotp2fActivation' => 0,
'viewerHiddenKeys' => 'samlIDPMetaDataNodes, samlSPMetaDataNodes',
'webIDAuthnLevel' => 1,
'webIDExportedVars' => {},
'whatToTrace' => 'uid',
'yubikey2fActivation' => 0,
'yubikey2fPublicIDSize' => 12,
'yubikey2fSelfRegistration' => 0,
'yubikey2fUserCanRemoveKey' => 1
'viewerHiddenKeys' => 'samlIDPMetaDataNodes, samlSPMetaDataNodes',
'webauthn2fActivation' => 0,
'webauthn2fSelfRegistration' => 0,
'webauthn2fUserCanRemoveKey' => 1,
'webauthn2fUserVerification' => 'preferred',
'webIDAuthnLevel' => 1,
'webIDExportedVars' => {},
'whatToTrace' => 'uid',
'yubikey2fActivation' => 0,
'yubikey2fPublicIDSize' => 12,
'yubikey2fSelfRegistration' => 0,
'yubikey2fUserCanRemoveKey' => 1
};
}

View File

@ -22,6 +22,7 @@ GetOptions(
'help|h' => \$help,
'select|s=s@' => \$opts->{select},
'where|w=s' => \$opts->{where},
'all|a' => \$opts->{all},
'backend|b=s' => \$opts->{backend},
'persistent|p' => \$opts->{persistent},
'id-only|i' => \$opts->{idonly},
@ -80,7 +81,7 @@ if ( $action eq "setKey" ) {
}
if ( $action eq "secondfactors" ) {
unless ( @ARGV >= 2 ) {
unless ( @ARGV >= 1 ) {
pod2usage(
-exitval => 1,
-verbose => 99,
@ -238,8 +239,10 @@ Commands:
delete <user> <id> [<id> ...]
delete second factors for a user. The ID must match one of the
IDs returned by the "show" command.
delType <user> <type> [<type> ...]
delType [<user>|--all] <type> [<type> ...]
delete all second factors of a given type for a user
migrateu2f [<user>|--all]
migrate U2F device registrations to WebAuthn device registrations
=head2 Consents

View File

@ -62,97 +62,115 @@ my @psessionsOpts = (
force => 1,
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "1b3231655cebb7a1f783eddf27d254ca",
info => {
"uid" => "rtyler",
sub resetSessions {
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "1b3231655cebb7a1f783eddf27d254ca",
info => {
"uid" => "rtyler",
}
}
}
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "9684dd2a6489bf2be2fbdd799a8028e3",
info => {
"uid" => "dwho",
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "9684dd2a6489bf2be2fbdd799a8028e3",
info => {
"uid" => "dwho",
}
}
}
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "f90f597566f5cce47d9641377776c0c2",
info => {
"uid" => "dwho",
"deleteme" => 1,
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "f90f597566f5cce47d9641377776c0c2",
info => {
"uid" => "dwho",
"deleteme" => 1,
}
}
}
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "1234",
info => {
"uid" => "foo",
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "1234",
info => {
"uid" => "foo",
}
}
}
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "1235",
info => {
"uid" => "foo",
);
Lemonldap::NG::Common::Session->new( {
@sessionsOpts,
id => "1235",
info => {
"uid" => "foo",
}
}
}
);
);
Lemonldap::NG::Common::Session->new( {
@psessionsOpts,
id => "5efe8af397fc3577e05b483aca964f1b",
force => 1,
info => {
"_2fDevices" => to_json( [ {
'type' => 'UBK',
'epoch' => 1588691690,
'_yubikey' => 'cccccceijfnf',
'name' => 'Imported automatically'
},
{
'name' => 'MyU2F',
'type' => 'U2F',
'epoch' => 1588691728
},
{
'_secret' => 'mnxkiirpswuojr47kkrty7ax34fy2ix7',
'name' => 'MyTOTP',
'type' => 'TOTP',
'epoch' => 1588691728
}
]
),
"_oidcConsents" => to_json( [ {
'scope' => 'openid email',
'rp' => 'rp-example',
'epoch' => 1589288341
},
{
'scope' => 'openid email',
'epoch' => 1589291482,
'rp' => 'rp-example2'
}
]
),
"_session_uid" => "dwho",
Lemonldap::NG::Common::Session->new( {
@psessionsOpts,
id => "5efe8af397fc3577e05b483aca964f1b",
force => 1,
info => {
"_2fDevices" => to_json( [ {
'type' => 'UBK',
'epoch' => 1588691690,
'_yubikey' => 'cccccceijfnf',
'name' => 'Imported automatically'
},
{
'name' => 'MyU2F',
'type' => 'U2F',
'epoch' => 1588691728
},
{
'_secret' => 'mnxkiirpswuojr47kkrty7ax34fy2ix7',
'name' => 'MyTOTP',
'type' => 'TOTP',
'epoch' => 1588691728
}
]
),
"_oidcConsents" => to_json( [ {
'scope' => 'openid email',
'rp' => 'rp-example',
'epoch' => 1589288341
},
{
'scope' => 'openid email',
'epoch' => 1589291482,
'rp' => 'rp-example2'
}
]
),
"_session_uid" => "dwho",
}
}
}
);
Lemonldap::NG::Common::Session->new( {
@psessionsOpts,
id => "8d3bc3b0e14ea2a155f275aa7c07ebee",
force => 1,
info => {
"_session_uid" => "rtyler",
);
Lemonldap::NG::Common::Session->new( {
@psessionsOpts,
id => "8d3bc3b0e14ea2a155f275aa7c07ebee",
force => 1,
info => {
"_session_uid" => "rtyler",
"_2fDevices" => to_json( [ {
'type' => 'UBK',
'epoch' => 1588691690,
'_yubikey' => 'cccccceijfnf',
'name' => 'Imported automatically'
},
{
'_secret' => 'mnxkiirpswuojr47kkrty7ax34fy2ix7',
'name' => 'MyTOTP',
'type' => 'TOTP',
'epoch' => 1588691728
}
]
),
}
}
}
);
);
}
resetSessions;
sub getJson {
my @args = @_;
@ -301,6 +319,27 @@ is( ( keys %{$res} ), 1, "Found one second factors" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ), 0, "U2F was removed" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 1, "TOTP survived" );
# Delete 2FA by type (with search)
resetSessions;
$cli->run( "secondfactors", { where => "_session_uid=dwho" }, "delType",
"U2F" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( ( keys %{$res} ), 2, "Found one second factors" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ), 0, "U2F was removed" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 1, "TOTP survived" );
# Delete 2FA by type (with all)
resetSessions;
$cli->run( "secondfactors", { all => 1 }, "delType", "TOTP" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( ( keys %{$res} ), 2, "Found two second factors for dwho" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 0, "TOTP was removed" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ), 1, "UBK survived" );
$res = getJson( "secondfactors", {}, "get", "rtyler" );
is( ( keys %{$res} ), 1, "Found one second factors for rtyler" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 0, "TOTP was removed" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ), 1, "UBK survived" );
# Show consents
$res = getJson( "consents", {}, "get", "dwho" );
is( ( keys %{$res} ), 2, "Found two consents" );

View File

@ -0,0 +1,228 @@
# Before `make install' is performed this script should be runnable with
# `make test'. After `make install' it should work as `perl Lemonldap-NG-Manager.t'
#########################
# change 'tests => 1' to 'tests => last_test_to_print';
use Test::More;
use Test::Output;
use File::Path;
use JSON;
BEGIN {
use_ok('Lemonldap::NG::Common::Session');
use_ok('Lemonldap::NG::Common::CliSessions');
}
#########################
SKIP: {
eval "use Authen::WebAuthn::Test; use Authen::WebAuthn;";
if ($@) {
skip 'Authen::WebAuthn not found';
}
my $dir;
my $cli;
sub setup_sessions {
use File::Temp;
$dir = File::Temp::tempdir();
my $sessionsdir = "$dir/sessions";
my $psessionsdir = "$dir/psessions";
mkdir $sessionsdir;
mkdir $psessionsdir;
$cli = Lemonldap::NG::Common::CliSessions->new(
conf => {
globalStorage => "Apache::Session::File",
globalStorageOptions => {
Directory => $sessionsdir,
LockDirectory => $sessionsdir,
},
persistentStorage => "Apache::Session::File",
persistentStorageOptions => {
Directory => $psessionsdir,
LockDirectory => $psessionsdir,
},
}
);
# Provision test sessions
my @psessionsOpts = (
storageModule => "Apache::Session::File",
storageModuleOptions => {
Directory => $psessionsdir,
LockDirectory => $psessionsdir,
},
kind => 'Persistent',
force => 1,
);
#dwho
Lemonldap::NG::Common::Session->new( {
@psessionsOpts,
id => "5efe8af397fc3577e05b483aca964f1b",
force => 1,
info => {
"_2fDevices" => to_json( [ {
'type' => 'UBK',
'epoch' => 1588691690,
'_yubikey' => 'cccccceijfnf',
'name' => 'Imported automatically'
},
{
'name' => 'U2F-1',
'type' => 'U2F',
'epoch' => 1588691728,
'_keyHandle' =>
'4aS6vXlFQpG5XZSoad6auM9fFu7Q1wazQYwfPtPKN_Hll6Up_ceeWkOgqxm49swWq4Vvcg5UlX0sQQhuRe8heA',
'_userKey' =>
'BMgMqKPL2PhsjCNW78UEQyNF8zlJtrAAPtWMUDBp9VfDRF5oL2xkwFuyXRMPtRZ7lNfGijDrMc06bDNfp478sQQ',
},
{
'name' => 'U2F-2',
'type' => 'U2F',
'epoch' => 1588691730,
'_keyHandle' =>
'F1Kk9V_O7KDPIx-mqp6CIjbz7ljA-ihWVWyoP1xYBe_HPLHR74aTLanmn0b4vI8DumiBWO1DAle3k6N55cXreg',
'_userKey' =>
'BAE_svIcxLfm2Knart7DI1ScfBnCt-OFKDWugp3YMO14tamwuc_wN0vSh1D_0DV4Ao3S5GNQZXxtjtUADHTwXHA',
},
{
_credentialId => 'noconflict',
'name' => 'Existing WebAuthn',
'type' => 'WebAuthn',
'epoch' => 1588691798
}
]
),
"_oidcConsents" => to_json( [ {
'scope' => 'openid email',
'rp' => 'rp-example',
'epoch' => 1589288341
},
{
'scope' => 'openid email',
'epoch' => 1589291482,
'rp' => 'rp-example2'
}
]
),
"_session_uid" => "dwho",
}
}
);
# rtyler
Lemonldap::NG::Common::Session->new( {
@psessionsOpts,
id => "8d3bc3b0e14ea2a155f275aa7c07ebee",
force => 1,
info => {
"_session_uid" => "rtyler",
"_2fDevices" => to_json( [ {
'type' => 'UBK',
'epoch' => 1588691690,
'_yubikey' => 'cccccceijfnf',
'name' => 'Imported automatically'
},
{
'name' => 'U2F-3',
'type' => 'U2F',
'epoch' => 1588691734,
'_keyHandle' =>
'4suXv5Cf10vbJEP72mVkLpBjhSqy5niOgfc0X_MjdxZ_g2e-V8biC6WyCTpF_kGV1FCa06YlcryPCtWUuUST_g',
'_userKey' =>
'BIXrgc12iGGOYIGyWKd8WeOGCKyTkFA7jXkjlLS0i1MA3vy8gDocfqYCngXMzBAmtGI7FfMlbkG6DJeSubdxAVc',
},
]
),
}
}
);
}
sub getJson {
my @args = @_;
my ($str) = Test::Output::output_from( sub { $cli->run(@args); } );
return from_json($str);
}
sub getLines {
my @args = @_;
my ($str) = Test::Output::output_from( sub { $cli->run(@args); } );
return [ split /\n/, $str ];
}
setup_sessions();
my $res;
# Migrate U2F
$cli->run( "secondfactors", {}, "migrateu2f", "dwho" );
$res = getJson( "secondfactors", {}, "get", "rtyler" );
is( values %{$res}, 2, "Still 2 devices" );
is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
0, "No WebAuthn sessions created" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( values %{$res}, 6, "Expect 6 devices after migration" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ),
2, "U2F still present" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ),
1, "UBK still in place" );
is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
3, "New WebAuthn device" );
my $migratedid = "MTU4ODY5MTcyODo6V2ViQXV0aG46OlUyRi0x";
is( $res->{$migratedid}->{_signCount}, 0, "migrated signcount" );
is(
$res->{$migratedid}->{_credentialId},
'4aS6vXlFQpG5XZSoad6auM9fFu7Q1wazQYwfPtPKN_Hll6Up_ceeWkOgqxm49swWq4Vvcg5UlX0sQQhuRe8heA',
"migrated credential ID"
);
is(
$res->{$migratedid}->{_credentialPublicKey},
'pQECAyYgASFYIMgMqKPL2PhsjCNW78UEQyNF8zlJtrAAPtWMUDBp9VfDIlggRF5oL2xkwFuyXRMPtRZ7lNfGijDrMc06bDNfp478sQQ',
"migrated credential key"
);
is( $res->{$migratedid}->{epoch}, '1588691728', "migrated epoch" );
is( $res->{$migratedid}->{name}, "U2F-1", "migrated name" );
# Check idempotence
$cli->run( "secondfactors", {}, "migrateu2f", "dwho" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( values %{$res}, 6, "Expect still 6 devices after rerunning migration" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ),
2, "U2F still in place" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ),
1, "UBK still in place" );
is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
3, "Same WebAuthn devices" );
rmtree $dir;
setup_sessions();
# Migrate all
$cli->run( "secondfactors", { all => 1 }, "migrateu2f" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( values %{$res}, 6, "Expect 6 devices after migration" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ),
2, "U2F still in place" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ),
1, "UBK still in place" );
is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
3, "New WebAuthn device" );
$res = getJson( "secondfactors", {}, "get", "rtyler" );
is( values %{$res}, 3, "Expect 3 devices after migration" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ),
1, "U2F still in place" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ),
1, "UBK still in place" );
is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
1, "New WebAuthn device" );
rmtree $dir;
}
done_testing();

View File

@ -15,6 +15,7 @@ extends qw(
Lemonldap::NG::Common::Conf::AccessLib
);
use constant _2FTYPES => [ "UBK", "U2F", "TOTP", "WebAuthn" ];
our $VERSION = '2.0.10';
#############################
@ -46,7 +47,7 @@ sub init {
$self->{hiddenAttributes} //= "_password";
$self->{hiddenAttributes} .= ' _session_id'
unless $conf->{displaySessionId};
$self->{TOTPCheck} = $self->{U2FCheck} = $self->{UBKCheck} = '1';
$self->{TOTPCheck} = $self->{U2FCheck} = $self->{UBKCheck} = $self->{WebAuthnCheck} = '1';
return 1;
}
@ -67,7 +68,7 @@ sub del2F {
my $epoch = $params->{epoch}
or return $self->sendError( $req, 'Missing "epoch" parameter', 400 );
if ( $type =~ /\b(?:U2F|TOTP|UBK)\b/ ) {
if ( grep { $_ eq $type } @{_2FTYPES()} ) {
$self->logger->debug(
"Call procedure delete2F with type=$type and epoch=$epoch");
return $self->delete2F( $req, $session, $skey );
@ -117,7 +118,7 @@ sub sfa {
$moduleOptions->{backend} = $mod->{module};
# Select 2FA sessions to display
foreach (qw(U2F TOTP UBK)) {
foreach (@{_2FTYPES()}) {
$self->{ $_ . 'Check' } = delete $params->{ $_ . 'Check' }
if ( defined $params->{ $_ . 'Check' } );
}
@ -188,17 +189,18 @@ sub sfa {
# Remove sessions without at least one 2F device(s)
$self->logger->debug(
"Removing sessions without at least one 2F device(s)...");
my $_2f_types_re = join ('|', @{_2FTYPES()});
foreach my $session ( keys %$res ) {
delete $res->{$session}
unless ( defined $res->{$session}->{_2fDevices}
and $res->{$session}->{_2fDevices} =~
/"type":\s*"(?:U2F|TOTP|UBK)"/s );
/"type":\s*"(?:$_2f_types_re)"/s );
}
# Filter 2FA sessions if needed
$self->logger->debug("Filtering 2F sessions...");
my $all = ( keys %$res );
foreach (qw(U2F TOTP UBK)) {
foreach (@{_2FTYPES()}) {
if ( $self->{ $_ . 'Check' } eq '2' ) {
foreach my $session ( keys %$res ) {
delete $res->{$session}

View File

@ -159,7 +159,7 @@ sub _get2F {
type => $device->{type},
name => $device->{name}
}
unless ( ( defined $type and $type ne $device->{type} )
unless ( ( defined $type and uc($type) ne uc( $device->{type} ) )
or ( defined $id and $id ne genId2F($device) ) );
}
}
@ -224,8 +224,12 @@ sub _delete2FFromSessions {
my $element = shift @$devices;
if (
( defined $type or defined $id )
and ( ( defined $type and $type ne $element->{type} )
or ( defined $id and $id ne genId2F($element) ) )
and ( (
defined $type
and uc($type) ne uc( $element->{type} )
)
or ( defined $id and $id ne genId2F($element) )
)
)
{
push @keep, $element;
@ -334,9 +338,9 @@ sub _checkType {
res => "ko",
code => 400,
msg =>
"Invalid input: Type \"$type\" does not exist. Allowed values for type are: \"U2F\", \"TOTP\" or \"UBK\""
"Invalid input: Type \"$type\" does not exist. Allowed values for type are: \"U2F\", \"TOTP\", \"WebAuthn\" or \"UBK\""
}
unless ( $type =~ /\b(?:U2F|TOTP|UBK)\b/ );
unless ( $type =~ /\b(?:U2F|TOTP|UBK|WebAuthn)\b/i );
return { res => "ok" };
}

View File

@ -637,11 +637,12 @@ sub attributes {
'type' => 'keyTextContainer'
},
'available2F' => {
'default' => 'UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,Yubikey,Radius',
'type' => 'text'
'default' =>
'UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius',
'type' => 'text'
},
'available2FSelfRegistration' => {
'default' => 'TOTP,U2F,Yubikey',
'default' => 'TOTP,U2F,WebAuthn,Yubikey',
'type' => 'text'
},
'avoidAssignment' => {
@ -4478,6 +4479,50 @@ qr/^(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-
'virtualHosts' => {
'type' => 'virtualHostContainer'
},
'webauthn2fActivation' => {
'default' => 0,
'type' => 'boolOrExpr'
},
'webauthn2fAuthnLevel' => {
'type' => 'int'
},
'webauthn2fLabel' => {
'type' => 'text'
},
'webauthn2fLogo' => {
'type' => 'text'
},
'webauthn2fSelfRegistration' => {
'default' => 0,
'type' => 'boolOrExpr'
},
'webauthn2fUserCanRemoveKey' => {
'default' => 1,
'type' => 'bool'
},
'webauthn2fUserVerification' => {
'default' => 'preferred',
'select' => [ {
'k' => 'discouraged',
'v' => 'Discouraged'
},
{
'k' => 'preferred',
'v' => 'Preferred'
},
{
'k' => 'required',
'v' => 'Required'
}
],
'type' => 'select'
},
'webauthnDisplayNameAttr' => {
'type' => 'text'
},
'webauthnRpName' => {
'type' => 'text'
},
'webIDAuthnLevel' => {
'default' => 1,
'type' => 'int'

View File

@ -2191,6 +2191,55 @@ sub attributes {
documentation => 'Yubikey device time to live',
},
# WebAuthn 2FA
webauthn2fActivation => {
type => 'boolOrExpr',
default => 0,
documentation => 'WebAuthn second factor activation',
},
webauthn2fSelfRegistration => {
type => 'boolOrExpr',
default => 0,
documentation => 'WebAuthn self registration activation',
},
webauthn2fAuthnLevel => {
type => 'int',
documentation =>
'Authentication level for users authentified by WebAuthn second factor'
},
webauthn2fLabel => {
type => 'text',
documentation => 'Portal label for WebAuthn second factor'
},
webauthn2fLogo => {
type => 'text',
documentation => 'Custom logo for WebAuthn 2F',
},
webauthn2fUserVerification => {
type => 'select',
select => [
{ k => 'discouraged', v => 'Discouraged' },
{ k => 'preferred', v => 'Preferred' },
{ k => 'required', v => 'Required' },
],
default => 'preferred',
documentation => 'Verify user during registration and login',
},
webauthn2fUserCanRemoveKey => {
type => 'bool',
default => 1,
documentation => 'Authorize users to remove existing WebAuthn',
},
webauthnDisplayNameAttr => {
type => 'text',
documentation => 'Session attribute containing user display name',
},
webauthnRpName => {
type => 'text',
documentation => 'WebAuthn Relying Party display name',
},
# Single session
notifyDeleted => {
default => 1,
@ -3322,12 +3371,12 @@ sub attributes {
},
available2F => {
type => 'text',
default => 'UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,Yubikey,Radius',
default => 'UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius',
documentation => 'Available second factor modules',
},
available2FSelfRegistration => {
type => 'text',
default => 'TOTP,U2F,Yubikey',
default => 'TOTP,U2F,WebAuthn,Yubikey',
documentation =>
'Available self-registration modules for second factor',
},

View File

@ -992,6 +992,21 @@ sub tree {
'rest2fLabel', 'rest2fLogo'
]
},
{
title => 'webauthn2f',
help => 'webauthn2f.html',
nodes => [
'webauthn2fActivation',
'webauthn2fUserVerification',
'webauthn2fSelfRegistration',
'webauthn2fUserCanRemoveKey',
'webauthnRpName',
'webauthnDisplayNameAttr',
'webauthn2fAuthnLevel',
'webauthn2fLabel',
'webauthn2fLogo',
]
},
'sfExtra',
{
title => 'sfRemovedNotification',

View File

@ -630,7 +630,7 @@ sub tests {
my $msg = '';
my $ok = 0;
foreach (qw(u totp yubikey)) {
foreach (qw(u totp yubikey webauthn)) {
$ok ||= $conf->{ $_ . '2fActivation' }
&& $conf->{ $_ . '2fSelfRegistration' };
last if ($ok);

View File

@ -73,6 +73,7 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
$scope.U2FCheck = "1"
$scope.TOTPCheck = "1"
$scope.UBKCheck = "1"
$scope.WebAuthnCheck = "1"
# Import translations functions
$scope.translateP = $translator.translateP
@ -201,7 +202,7 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
subres = []
for attr in attrs
if session[attr]
if session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK)"/)
if session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK|WebAuthn)"/)
subres.push
title: "type"
value: "name"
@ -295,7 +296,7 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
over = 0
# Launch HTTP query
$http.get("#{scriptname}sfa/#{sessionType}?#{query}&U2FCheck=#{$scope.U2FCheck}&TOTPCheck=#{$scope.TOTPCheck}&UBKCheck=#{$scope.UBKCheck}").then (response) ->
$http.get("#{scriptname}sfa/#{sessionType}?#{query}&U2FCheck=#{$scope.U2FCheck}&TOTPCheck=#{$scope.TOTPCheck}&UBKCheck=#{$scope.UBKCheck}&WebAuthnCheck=#{$scope.WebAuthnCheck}").then (response) ->
data = response.data
if data.result
for n in data.values
@ -346,7 +347,7 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
over = 0
# Launch HTTP
$http.get("#{scriptname}sfa/#{sessionType}?_session_uid=#{$scope.searchString}*&groupBy=substr(_session_uid,#{$scope.searchString.length})&U2FCheck=#{$scope.U2FCheck}&TOTPCheck=#{$scope.TOTPCheck}&UBKCheck=#{$scope.UBKCheck}").then (response) ->
$http.get("#{scriptname}sfa/#{sessionType}?_session_uid=#{$scope.searchString}*&groupBy=substr(_session_uid,#{$scope.searchString.length})&U2FCheck=#{$scope.U2FCheck}&TOTPCheck=#{$scope.TOTPCheck}&UBKCheck=#{$scope.UBKCheck}&WebAuthnCheck=#{$scope.WebAuthnCheck}").then (response) ->
data = response.data
if data.result
for n in data.values

View File

@ -246,7 +246,7 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
subres = []
for attr in attrs
if session[attr]
if session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK)"/)
if session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK|WebAuthn)"/)
subres.push
title: "type"
value: "name"

View File

@ -89,6 +89,7 @@
$scope.U2FCheck = "1";
$scope.TOTPCheck = "1";
$scope.UBKCheck = "1";
$scope.WebAuthnCheck = "1";
$scope.translateP = $translator.translateP;
$scope.translate = $translator.translate;
$scope.translateTitle = function(node) {
@ -205,7 +206,7 @@
for (i = 0, len = attrs.length; i < len; i++) {
attr = attrs[i];
if (session[attr]) {
if (session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK)"/)) {
if (session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK|WebAuthn)"/)) {
subres.push({
title: "type",
value: "name",
@ -303,7 +304,7 @@
} else {
over = 0;
}
return $http.get(scriptname + "sfa/" + sessionType + "?" + query + "&U2FCheck=" + $scope.U2FCheck + "&TOTPCheck=" + $scope.TOTPCheck + "&UBKCheck=" + $scope.UBKCheck).then(function(response) {
return $http.get(scriptname + "sfa/" + sessionType + "?" + query + "&U2FCheck=" + $scope.U2FCheck + "&TOTPCheck=" + $scope.TOTPCheck + "&UBKCheck=" + $scope.UBKCheck + "&WebAuthnCheck=" + $scope.WebAuthnCheck).then(function(response) {
var data, i, len, n, ref;
data = response.data;
if (data.result) {
@ -345,7 +346,7 @@
} else {
over = 0;
}
return $http.get(scriptname + "sfa/" + sessionType + "?_session_uid=" + $scope.searchString + "*&groupBy=substr(_session_uid," + $scope.searchString.length + ")&U2FCheck=" + $scope.U2FCheck + "&TOTPCheck=" + $scope.TOTPCheck + "&UBKCheck=" + $scope.UBKCheck).then(function(response) {
return $http.get(scriptname + "sfa/" + sessionType + "?_session_uid=" + $scope.searchString + "*&groupBy=substr(_session_uid," + $scope.searchString.length + ")&U2FCheck=" + $scope.U2FCheck + "&TOTPCheck=" + $scope.TOTPCheck + "&UBKCheck=" + $scope.UBKCheck + "&WebAuthnCheck=" + $scope.WebAuthnCheck).then(function(response) {
var data, i, len, n, ref;
data = response.data;
if (data.result) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -280,7 +280,7 @@
for (i = 0, len = attrs.length; i < len; i++) {
attr = attrs[i];
if (session[attr]) {
if (session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK)"/)) {
if (session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK|WebAuthn)"/)) {
subres.push({
title: "type",
value: "name",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"مستوى إثبات الهوية",
"webIDExportedVars":"المتغيرات المصدرة",
"webIDWhitelist":"القائمة البيضاء للويب آي دي",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"تفعيل",
"webauthn2fAuthnLevel":"مستوى إثبات الهوية",
"webauthn2fLabel":"Label",
"webauthn2fLogo":"شعار",
"webauthn2fSelfRegistration":"التسجيل الذاتي",
"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"معايير ويب أي دي",
"whatToTrace":"المستخدم_البعيد",
"whiteList":"القائمة البيضاء",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"Authentication level",
"webIDExportedVars":"Exported variables",
"webIDWhitelist":"WebID whitelist",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"Activation",
"webauthn2fAuthnLevel":"Authentication level",
"webauthn2fLabel":"Label",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Self registration",
"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"WebID parameters",
"whatToTrace":"REMOTE_USER",
"whiteList":"White list",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"Authentication level",
"webIDExportedVars":"Exported variables",
"webIDWhitelist":"WebID whitelist",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"Activation",
"webauthn2fAuthnLevel":"Authentication level",
"webauthn2fLabel":"Label",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Self registration",
"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"WebID parameters",
"whatToTrace":"REMOTE_USER",
"whiteList":"White list",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"Nivel de autentificación",
"webIDExportedVars":"Exported variables",
"webIDWhitelist":"WebID whitelist",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"Activación",
"webauthn2fAuthnLevel":"Nivel de autentificación",
"webauthn2fLabel":"Label",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Self registration",
"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"WebID parameters",
"whatToTrace":"REMOTE_USER",
"whiteList":"White list",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"Niveau d'authentification",
"webIDExportedVars":"Variables exportées",
"webIDWhitelist":"Liste blanche WebID",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"Activation",
"webauthn2fAuthnLevel":"Niveau d'authentification",
"webauthn2fLabel":"Label",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Auto-enregistrement",
"webauthn2fUserCanRemoveKey":"Autoriser les utilisateurs à effacer leur WebAuthn",
"webauthn2fUserVerification":"Vérification de l'utilisateur",
"webauthnDisplayNameAttr":"Attribut du nom d'affichage de l'utilisateur",
"webauthnRpName":"Nom d'affichage du portail",
"webidParams":"Paramètres WebID",
"whatToTrace":"REMOTE_USER",
"whiteList":"Liste blanche",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"Livello di autenticazione",
"webIDExportedVars":"Variabili esportate",
"webIDWhitelist":"Whitelist WebID",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"Attivazione",
"webauthn2fAuthnLevel":"Livello di autenticazione",
"webauthn2fLabel":"Label",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Auto-registrazione",
"webauthn2fUserCanRemoveKey":"Autorizza l'utente a rimuovere la WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"Parametri di WebID",
"whatToTrace":"\nREMOTE_USER",
"whiteList":"Lista bianca",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"Poziom uwierzytelnienia",
"webIDExportedVars":"Wyeksportowane zmienne",
"webIDWhitelist":"Biała lista WebID",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"Aktywacja",
"webauthn2fAuthnLevel":"Poziom uwierzytelnienia",
"webauthn2fLabel":"Etykieta",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Samodzielna rejestracja",
"webauthn2fUserCanRemoveKey":"Pozwól użytkownikowi usunąć WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"Parametry WebID",
"whatToTrace":"REMOTE_USER",
"whiteList":"Biała lista",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"Doğrulama seviyesi",
"webIDExportedVars":"Dışa aktarılan değişkenler",
"webIDWhitelist":"WebID beyaz listesi",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"Aktivasyon",
"webauthn2fAuthnLevel":"Doğrulama seviyesi",
"webauthn2fLabel":"Etiket",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Kendi kendine kayıt",
"webauthn2fUserCanRemoveKey":"WebAuthn'i kaldırmak için kullanıcıya izin ver",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"WebID parametreleri",
"whatToTrace":"REMOTE_USER",
"whiteList":"Beyaz liste",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"Mức xác thực",
"webIDExportedVars":"Xuất khẩu biến",
"webIDWhitelist":"WebID white list",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"Kích hoạt",
"webauthn2fAuthnLevel":"Mức xác thực",
"webauthn2fLabel":"Label",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Tự đăng ký",
"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"Tham số WebID",
"whatToTrace":"REMOTE_USER",
"whiteList":"Danh sách trắng",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"认证等级",
"webIDExportedVars":"Exported variables",
"webIDWhitelist":"WebID whitelist",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"激活",
"webauthn2fAuthnLevel":"认证等级",
"webauthn2fLabel":"Label",
"webauthn2fLogo":"Logo",
"webauthn2fSelfRegistration":"Self registration",
"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"WebID parameters",
"whatToTrace":"REMOTE_USER",
"whiteList":"White list",

View File

@ -1214,6 +1214,16 @@
"webIDAuthnLevel":"驗證等級",
"webIDExportedVars":"已匯出的變數",
"webIDWhitelist":"WebID 白名單",
"webauthn2f":"WebAuthn",
"webauthn2fActivation":"啟用",
"webauthn2fAuthnLevel":"驗證等級",
"webauthn2fLabel":"標籤",
"webauthn2fLogo":"圖示",
"webauthn2fSelfRegistration":"自行註冊",
"webauthn2fUserCanRemoveKey":"允許使用者移除 WebAuthn",
"webauthn2fUserVerification":"User verification",
"webauthnDisplayNameAttr":"User Display Name attribute",
"webauthnRpName":"Relying Party display name",
"webidParams":"WebID 參數",
"whatToTrace":"REMOTE_USER",
"whiteList":"白名單",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -22,6 +22,9 @@
&nbsp;&nbsp;&&nbsp;&nbsp;
<input type="checkbox" ng-model="UBKCheck" class="form-check-input" ng-true-value="2" ng-false-value="1" ng-change="search2FA()"/>
<label class="form-check-label" for="UBKCheck">UBK</label>
&nbsp;&nbsp;&&nbsp;&nbsp;
<input type="checkbox" ng-model="WebAuthnCheck" class="form-check-input" ng-true-value="2" ng-false-value="1" ng-change="search2FA()"/>
<label class="form-check-label" for="WebAuthnCheck">WebAuthn</label>
</div>
</form>
</ul>
@ -104,16 +107,16 @@
</table>
</div>
<div ng-if="!node.nodes" >
<th class="col-md-3" ng-if="node.title!='UBK' && node.title!='TOTP' && node.title!='U2F'">{{translate(node.title)}}</th>
<td class="data-{{node.epoch}}" ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F'" >{{node.title}}</td>
<th class="col-md-3" ng-if="node.title!='UBK' && node.title!='TOTP' && node.title!='U2F' && node.title!='WebAuthn'">{{translate(node.title)}}</th>
<td class="data-{{node.epoch}}" ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn'" >{{node.title}}</td>
<th class="col-md-3" ng-if="node.title=='type'">{{translate(node.value)}}</th>
<td class="col-md-3 data-{{node.epoch}}" ng-if="node.title!='type'" >{{node.value}}</td>
<th class="col-md-3" ng-if="node.title=='type'">{{translate(node.epoch)}}</th>
<td class="col-md-3 data-{{node.epoch}}" ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F'">{{localeDate(node.epoch)}}</td>
<td class="col-md-3 data-{{node.epoch}}" ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn'">{{localeDate(node.epoch)}}</td>
<td class="data-{{node.epoch}}">
<span ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F'" class="link text-danger glyphicon glyphicon-minus-sign" ng-click="delete2FA(node.title, node.epoch)"></span>
<span ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn'" class="link text-danger glyphicon glyphicon-minus-sign" ng-click="delete2FA(node.title, node.epoch)"></span>
<!--
<span ng-if="$last && ( node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' )" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newRule'})"></span>
<span ng-if="$last && ( node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn' )" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newRule'})"></span>
-->
</td>
</div>

View File

@ -108,7 +108,7 @@
<td class="data-{{node.epoch}}">
<span ng-if="node.td=='2'" class="link text-danger glyphicon glyphicon-minus-sign" ng-click="deleteOIDCConsent(node.title, node.epoch)"></span>
<!--
<span ng-if="$last && ( node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' )" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newRule'})"></span>
<span ng-if="$last && ( node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn' )" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newRule'})"></span>
-->
</td>
</div>

View File

@ -241,7 +241,15 @@ $sfaDevices = [ {
"type" => "UBK",
"_secret" => "123456",
"epoch" => time
}
},
{
"_credentialId" => "abc",
"_credentialPublicKey" => "abc",
"_signCount" => "65",
"epoch" => "1643201784",
"name" => "MyFidoKey",
"type" => "WebAuthn"
},
];
newSession( 'dwho', '127.10.0.1', 'SSO', $sfaDevices );
newSession( 'dwho', '127.10.0.1', 'Persistent', $sfaDevices );
@ -304,12 +312,15 @@ newSession( 'tof', '127.10.0.1', 'Persistent', $sfaDevices );
checkGetList( 1, 'dwho', 'U2F' );
checkGetList( 1, 'dwho', 'TOTP' );
checkGetList( 1, 'dwho', 'UBK' );
checkGetList( 1, 'dwho', 'WebAuthn' );
checkGetBadType( 'dwho', 'UBKIKI' );
$ret = checkGetList( 3, 'dwho' );
$ret = checkGetList( 4, 'dwho' );
checkGetOnIds( 'dwho', $ret );
checkDelete( 'dwho', @$ret[0]->{id} );
checkDelete404( 'dwho', @$ret[0]->{id} );
checkGetList( 2, 'dwho' );
checkGetList( 3, 'dwho' );
checkDeleteList( 1, 'dwho', 'WebAuthn' );
checkGetList( 0, 'dwho', 'WebAuthn' );
checkDeleteList( 2, 'dwho' );
checkGetList( 0, 'dwho' );
checkDeleteList( 0, 'dwho' );

View File

@ -4,21 +4,20 @@ eg/index.cgi
eg/index.fcgi
eg/index.psgi
inc/LWP/Protocol/PSGI.pm
lib/Lemonldap/NG/Portal.pm
lib/Lemonldap/NG/Portal/2F/Engines/Default.pm
lib/Lemonldap/NG/Portal/2F/Ext2F.pm
lib/Lemonldap/NG/Portal/2F/Mail2F.pm
lib/Lemonldap/NG/Portal/2F/Radius.pm
lib/Lemonldap/NG/Portal/2F/Register/TOTP.pm
lib/Lemonldap/NG/Portal/2F/Register/U2F.pm
lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm
lib/Lemonldap/NG/Portal/2F/Register/Yubikey.pm
lib/Lemonldap/NG/Portal/2F/REST.pm
lib/Lemonldap/NG/Portal/2F/TOTP.pm
lib/Lemonldap/NG/Portal/2F/U2F.pm
lib/Lemonldap/NG/Portal/2F/UTOTP.pm
lib/Lemonldap/NG/Portal/2F/WebAuthn.pm
lib/Lemonldap/NG/Portal/2F/Yubikey.pm
lib/Lemonldap/NG/Portal/Auth.pod
lib/Lemonldap/NG/Portal/Auth/_WebForm.pm
lib/Lemonldap/NG/Portal/Auth/AD.pm
lib/Lemonldap/NG/Portal/Auth/Apache.pm
lib/Lemonldap/NG/Portal/Auth/CAS.pm
@ -34,9 +33,10 @@ lib/Lemonldap/NG/Portal/Auth/Kerberos.pm
lib/Lemonldap/NG/Portal/Auth/LDAP.pm
lib/Lemonldap/NG/Portal/Auth/LinkedIn.pm
lib/Lemonldap/NG/Portal/Auth/Null.pm
lib/Lemonldap/NG/Portal/Auth/OpenID.pm
lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm
lib/Lemonldap/NG/Portal/Auth/OpenID.pm
lib/Lemonldap/NG/Portal/Auth/PAM.pm
lib/Lemonldap/NG/Portal/Auth.pod
lib/Lemonldap/NG/Portal/Auth/Proxy.pm
lib/Lemonldap/NG/Portal/Auth/Radius.pm
lib/Lemonldap/NG/Portal/Auth/Remote.pm
@ -45,6 +45,7 @@ lib/Lemonldap/NG/Portal/Auth/SAML.pm
lib/Lemonldap/NG/Portal/Auth/Slave.pm
lib/Lemonldap/NG/Portal/Auth/SSL.pm
lib/Lemonldap/NG/Portal/Auth/Twitter.pm
lib/Lemonldap/NG/Portal/Auth/_WebForm.pm
lib/Lemonldap/NG/Portal/Auth/WebID.pm
lib/Lemonldap/NG/Portal/CDC.pm
lib/Lemonldap/NG/Portal/CertificateResetByMail/Custom.pm
@ -52,10 +53,10 @@ lib/Lemonldap/NG/Portal/CertificateResetByMail/Demo.pm
lib/Lemonldap/NG/Portal/CertificateResetByMail/LDAP.pm
lib/Lemonldap/NG/Portal/Issuer/CAS.pm
lib/Lemonldap/NG/Portal/Issuer/Get.pm
lib/Lemonldap/NG/Portal/Issuer/OpenID.pm
lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
lib/Lemonldap/NG/Portal/Issuer/OpenID.pm
lib/Lemonldap/NG/Portal/Issuer/SAML.pm
lib/Lemonldap/NG/Portal/Lib/_tokenRule.pm
lib/Lemonldap/NG/Portal/Lib/Authen/WebAuthn.pm
lib/Lemonldap/NG/Portal/Lib/Captcha.pm
lib/Lemonldap/NG/Portal/Lib/CAS.pm
lib/Lemonldap/NG/Portal/Lib/Choice.pm
@ -66,9 +67,9 @@ lib/Lemonldap/NG/Portal/Lib/Net/LDAP.pm
lib/Lemonldap/NG/Portal/Lib/Notifications/JSON.pm
lib/Lemonldap/NG/Portal/Lib/Notifications/XML.pm
lib/Lemonldap/NG/Portal/Lib/OneTimeToken.pm
lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm
lib/Lemonldap/NG/Portal/Lib/OpenID/Server.pm
lib/Lemonldap/NG/Portal/Lib/OpenID/SREG.pm
lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm
lib/Lemonldap/NG/Portal/Lib/OtherSessions.pm
lib/Lemonldap/NG/Portal/Lib/OverConf.pm
lib/Lemonldap/NG/Portal/Lib/Remote.pm
@ -78,9 +79,10 @@ lib/Lemonldap/NG/Portal/Lib/SAML.pm
lib/Lemonldap/NG/Portal/Lib/Slave.pm
lib/Lemonldap/NG/Portal/Lib/SMTP.pm
lib/Lemonldap/NG/Portal/Lib/SOAPProxy.pm
lib/Lemonldap/NG/Portal/Lib/_tokenRule.pm
lib/Lemonldap/NG/Portal/Lib/U2F.pm
lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm
lib/Lemonldap/NG/Portal/Lib/Wrapper.pm
lib/Lemonldap/NG/Portal/Main.pm
lib/Lemonldap/NG/Portal/Main/Auth.pm
lib/Lemonldap/NG/Portal/Main/Constants.pm
lib/Lemonldap/NG/Portal/Main/Display.pm
@ -89,6 +91,7 @@ lib/Lemonldap/NG/Portal/Main/Issuer.pm
lib/Lemonldap/NG/Portal/Main/Menu.pm
lib/Lemonldap/NG/Portal/Main/Plugin.pm
lib/Lemonldap/NG/Portal/Main/Plugins.pm
lib/Lemonldap/NG/Portal/Main.pm
lib/Lemonldap/NG/Portal/Main/Process.pm
lib/Lemonldap/NG/Portal/Main/Request.pm
lib/Lemonldap/NG/Portal/Main/Run.pm
@ -132,12 +135,12 @@ lib/Lemonldap/NG/Portal/Plugins/SOAPServer.pm
lib/Lemonldap/NG/Portal/Plugins/Status.pm
lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm
lib/Lemonldap/NG/Portal/Plugins/Upgrade.pm
lib/Lemonldap/NG/Portal.pm
lib/Lemonldap/NG/Portal/Register/AD.pm
lib/Lemonldap/NG/Portal/Register/Base.pm
lib/Lemonldap/NG/Portal/Register/Custom.pm
lib/Lemonldap/NG/Portal/Register/Demo.pm
lib/Lemonldap/NG/Portal/Register/LDAP.pm
lib/Lemonldap/NG/Portal/UserDB.pod
lib/Lemonldap/NG/Portal/UserDB/AD.pm
lib/Lemonldap/NG/Portal/UserDB/CAS.pm
lib/Lemonldap/NG/Portal/UserDB/Choice.pm
@ -148,8 +151,9 @@ lib/Lemonldap/NG/Portal/UserDB/Demo.pm
lib/Lemonldap/NG/Portal/UserDB/Facebook.pm
lib/Lemonldap/NG/Portal/UserDB/LDAP.pm
lib/Lemonldap/NG/Portal/UserDB/Null.pm
lib/Lemonldap/NG/Portal/UserDB/OpenID.pm
lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm
lib/Lemonldap/NG/Portal/UserDB/OpenID.pm
lib/Lemonldap/NG/Portal/UserDB.pod
lib/Lemonldap/NG/Portal/UserDB/Proxy.pm
lib/Lemonldap/NG/Portal/UserDB/Remote.pm
lib/Lemonldap/NG/Portal/UserDB/REST.pm

View File

@ -280,8 +280,11 @@ sub run {
# Delete U2F device
@$_2fDevices = map {
if ( $_->{epoch} eq $epoch ) { $keyName = $_->{name}; () }
else { $_ }
if ( $_->{epoch} eq $epoch and $_->{type} eq "U2F" ) {
$keyName = $_->{name};
();
}
else { $_ }
} @$_2fDevices;
if ($keyName) {
$self->logger->debug(

View File

@ -0,0 +1,352 @@
# Self WebAuthn registration
package Lemonldap::NG::Portal::2F::Register::WebAuthn;
use strict;
use Mouse;
use JSON qw(from_json to_json);
use MIME::Base64 qw(encode_base64url decode_base64url);
use Crypt::URandom;
our $VERSION = '2.0.12';
extends 'Lemonldap::NG::Portal::Main::Plugin';
with 'Lemonldap::NG::Portal::Lib::WebAuthn';
# INITIALIZATION
has prefix => ( is => 'rw', default => 'webauthn' );
has template => ( is => 'ro', default => 'webauthn2fregister' );
has welcome => ( is => 'ro', default => 'webauthn2fWelcome' );
has logo => ( is => 'rw', default => 'webauthn.png' );
has ott => (
is => 'ro',
lazy => 1,
default => sub {
my $ott =
$_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
my $timeout = $_[0]->{conf}->{sfRegisterTimeout}
// $_[0]->{conf}->{formTimeout};
$ott->timeout($timeout);
return $ott;
}
);
has displayname_attr => (
is => 'rw',
lazy => 1,
default => sub {
my $self = shift;
$self->conf->{webauthnDisplayNameAttr}
|| $self->conf->{portalUserAttr}
|| $self->conf->{whatToTrace}
|| '_user';
}
);
has rpName => (
is => 'rw',
lazy => 1,
default => sub {
my $self = shift;
$self->conf->{webauthnRpName} || "LemonLDAP::NG";
}
);
sub init { return 1; }
# RUNNING METHODS
# Return a Base64url encoded user handle
sub getRegistrationUserHandle {
my ( $self, $req ) = @_;
my $current_user_handle = $self->getUserHandle( $req, $req->userData );
if ($current_user_handle) {
return $current_user_handle;
}
else {
my $new_user_handle = $self->_generate_user_handle;
$self->setUserHandle( $req, $new_user_handle );
return $new_user_handle;
}
}
# https://www.w3.org/TR/webauthn-2/#sctn-user-handle-privacy
# It is RECOMMENDED to let the user handle be 64 random bytes, and store this
# value in the users account.
sub _generate_user_handle {
my ($self) = @_;
return encode_base64url( Crypt::URandom::urandom(64) );
}
sub _registrationchallenge {
my ( $self, $req, $user ) = @_;
my @_2fDevices = $self->find2fByType( $req, $req->userData );
# Check if user can register one more 2F device
my $size = @_2fDevices;
my $maxSize = $self->conf->{max2FDevices};
$self->logger->debug("Registered 2F Device(s): $size / $maxSize");
if ( $size >= $maxSize ) {
$self->userLogger->warn("Max number of 2F devices is reached");
return $self->p->sendError( $req, 'maxNumberof2FDevicesReached', 400 );
}
my $challenge_base64 = encode_base64url( Crypt::URandom::urandom(32) );
# Challenge is persisted on the server
my $token = $self->ott->createToken( {
registration_options => {
challenge => $challenge_base64,
}
}
);
my $displayName = $req->userData->{ $self->displayname_attr } || $user;
my $userVerification = $self->conf->{webauthn2fUserVerification};
my $request = {
rp => {
name => $self->rpName,
},
user => {
name => $user,
id => $self->getRegistrationUserHandle($req),
displayName => $displayName,
},
challenge => $challenge_base64,
pubKeyCredParams => [],
authenticatorSelection => { (
$userVerification
? ( userVerification => $userVerification )
: ()
)
}
};
$self->logger->debug( "Register parameters " . to_json($request) );
return $self->p->sendJSONresponse( $req,
{ request => $request, state_id => $token } );
}
sub _registration {
my ( $self, $req, $user ) = @_;
# Recover creation parameters, including challenge
my $state_id = $req->param('state_id');
unless ($state_id) {
$self->logger->error("Could not find state ID in WebAuthn response");
return $self->p->sendError( $req, 'Invalid response', 400 );
}
my $state_data;
unless ( $state_data = $self->ott->getToken($state_id) ) {
$self->logger->error(
"Expired or invalid state ID in WebAuthn response: $state_id");
return $self->p->sendError( $req, 'PE82', 400 );
}
my $registration_options = ( $state_data->{registration_options} );
return $self->p->sendError( $req,
'Registration options missing from state data', 400 )
unless ($registration_options);
# Data required for WebAuthn verification
my $credential_json = $req->param('credential');
$self->logger->debug("Get registered credential data $credential_json");
return $self->p->sendError( $req, 'Missing credential parameter', 400 )
unless ($credential_json);
my $validation = eval {
$self->validateCredential( $req, $registration_options,
$credential_json );
};
if ($@) {
$self->logger->error("Credential validation error: $@");
return $self->p->sendError( $req, "webAuthnRegisterFailed", 400 );
}
my $credential_id = $validation->{credential_id};
my $credential_pubkey = $validation->{credential_pubkey};
my $signature_count = $validation->{signature_count};
$self->logger->debug( "Registering new credential : \n" . "ID: "
. $credential_id . "\n"
. "Public key "
. $credential_pubkey . "\n"
. "Signature count "
. $signature_count
. "\n" );
if (
$self->find2fByKey(
$req, $req->userData, $self->type,
"_credentialId", $credential_id
)
)
{
return $self->p->sendError( $req, 'webauthnAlreadyRegistered', 400 );
}
my $keyName = $req->param('keyName');
my $epoch = time();
# Set default name if empty, check characters and truncate name if too long
$keyName ||= $epoch;
unless ( $keyName =~ /^[\w]+$/ ) {
$self->userLogger->error('WebAuthn name with bad character(s)');
return $self->p->sendError( $req, 'badName', 200 );
}
$keyName = substr( $keyName, 0, $self->conf->{max2FDevicesNameLength} );
$self->logger->debug("Key name: $keyName");
if (
$self->add2fDevice(
$req,
$req->userData,
{
type => $self->type,
name => $keyName,
_credentialId => $credential_id,
_credentialPublicKey => $credential_pubkey,
_signCount => $signature_count,
epoch => $epoch
}
)
)
{
$self->userLogger->notice(
"Webauthn key registration of $keyName succeeds for $user");
return $self->p->sendJSONresponse( $req, { result => 1 } );
}
else {
return $self->p->sendError( $req, 'Server Error', 500 );
}
}
sub _verificationchallenge {
my ( $self, $req, $user ) = @_;
$self->logger->debug('Verification challenge req');
my $request = $self->generateChallenge( $req, $req->userData );
unless ($request) {
return $self->p->sendError( $req, "No registered devices", 400 );
}
# Request is persisted on the server
my $token = $self->ott->createToken( {
authentication_options => $request,
}
);
$self->logger->debug( "Authentication parameters: " . to_json($request) );
return $self->p->sendJSONresponse( $req,
{ request => $request, state_id => $token } );
}
sub _verification {
my ( $self, $req, $user ) = @_;
my $credential_json = $req->param('credential');
return $self->p->sendError( $req, 'Missing credential parameter', 400 )
unless ($credential_json);
my $state_id = $req->param('state_id');
unless ($state_id) {
$self->logger->error(
"Could not find state ID in WebAuthn response: $credential_json");
return $self->p->sendError( $req, 'Invalid response', 400 );
}
# Recover challenge
my $state_data;
unless ( $state_data = $self->ott->getToken($state_id) ) {
$self->logger->error(
"Expired or invalid state ID in WebAuthn response: $state_id");
return $self->p->sendError( $req, 'PE82', 400 );
}
my $signature_options = ( $state_data->{authentication_options} );
my $validation_result = eval {
$self->validateAssertion( $req, $req->userData, $signature_options,
$credential_json );
};
if ($@) {
$self->logger->error("Webauthn validation error for $user: $@");
return $self->p->sendJSONresponse( $req, { result => 0 } );
}
if ( $validation_result->{success} == 1 ) {
return $self->p->sendJSONresponse( $req, { result => 1 } );
}
else {
return $self->p->sendJSONresponse( $req, { result => 0 } );
}
}
sub _delete {
my ( $self, $req, $user ) = @_;
# Check if unregistration is allowed
return $self->p->sendError( $req, 'notAuthorized', 200 )
unless $self->conf->{webauthn2fUserCanRemoveKey};
my $epoch = $req->param('epoch')
or
return $self->p->sendError( $req, '"epoch" parameter is missing', 400 );
if ( $self->del2fDevice( $req, $req->userData, $self->type, $epoch ) ) {
return $self->p->sendJSONresponse( $req, { result => 1 } );
}
else {
return $self->p->sendError( $req, '2FDeviceNotFound', 400 );
}
}
# Main method
sub run {
my ( $self, $req, $action ) = @_;
my $user = $req->userData->{ $self->conf->{whatToTrace} };
return $self->p->sendError( $req,
'No ' . $self->conf->{whatToTrace} . ' found in user data', 500 )
unless $user;
# Check if U2F key can be updated
my $msg = $self->canUpdateSfa( $req, $action );
return $self->p->sendError( $req, $msg, 400 ) if $msg;
if ( $action eq 'registrationchallenge' ) {
return $self->_registrationchallenge( $req, $user );
}
elsif ( $action eq 'registration' ) {
return $self->_registration( $req, $user );
}
elsif ( $action eq 'verificationchallenge' ) {
return $self->_verificationchallenge( $req, $user );
}
elsif ( $action eq 'verification' ) {
return $self->_verification( $req, $user );
}
elsif ( $action eq 'delete' ) {
return $self->_delete( $req, $user );
}
else {
$self->logger->error("Unknown WebAuthn action -> $action");
return $self->p->sendError( $req, 'unknownAction', 400 );
}
}
1;

View File

@ -0,0 +1,122 @@
# WebAuthn second factor authentication
#
# This plugin handle authentications to ask WebAuthn second factor for users that
# have registered their WebAuthn authenticators
package Lemonldap::NG::Portal::2F::WebAuthn;
use strict;
use Mouse;
use JSON qw(from_json to_json);
use MIME::Base64 qw(encode_base64url decode_base64url);
use Crypt::URandom;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_OK
PE_ERROR
PE_SENDRESPONSE
PE_BADCREDENTIALS
);
our $VERSION = '2.0.12';
extends 'Lemonldap::NG::Portal::Main::SecondFactor';
with 'Lemonldap::NG::Portal::Lib::WebAuthn';
# INITIALIZATION
has rule => ( is => 'rw' );
has prefix => ( is => 'ro', default => 'webauthn' );
has logo => ( is => 'rw', default => 'webauthn.png' );
sub init {
my ($self) = @_;
# If self registration is enabled and "activation" is just set to
# "enabled", replace the rule to detect if user has registered its key
if ( $self->conf->{webauthn2fSelfRegistration}
and $self->conf->{webauthn2fActivation} eq '1' )
{
$self->conf->{webauthn2fActivation} =
'$_2fDevices && $_2fDevices =~ /"type"\s*:\s*"WebAuthn"/s';
}
return 0
unless ( $self->Lemonldap::NG::Portal::Main::SecondFactor::init() );
return 1;
}
# RUNNING METHODS
# Main method
sub run {
my ( $self, $req, $token ) = @_;
my $user = $req->user;
my $checkLogins = $req->param('checkLogins');
$self->logger->debug("WebAuthn: checkLogins set") if $checkLogins;
my $stayconnected = $req->param('stayconnected');
$self->logger->debug("WebAuthn: stayconnected set") if $stayconnected;
my $request = $self->generateChallenge( $req, $req->sessionInfo );
unless ($request) {
$self->logger->error(
"No registered WebAuthn devices for " . $req->user );
return PE_ERROR;
}
$self->ott->updateToken( $token, _webauthn_request => $request );
my $tmp = $self->p->sendHtml(
$req,
'webauthn2fcheck',
params => {
MAIN_LOGO => $self->conf->{portalMainLogo},
SKIN => $self->p->getSkin($req),
DATA => to_json( { request => $request } ),
TOKEN => $token,
CHECKLOGINS => $checkLogins,
STAYCONNECTED => $stayconnected
}
);
$req->response($tmp);
return PE_SENDRESPONSE;
}
sub verify {
my ( $self, $req, $session ) = @_;
my $user = $session->{ $self->conf->{whatToTrace} };
my $credential_json = $req->param('credential');
unless ($credential_json) {
$self->logger->error('Missing signature parameter');
return PE_ERROR;
}
my $signature_options = $session->{_webauthn_request};
delete $session->{_webauthn_request};
my $validation_result = eval {
$self->validateAssertion( $req, $session, $signature_options,
$credential_json );
};
if ($@) {
$self->logger->error("Webauthn validation error for $user: $@");
return PE_ERROR;
}
if ( $validation_result->{success} == 1 ) {
return PE_OK;
}
else {
$self->logger->error(
"Webauthn validation did not return success for $user");
return PE_ERROR;
}
}
1;

View File

@ -0,0 +1,342 @@
package Lemonldap::NG::Portal::Lib::WebAuthn;
use strict;
use Mouse::Role;
use MIME::Base64 qw(encode_base64url decode_base64url);
use JSON qw(decode_json from_json to_json);
use Digest::SHA qw(sha256);
use URI;
use Carp;
our $VERSION = '2.0.12';
has rp_id => ( is => 'rw', lazy => 1, builder => "_build_rp_id" );
has origin => ( is => 'rw', lazy => 1, builder => "_build_origin" );
has type => ( is => 'ro', default => 'WebAuthn' );
has verifier => ( is => 'rw', lazy => 1, builder => "_build_verifier" );
sub _build_verifier {
my $self = shift;
return Authen::WebAuthn->new(
rp_id => $self->rp_id,
origin => $self->origin,
);
}
sub _build_rp_id {
my ($self) = @_;
# TODO make this configurable
my $portal_uri = URI->new( $self->{conf}->{portal} );
return $portal_uri->authority;
}
sub _build_origin {
my ($self) = @_;
my $portal_uri = URI->new( $self->{conf}->{portal} );
return ( $portal_uri->scheme . "://" . $portal_uri->authority );
}
around 'init' => sub {
my $orig = shift;
my $self = shift;
eval { require Authen::WebAuthn };
if ($@) {
$self->logger->error("Can't load WebAuthn library: $@");
$self->error("Can't load WebAuthn library: $@");
return 0;
}
return $orig->( $self, @_ );
};
sub getUserHandle {
my ( $self, $req, $data ) = @_;
return $data->{_webAuthnUserHandle};
}
sub setUserHandle {
my ( $self, $req, $user_handle ) = @_;
$self->p->updatePersistentSession( $req,
{ _webAuthnUserHandle => $user_handle } );
return;
}
sub generateChallenge {
my ( $self, $req, $data ) = @_;
# Find webauthn devices for user
my @webauthn_devices = $self->find2fByType( $req, $data, $self->type );
unless (@webauthn_devices) {
return;
}
my $challenge_base64 = encode_base64url( Crypt::URandom::urandom(32) );
my $userVerification = $self->conf->{webauthn2fUserVerification};
return {
challenge => $challenge_base64,
allowCredentials => [
map { { type => "public-key", id => $_->{_credentialId}, } }
@webauthn_devices
],
( $userVerification ? ( userVerification => $userVerification ) : () ),
extensions => {
appid => $self->origin,
},
};
}
sub validateCredential {
my ( $self, $req, $registration_options, $credential_json ) = @_;
my $credential = from_json($credential_json);
my $client_data_json_b64 = $credential->{response}->{clientDataJSON};
my $attestation_object_b64 = $credential->{response}->{attestationObject};
my $requested_uv =
$registration_options->{authenticatorSelection}->{userVerification} || "";
my $challenge_b64 = $registration_options->{challenge};
my $token_binding_id_b64 = encode_base64url(
$req->headers->header('Sec-Provided-Token-Binding-ID') );
return $self->verifier->validate_registration(
challenge_b64 => $challenge_b64,
requested_uv => $requested_uv,
client_data_json_b64 => $client_data_json_b64,
attestation_object_b64 => $attestation_object_b64,
token_binding_id_b64 => $token_binding_id_b64
);
}
sub validateAssertion {
my ( $self, $req, $data, $signature_options, $credential_json ) = @_;
my $user = $data->{ $self->conf->{whatToTrace} };
$self->logger->debug("Get asserted credential $credential_json");
my $credential = from_json($credential_json);
my $credential_id = $credential->{id};
croak("Empty credential id in credential response") unless $credential_id;
# 5. If options.allowCredentials is not empty, verify that credential.id
# identifies one of the public key credentials listed in
# options.allowCredentials.
my @allowed_credential_ids =
map { $_->{id} } @{ $signature_options->{allowCredentials} };
if ( @allowed_credential_ids
and not grep { $_ eq $credential_id } @allowed_credential_ids )
{
croak("Received credential ID $credential_id was not requested");
}
# 6. Identify the user being authenticated and verify that this user is the
# owner of the public key credential source credentialSource identified by
# credential.id If the user was identified before the authentication
# ceremony was initiated, e.g., via a username or cookie, verify that the
# identified user is the owner of credentialSource.
my @webauthn_devices = $self->find2fByType( $req, $data, $self->type );
my @matching_credentials =
grep { $_->{_credentialId} eq $credential_id } @webauthn_devices;
if ( @matching_credentials < 1 ) {
croak("Received credential ID $credential_id does not belong to user");
}
if ( @matching_credentials > 1 ) {
croak("Found multiple credentials with ID $credential_id for user");
}
my $matching_credential = $matching_credentials[0];
# If response.userHandle is present, let userHandle be its value.
# Verify that userHandle also maps to the same user.
if ( $credential->{response}->{userHandle} ) {
my $user_handle = $credential->{response}->{userHandle};
my $current_user_handle = $self->getUserHandle( $req, $data );
unless ( $user_handle eq $current_user_handle ) {
croak(
"Received user handle ($user_handle) does not match current user ($current_user_handle)"
);
}
}
# TODO If the user was not identified before the authentication ceremony
# was initiated, verify that response.userHandle is present, and that the
# user identified by this value is the owner of credentialSource.
# NOTE: irrelevant for now, take this into account when implementing
# Auth::WebAuthn
my $client_data_json_b64 = $credential->{response}->{clientDataJSON};
my $authenticator_data_b64 = $credential->{response}->{authenticatorData};
my $signature_b64 = $credential->{response}->{signature};
my $extension_results = $credential->{clientExtensionResults};
my $requested_uv = $signature_options->{userVerification} || "";
my $token_binding_id_b64 = encode_base64url(
$req->headers->header('Sec-Provided-Token-Binding-ID') );
my $validation_result = $self->verifier->validate_assertion(
challenge_b64 => $signature_options->{challenge},
credential_pubkey_b64 => $matching_credential->{_credentialPublicKey},
stored_sign_count => $matching_credential->{_signCount},
requested_uv => $requested_uv,
client_data_json_b64 => $client_data_json_b64,
authenticator_data_b64 => $authenticator_data_b64,
signature_b64 => $signature_b64,
extension_results => $extension_results,
token_binding_id_b64 => $token_binding_id_b64,
);
if ( $validation_result->{success} == 1 ) {
my $new_signature_count = $validation_result->{signature_count};
$self->userLogger->info(
"Successfully verified signature with count "
. "$new_signature_count for $user" );
# Update storedSignCount to be the value of authData.signCount
$self->update2fDevice( $req, $data, $self->type,
"_credentialId", $credential_id, "_signCount",
$new_signature_count );
}
return $validation_result;
}
sub decode_credential {
my ( $self, $json ) = @_;
my $credential = decode_json($json);
# Decode ClientDataJSON
if ( $credential->{response}->{clientDataJSON} ) {
$credential->{response}->{clientDataJSON} = decode_json(
decode_base64url( $credential->{response}->{clientDataJSON} ) );
}
# Decode attestation object
if ( $credential->{response}->{attestationObject} ) {
$credential->{response}->{attestationObject} =
getAttestationObject( $credential->{response}->{attestationObject} );
}
# Decode authenticator data
if ( $credential->{response}->{authenticatorData} ) {
$credential->{response}->{authenticatorData} =
getAuthData(
decode_base64url( $credential->{response}->{authenticatorData} ) );
}
# Decode rawID
if ( $credential->{rawId} ) {
$credential->{rawId} = decode_base64url( $credential->{rawId} );
}
return $credential;
}
sub update2fDevice {
my ( $self, $req, $info, $type, $key, $value, $update_key, $update_value )
= @_;
my $user = $info->{ $self->conf->{whatToTrace} };
my $_2fDevices = $self->get2fDevices( $req, $info );
return 0 unless $_2fDevices;
my @found =
grep { $_->{type} eq $type and $_->{$key} eq $value } @{$_2fDevices};
for my $device (@found) {
$device->{$update_key} = $update_value;
}
if (@found) {
$self->p->updatePersistentSession( $req,
{ _2fDevices => to_json($_2fDevices) }, $user );
return 1;
}
return 0;
}
sub add2fDevice {
my ( $self, $req, $info, $device ) = @_;
my $_2fDevices = $self->get2fDevices( $req, $info );
push @{$_2fDevices}, $device;
$self->logger->debug(
"Append 2F Device: { type => 'Webauthn', name => $device->{name} }");
$self->p->updatePersistentSession( $req,
{ _2fDevices => to_json($_2fDevices) } );
return 1;
}
sub del2fDevice {
my ( $self, $req, $info, $type, $epoch ) = @_;
my $_2fDevices = $self->get2fDevices( $req, $info );
return 0 unless $_2fDevices;
my @updated_2fDevices =
grep { not( $_->{type} eq $type and $_->{epoch} eq $epoch ) }
@{$_2fDevices};
$self->logger->debug(
"Deleted 2F Device: { type => $type, epoch => $epoch }");
$self->p->updatePersistentSession( $req,
{ _2fDevices => to_json( [@updated_2fDevices] ) } );
return 1;
}
sub find2fByKey {
my ( $self, $req, $info, $type, $key, $value ) = @_;
my $_2fDevices = $self->get2fDevices( $req, $info );
return unless $_2fDevices;
my @found =
grep { $_->{type} eq $type and $_->{$key} eq $value } @{$_2fDevices};
return @found;
}
## @method get2fDevices($req, $info)
# Validate logout request
# @param req Request object
# @param info HashRef of session data
# @return undef or ArrayRef of second factors
sub get2fDevices {
my ( $self, $req, $info ) = @_;
my $_2fDevices;
if ( $info->{_2fDevices} ) {
$_2fDevices =
eval { from_json( $info->{_2fDevices}, { allow_nonref => 1 } ); };
if ($@) {
$self->logger->error("Corrupted session (_2fDevices): $@");
return;
}
}
else {
# Return new ArrayRef
return [];
}
if ( ref($_2fDevices) eq "ARRAY" ) {
return $_2fDevices;
}
else {
return;
}
}
sub find2fByType {
my ( $self, $req, $info, $type ) = @_;
my $_2fDevices = $self->get2fDevices( $req, $info );
return unless $_2fDevices;
return @{$_2fDevices} unless $type;
my @found = grep { $_->{type} eq $type } @{$_2fDevices};
return @found;
}
1;

View File

@ -28,9 +28,11 @@ delete2F = (device, epoch) ->
device = 'u'
else if device == 'UBK'
device = 'yubikey'
else if device == 'TOTP'
device = 'totp'
else setMsg 'u2fFailed', 'warning'
else if device == 'TOTP'
device = 'totp'
else if device == 'WebAuthn'
device = 'webauthn'
else setMsg 'u2fFailed', 'warning'
$.ajax
type: "POST"
url: "#{portal}2fregisters/#{device}/delete"

View File

@ -0,0 +1,30 @@
###
LemonLDAP::NG WebAuthn verify script
###
setMsg = (msg, level) ->
$('#msg').attr 'trspan', msg
$('#msg').html window.translate msg
$('#color').removeClass 'message-positive message-warning message-danger alert-success alert-warning alert-danger'
$('#color').addClass "message-#{level}"
level = 'success' if level == 'positive'
$('#color').addClass "alert-#{level}"
webAuthnError = (error) ->
switch (error.name)
when 'unsupported' then setMsg 'webAuthnUnsupported', 'warning'
else setMsg 'webAuthnBrowserFailed', 'danger'
check = ->
setMsg 'webAuthnBrowserInProgress', 'warning'
request = window.datas.request
WebAuthnUI.WebAuthnUI.getCredential request
. then (response) ->
$('#credential').val JSON.stringify response
$('#verify-form').submit()
. catch (error) ->
webAuthnError(error)
$(document).ready ->
setTimeout check, 1000
$('#retrybutton').on 'click', check

View File

@ -0,0 +1,97 @@
###
LemonLDAP::NG WebAuthn registration script
###
setMsg = (msg, level) ->
$('#msg').attr 'trspan', msg
$('#msg').html window.translate msg
$('#color').removeClass 'message-positive message-warning message-danger alert-success alert-warning alert-danger'
$('#color').addClass "message-#{level}"
level = 'success' if level == 'positive'
$('#color').addClass "alert-#{level}"
displayError = (j, status, err) ->
console.log 'Error', err
res = JSON.parse j.responseText
if res and res.error
res = res.error.replace(/.* /, '')
console.log 'Returned error', res
setMsg res, 'danger'
webAuthnError = (error) ->
switch (error.name)
when 'unsupported' then setMsg 'webAuthnUnsupported', 'warning'
else setMsg 'webAuthnBrowserFailed', 'danger'
# Registration function (launched by "register" button)
register = ->
# 1 get registration token
$.ajax
type: "POST",
url: "#{portal}2fregisters/webauthn/registrationchallenge"
data: {}
dataType: 'json'
error: displayError
success: (ch) ->
# 2 build response
request = ch.request
setMsg 'webAuthnRegisterInProgress', 'warning'
$('#u2fPermission').show()
WebAuthnUI.WebAuthnUI.createCredential request
. then (response) ->
$.ajax
type: "POST"
url: "#{portal}2fregisters/webauthn/registration"
data:
state_id: ch.state_id
credential: JSON.stringify response
keyName: $('#keyName').val()
dataType: 'json'
success: (resp) ->
if resp.error
if resp.error.match /badName/
setMsg resp.error, 'danger'
else setMsg 'webAuthnRegisterFailed', 'danger'
else if resp.result
setMsg 'yourKeyIsRegistered', 'positive'
error: displayError
. catch (error) ->
webAuthnError(error)
# Verification function (launched by "verify" button)
verify = ->
# 1 get challenge
$.ajax
type: "POST",
url: "#{portal}2fregisters/webauthn/verificationchallenge"
data: {}
dataType: 'json'
error: displayError
success: (ch) ->
# 2 build response
request = ch.request
setMsg 'webAuthnBrowserInProgress', 'warning'
WebAuthnUI.WebAuthnUI.getCredential request
. then (response) ->
$.ajax
type: "POST"
url: "#{portal}2fregisters/webauthn/verification"
data:
state_id: ch.state_id
credential: JSON.stringify response
dataType: 'json'
success: (resp) ->
if resp.error
setMsg 'webAuthnFailed', 'danger'
else if resp.result
setMsg 'yourKeyIsVerified', 'positive'
error: displayError
. catch (error) ->
webAuthnError(error)
# Register "click" events
$(document).ready ->
$('#u2fPermission').hide()
$('#register').on 'click', register
$('#verify').on 'click', verify
$('#goback').attr 'href', portal

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -41,6 +41,8 @@ LemonLDAP::NG 2F registration script
device = 'yubikey';
} else if (device === 'TOTP') {
device = 'totp';
} else if (device === 'WebAuthn') {
device = 'webauthn';
} else {
setMsg('u2fFailed', 'warning');
}

View File

@ -1 +1 @@
!function(){var o=function(e,r){return $("#msg").attr("trspan",e),$("#msg").html(window.translate(e)),$("#color").removeClass("message-positive message-warning alert-success alert-warning"),$("#color").addClass("message-"+r),"positive"===r&&(r="success"),$("#color").addClass("alert-"+r),$("#color").attr("role","status")},t=function(e,r,t){if(console.log("Error",t),(e=JSON.parse(e.responseText))&&e.error)return e=e.error.replace(/.* /,""),console.log("Returned error",e),e.match(/module/)?o("notAuthorized","warning"):o(e,"warning")},e=function(e,r){return"U2F"===e?e="u":"UBK"===e?e="yubikey":"TOTP"===e?e="totp":o("u2fFailed","warning"),$.ajax({type:"POST",url:portal+"2fregisters/"+e+"/delete",data:{epoch:r},dataType:"json",error:t,success:function(e){return e.error?e.error.match(/notAuthorized/)?o("notAuthorized","warning"):o("unknownAction","warning"):e.result?($("#delete-"+r).hide(),o("yourKeyIsUnregistered","positive")):void 0}})};$(document).ready(function(){return $("body").on("click",".remove2f",function(){return e($(this).attr("device"),$(this).attr("epoch"))}),$("#goback").attr("href",portal),$(".data-epoch").each(function(){var e=new Date(1e3*$(this).text());return $(this).text(e.toLocaleString())})})}.call(this);
!function(){var o=function(e,r){return $("#msg").attr("trspan",e),$("#msg").html(window.translate(e)),$("#color").removeClass("message-positive message-warning alert-success alert-warning"),$("#color").addClass("message-"+r),"positive"===r&&(r="success"),$("#color").addClass("alert-"+r),$("#color").attr("role","status")},t=function(e,r,t){if(console.log("Error",t),(e=JSON.parse(e.responseText))&&e.error)return e=e.error.replace(/.* /,""),console.log("Returned error",e),e.match(/module/)?o("notAuthorized","warning"):o(e,"warning")},e=function(e,r){return"U2F"===e?e="u":"UBK"===e?e="yubikey":"TOTP"===e?e="totp":"WebAuthn"===e?e="webauthn":o("u2fFailed","warning"),$.ajax({type:"POST",url:portal+"2fregisters/"+e+"/delete",data:{epoch:r},dataType:"json",error:t,success:function(e){return e.error?e.error.match(/notAuthorized/)?o("notAuthorized","warning"):o("unknownAction","warning"):e.result?($("#delete-"+r).hide(),o("yourKeyIsUnregistered","positive")):void 0}})};$(document).ready(function(){return $("body").on("click",".remove2f",function(){return e($(this).attr("device"),$(this).attr("epoch"))}),$("#goback").attr("href",portal),$(".data-epoch").each(function(){var e=new Date(1e3*$(this).text());return $(this).text(e.toLocaleString())})})}.call(this);

View File

@ -1 +1 @@
{"version":3,"sources":["2fregistration.js"],"names":["setMsg","msg","level","$","attr","html","window","translate","removeClass","addClass","displayError","j","status","err","console","log","res","JSON","parse","responseText","error","replace","match","delete2F","device","epoch","ajax","type","url","portal","data","dataType","success","resp","result","hide","document","ready","on","this","each","myDate","Date","text","toLocaleString","call"],"mappings":"CAMA,WACE,IAEAA,EAAS,SAASC,EAAKC,GASrB,OARAC,EAAE,QAAQC,KAAK,SAAUH,GACzBE,EAAE,QAAQE,KAAKC,OAAOC,UAAUN,IAChCE,EAAE,UAAUK,YAAY,gEACxBL,EAAE,UAAUM,SAAS,WAAaP,GACpB,aAAVA,IACFA,EAAQ,WAEVC,EAAE,UAAUM,SAAS,SAAWP,GACzBC,EAAE,UAAUC,KAAK,OAAQ,WAGlCM,EAAe,SAASC,EAAGC,EAAQC,GAIjC,GAFAC,QAAQC,IAAI,QAASF,IACrBG,EAAMC,KAAKC,MAAMP,EAAEQ,gBACRH,EAAII,MAGb,OAFAJ,EAAMA,EAAII,MAAMC,QAAQ,MAAO,IAC/BP,QAAQC,IAAI,iBAAkBC,GAC1BA,EAAIM,MAAM,UACLtB,EAAO,gBAAiB,WAExBA,EAAOgB,EAAK,YAKzBO,EAAW,SAASC,EAAQC,GAU1B,MATe,QAAXD,EACFA,EAAS,IACW,QAAXA,EACTA,EAAS,UACW,SAAXA,EACTA,EAAS,OAETxB,EAAO,YAAa,WAEfG,EAAEuB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,eAAiBL,EAAS,UACxCM,KAAM,CACJL,MAAOA,GAETM,SAAU,OANEX,MAoBLV,EAZPsB,QAAS,SAASC,GAChB,OAAIA,EAAKb,MACHa,EAAKb,MAAME,MAAM,iBACZtB,EAAO,gBAAiB,WAExBA,EAAO,gBAAiB,WAExBiC,EAAKC,QACd/B,EAAE,WAAasB,GAAOU,OACfnC,EAAO,wBAAyB,kBAFlC,MASbG,EAAEiC,UAAUC,MAAM,WAKhB,OAJAlC,EAAE,QAAQmC,GAAG,QAAS,YAAa,WACjC,OAAOf,EAASpB,EAAEoC,MAAMnC,KAAK,UAAWD,EAAEoC,MAAMnC,KAAK,YAEvDD,EAAE,WAAWC,KAAK,OAAQyB,QACnB1B,EAAE,eAAeqC,KAAK,WAC3B,IACAC,EAAS,IAAIC,KAAsB,IAAjBvC,EAAEoC,MAAMI,QAC1B,OAAOxC,EAAEoC,MAAMI,KAAKF,EAAOG,uBAI9BC,KAAKN"}
{"version":3,"sources":["2fregistration.js"],"names":["setMsg","msg","level","$","attr","html","window","translate","removeClass","addClass","displayError","j","status","err","console","log","res","JSON","parse","responseText","error","replace","match","delete2F","device","epoch","ajax","type","url","portal","data","dataType","success","resp","result","hide","document","ready","on","this","each","myDate","Date","text","toLocaleString","call"],"mappings":"CAMA,WACE,IAEAA,EAAS,SAASC,EAAKC,GASrB,OARAC,EAAE,QAAQC,KAAK,SAAUH,GACzBE,EAAE,QAAQE,KAAKC,OAAOC,UAAUN,IAChCE,EAAE,UAAUK,YAAY,gEACxBL,EAAE,UAAUM,SAAS,WAAaP,GACpB,aAAVA,IACFA,EAAQ,WAEVC,EAAE,UAAUM,SAAS,SAAWP,GACzBC,EAAE,UAAUC,KAAK,OAAQ,WAGlCM,EAAe,SAASC,EAAGC,EAAQC,GAIjC,GAFAC,QAAQC,IAAI,QAASF,IACrBG,EAAMC,KAAKC,MAAMP,EAAEQ,gBACRH,EAAII,MAGb,OAFAJ,EAAMA,EAAII,MAAMC,QAAQ,MAAO,IAC/BP,QAAQC,IAAI,iBAAkBC,GAC1BA,EAAIM,MAAM,UACLtB,EAAO,gBAAiB,WAExBA,EAAOgB,EAAK,YAKzBO,EAAW,SAASC,EAAQC,GAY1B,MAXe,QAAXD,EACFA,EAAS,IACW,QAAXA,EACTA,EAAS,UACW,SAAXA,EACTA,EAAS,OACW,aAAXA,EACTA,EAAS,WAETxB,EAAO,YAAa,WAEfG,EAAEuB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,eAAiBL,EAAS,UACxCM,KAAM,CACJL,MAAOA,GAETM,SAAU,OANEX,MAoBLV,EAZPsB,QAAS,SAASC,GAChB,OAAIA,EAAKb,MACHa,EAAKb,MAAME,MAAM,iBACZtB,EAAO,gBAAiB,WAExBA,EAAO,gBAAiB,WAExBiC,EAAKC,QACd/B,EAAE,WAAasB,GAAOU,OACfnC,EAAO,wBAAyB,kBAFlC,MASbG,EAAEiC,UAAUC,MAAM,WAKhB,OAJAlC,EAAE,QAAQmC,GAAG,QAAS,YAAa,WACjC,OAAOf,EAASpB,EAAEoC,MAAMnC,KAAK,UAAWD,EAAEoC,MAAMnC,KAAK,YAEvDD,EAAE,WAAWC,KAAK,OAAQyB,QACnB1B,EAAE,eAAeqC,KAAK,WAC3B,IACAC,EAAS,IAAIC,KAAsB,IAAjBvC,EAAEoC,MAAMI,QAC1B,OAAOxC,EAAEoC,MAAMI,KAAKF,EAAOG,uBAI9BC,KAAKN"}

View File

@ -0,0 +1,561 @@
/*! webauthn-ui library (C) 2018 - 2020 Thomas Bleeker (www.madwizard.org) - MIT license */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WebAuthnUI = {}));
}(this, (function (exports) { 'use strict';
/* Microsoft tslib 0BSD licensed */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
function waitReadyState(alreadyDone, eventDispatcher, eventName) {
if (alreadyDone) {
return Promise.resolve();
}
return new Promise(function (resolve) {
var readyFunc = function () {
eventDispatcher.removeEventListener(eventName, readyFunc);
resolve();
};
eventDispatcher.addEventListener(eventName, readyFunc);
});
}
function ready() {
return waitReadyState(document.readyState !== 'loading', document, 'DOMContentLoaded');
}
function loaded() {
return waitReadyState(document.readyState === 'complete', window, 'load');
}
var WebAuthnError = /** @class */ (function (_super) {
__extends(WebAuthnError, _super);
function WebAuthnError(name, message, innerError) {
var _newTarget = this.constructor;
var _this = _super.call(this, "WebAuthnUI error: " + (message !== undefined ? message : name)) || this;
Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain
_this.name = name;
_this.innerError = innerError;
return _this;
}
WebAuthnError.fromError = function (error) {
var type = 'unknown';
var message;
if (error instanceof DOMException) {
var map = {
NotAllowedError: 'dom-not-allowed',
SecurityError: 'dom-security',
NotSupportedError: 'dom-not-supported',
AbortError: 'dom-abort',
InvalidStateError: 'dom-invalid-state',
};
type = map[error.name] || 'dom-unknown';
message = type;
}
else {
message = "unknown (" + error.toString() + ")";
}
return new WebAuthnError(type, message, error instanceof Error ? error : undefined);
};
return WebAuthnError;
}(Error));
function encode(arraybuffer) {
var buffer = new Uint8Array(arraybuffer);
var binary = '';
for (var i_1 = 0; i_1 < buffer.length; i_1++) {
binary += String.fromCharCode(buffer[i_1]);
}
var encoded = window.btoa(binary);
var i = encoded.length - 1;
while (i > 0 && encoded[i] === '=') {
i--;
}
encoded = encoded.substr(0, i + 1);
encoded = encoded.replace(/\+/g, '-').replace(/\//g, '_');
return encoded;
}
function decode(base64) {
var converted = base64.replace(/-/g, '+').replace(/_/g, '/');
switch (converted.length % 4) {
case 2:
converted += '==';
break;
case 3:
converted += '=';
break;
case 1:
throw new WebAuthnError('parse-error');
}
var bin = window.atob(converted);
var buffer = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) {
buffer[i] = bin.charCodeAt(i);
}
return buffer;
}
function map(src, mapper) {
var dest = {};
var keys = Object.keys(mapper);
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
var action = mapper[k];
var val = src[k];
if (val !== undefined) {
if (action === 0 /* Copy */) {
dest[k] = val;
}
else if (action === 1 /* Base64Decode */) {
dest[k] = val === null ? null : decode(val);
}
else if (action === 2 /* Base64Encode */) {
dest[k] = val === null ? null : encode(val);
}
else if (typeof action === 'object') {
dest[k] = map(val, action);
}
else {
dest[k] = action(val);
}
}
}
return dest;
}
function arrayMap(mapper) {
return function (src) {
var dest = [];
for (var i = 0; i < src.length; i++) {
dest[i] = map(src[i], mapper);
}
return dest;
};
}
function getCredentialDescListMap() {
return arrayMap({
type: 0 /* Copy */,
id: 1 /* Base64Decode */,
transports: 0 /* Copy */,
});
}
function addExtensionOutputs(dest, pkc) {
var clientExtensionResults = pkc.getClientExtensionResults();
if (Object.keys(clientExtensionResults).length > 0) {
dest.clientExtensionResults = map(clientExtensionResults, {
appid: 0 /* Copy */,
});
}
}
var Converter = /** @class */ (function () {
function Converter() {
}
Converter.convertCreationOptions = function (options) {
return map(options, {
rp: 0 /* Copy */,
user: {
id: 1 /* Base64Decode */,
name: 0 /* Copy */,
displayName: 0 /* Copy */,
icon: 0 /* Copy */,
},
challenge: 1 /* Base64Decode */,
pubKeyCredParams: 0 /* Copy */,
timeout: 0 /* Copy */,
excludeCredentials: getCredentialDescListMap(),
authenticatorSelection: 0 /* Copy */,
attestation: 0 /* Copy */,
extensions: {
appid: 0 /* Copy */,
},
});
};
Converter.convertCreationResponse = function (pkc) {
var response = map(pkc, {
type: 0 /* Copy */,
id: 0 /* Copy */,
rawId: 2 /* Base64Encode */,
response: {
clientDataJSON: 2 /* Base64Encode */,
attestationObject: 2 /* Base64Encode */,
},
});
addExtensionOutputs(response, pkc);
return response;
};
Converter.convertRequestOptions = function (options) {
return map(options, {
challenge: 1 /* Base64Decode */,
timeout: 0 /* Copy */,
rpId: 0 /* Copy */,
allowCredentials: getCredentialDescListMap(),
userVerification: 0 /* Copy */,
extensions: {
appid: 0 /* Copy */,
},
});
};
Converter.convertRequestResponse = function (pkc) {
var response = map(pkc, {
type: 0 /* Copy */,
id: 0 /* Copy */,
rawId: 2 /* Base64Encode */,
response: {
clientDataJSON: 2 /* Base64Encode */,
authenticatorData: 2 /* Base64Encode */,
signature: 2 /* Base64Encode */,
userHandle: 2 /* Base64Encode */,
},
});
addExtensionOutputs(response, pkc);
return response;
};
return Converter;
}());
var loadEvents = { loaded: loaded, ready: ready };
function elementSelector(selector) {
var items;
if (typeof selector === 'string') {
items = document.querySelectorAll(selector);
}
else {
items = [selector];
}
return items;
}
var WebAuthnUI = /** @class */ (function () {
function WebAuthnUI() {
}
WebAuthnUI.isSupported = function () {
return typeof window.PublicKeyCredential !== 'undefined';
};
WebAuthnUI.isUVPASupported = function () {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
if (this.isSupported()) {
return [2 /*return*/, window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()];
}
return [2 /*return*/, false];
});
});
};
WebAuthnUI.checkSupport = function () {
if (!WebAuthnUI.isSupported()) {
throw new WebAuthnError('unsupported');
}
};
WebAuthnUI.createCredential = function (options) {
return __awaiter(this, void 0, void 0, function () {
var request, credential, e_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
WebAuthnUI.checkSupport();
request = {
publicKey: Converter.convertCreationOptions(options),
};
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, navigator.credentials.create(request)];
case 2:
credential = (_a.sent());
return [3 /*break*/, 4];
case 3:
e_1 = _a.sent();
throw WebAuthnError.fromError(e_1);
case 4: return [2 /*return*/, Converter.convertCreationResponse(credential)];
}
});
});
};
WebAuthnUI.getCredential = function (options) {
return __awaiter(this, void 0, void 0, function () {
var request, credential, e_2;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
WebAuthnUI.checkSupport();
request = {
publicKey: Converter.convertRequestOptions(options),
};
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, navigator.credentials.get(request)];
case 2:
credential = (_a.sent());
return [3 /*break*/, 4];
case 3:
e_2 = _a.sent();
throw WebAuthnError.fromError(e_2);
case 4: return [2 /*return*/, Converter.convertRequestResponse(credential)];
}
});
});
};
WebAuthnUI.setFeatureCssClasses = function (selector) {
return __awaiter(this, void 0, void 0, function () {
var items, applyClass;
return __generator(this, function (_a) {
items = elementSelector(selector);
applyClass = function (cls) {
for (var i = 0; i < items.length; i++) {
items[i].classList.add(cls);
}
};
applyClass("webauthn-" + (WebAuthnUI.isSupported() ? '' : 'un') + "supported");
return [2 /*return*/, WebAuthnUI.isUVPASupported().then(function (available) {
applyClass("webauthn-uvpa-" + (available ? '' : 'un') + "supported");
})];
});
});
};
WebAuthnUI.loadConfig = function (config) {
return __awaiter(this, void 0, void 0, function () {
var field, el, submit, response, newField;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
// Wait for DOM ready
return [4 /*yield*/, ready()];
case 1:
// Wait for DOM ready
_a.sent();
field = config.formField;
if (typeof field === 'string') {
el = document.querySelector(field);
if (el === null) {
throw new WebAuthnError('bad-config', 'Could not find formField.');
}
field = el;
}
if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement)) {
throw new WebAuthnError('bad-config', 'formField does not refer to an input element.');
}
submit = config.submitForm !== false;
if (!this.isSupported() && config.postUnsupportedImmediately === true) {
response = { status: 'failed', error: 'unsupported' };
this.setForm(field, response, submit);
return [2 /*return*/, response];
}
newField = field;
return [2 /*return*/, new Promise(function (resolve) {
var trigger = config.trigger;
var resolved = false;
if (trigger.event === 'click') {
var targets = elementSelector(config.trigger.element);
var handler = function () { return __awaiter(_this, void 0, void 0, function () {
var response;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.runAutoConfig(config)];
case 1:
response = _a.sent();
this.setForm(newField, response, submit);
if (!resolved) {
resolved = true;
resolve(response);
}
return [2 /*return*/];
}
});
}); };
for (var i = 0; i < targets.length; i++) {
targets[i].addEventListener('click', handler);
}
}
else {
throw new WebAuthnError('bad-config');
}
})];
}
});
});
};
WebAuthnUI.startConfig = function (config) {
return __awaiter(this, void 0, void 0, function () {
var credential;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!(config.type === 'get')) return [3 /*break*/, 2];
return [4 /*yield*/, this.getCredential(config.request)];
case 1:
credential = _a.sent();
return [3 /*break*/, 5];
case 2:
if (!(config.type === 'create')) return [3 /*break*/, 4];
return [4 /*yield*/, this.createCredential(config.request)];
case 3:
credential = _a.sent();
return [3 /*break*/, 5];
case 4: throw new WebAuthnError('bad-config', "Invalid config.type " + config.type);
case 5: return [2 /*return*/, {
status: 'ok',
credential: credential,
}];
}
});
});
};
WebAuthnUI.runAutoConfig = function (config) {
return __awaiter(this, void 0, void 0, function () {
var response, e_3, waError;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.startConfig(config)];
case 1:
response = _a.sent();
return [3 /*break*/, 3];
case 2:
e_3 = _a.sent();
waError = (e_3 instanceof WebAuthnError);
if (config.debug === true) {
console.error(e_3); // eslint-disable-line no-console
if (waError && e_3.innerError) {
console.error(e_3.innerError); // eslint-disable-line no-console
}
}
response = {
status: 'failed',
error: (waError ? e_3.name : WebAuthnError.fromError(e_3).name),
};
return [3 /*break*/, 3];
case 3: return [2 /*return*/, response];
}
});
});
};
WebAuthnUI.setForm = function (field, response, submit) {
field.value = JSON.stringify(response);
if (submit && field.form) {
field.form.submit();
}
};
WebAuthnUI.autoConfig = function () {
return __awaiter(this, void 0, void 0, function () {
var promises, list, i, el, isScript, rawJson, json;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
promises = [];
list = document.querySelectorAll('input[data-webauthn],textarea[data-webauthn],script[data-webauthn]');
for (i = 0; i < list.length; i++) {
el = list[i];
isScript = el.tagName === 'SCRIPT';
if (isScript && el.type !== 'application/json') {
throw new WebAuthnError('bad-config', 'Expecting application/json script with data-webauthn');
}
rawJson = isScript ? el.textContent : (el).getAttribute('data-webauthn');
if (rawJson === null) {
throw new WebAuthnError('bad-config', 'Missing JSON in data-webauthn');
}
json = void 0;
try {
json = JSON.parse(rawJson);
}
catch (e) {
throw new WebAuthnError('bad-config', 'invalid JSON in data-webauthn on element');
}
if (!isScript && json.formField === undefined) {
json.formField = el;
}
promises.push(this.loadConfig(json));
}
return [4 /*yield*/, Promise.all(promises)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
};
WebAuthnUI.inProgress = false;
return WebAuthnUI;
}());
function auto() {
return __awaiter(this, void 0, void 0, function () {
var list, i;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ready()];
case 1:
_a.sent();
list = document.querySelectorAll('.webauthn-detect');
for (i = 0; i < list.length; i++) {
WebAuthnUI.setFeatureCssClasses(list[i]);
}
return [2 /*return*/, WebAuthnUI.autoConfig()];
}
});
});
}
var autoPromise = auto().catch(function (e) {
if (console && console.error) { // eslint-disable-line no-console
console.error(e); // eslint-disable-line no-console
}
});
exports.WebAuthnError = WebAuthnError;
exports.WebAuthnUI = WebAuthnUI;
exports.autoPromise = autoPromise;
exports.loadEvents = loadEvents;
Object.defineProperty(exports, '__esModule', { value: true });
})));

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,47 @@
// Generated by CoffeeScript 1.12.8
/*
LemonLDAP::NG WebAuthn verify script
*/
(function() {
var check, setMsg, webAuthnError;
setMsg = function(msg, level) {
$('#msg').attr('trspan', msg);
$('#msg').html(window.translate(msg));
$('#color').removeClass('message-positive message-warning message-danger alert-success alert-warning alert-danger');
$('#color').addClass("message-" + level);
if (level === 'positive') {
level = 'success';
}
return $('#color').addClass("alert-" + level);
};
webAuthnError = function(error) {
switch (error.name) {
case 'unsupported':
return setMsg('webAuthnUnsupported', 'warning');
default:
return setMsg('webAuthnBrowserFailed', 'danger');
}
};
check = function() {
var request;
setMsg('webAuthnBrowserInProgress', 'warning');
request = window.datas.request;
return WebAuthnUI.WebAuthnUI.getCredential(request).then(function(response) {
$('#credential').val(JSON.stringify(response));
return $('#verify-form').submit();
})["catch"](function(error) {
return webAuthnError(error);
});
};
$(document).ready(function() {
setTimeout(check, 1000);
return $('#retrybutton').on('click', check);
});
}).call(this);

View File

@ -0,0 +1 @@
!function(){var n=function(e,n){return $("#msg").attr("trspan",e),$("#msg").html(window.translate(e)),$("#color").removeClass("message-positive message-warning message-danger alert-success alert-warning alert-danger"),$("#color").addClass("message-"+n),"positive"===n&&(n="success"),$("#color").addClass("alert-"+n)},r=function(e){return"unsupported"!==e.name?n("webAuthnBrowserFailed","danger"):n("webAuthnUnsupported","warning")},e=function(){var e;return n("webAuthnBrowserInProgress","warning"),e=window.datas.request,WebAuthnUI.WebAuthnUI.getCredential(e).then(function(e){return $("#credential").val(JSON.stringify(e)),$("#verify-form").submit()}).catch(r)};$(document).ready(function(){return setTimeout(e,1e3),$("#retrybutton").on("click",e)})}.call(this);

View File

@ -0,0 +1 @@
{"version":3,"sources":["webauthncheck.js"],"names":["setMsg","msg","level","$","attr","html","window","translate","removeClass","addClass","webAuthnError","error","name","check","request","datas","WebAuthnUI","getCredential","then","response","val","JSON","stringify","submit","document","ready","setTimeout","on","call","this"],"mappings":"CAMA,WACE,IAEAA,EAAS,SAASC,EAAKC,GAQrB,OAPAC,EAAE,QAAQC,KAAK,SAAUH,GACzBE,EAAE,QAAQE,KAAKC,OAAOC,UAAUN,IAChCE,EAAE,UAAUK,YAAY,4FACxBL,EAAE,UAAUM,SAAS,WAAaP,GACpB,aAAVA,IACFA,EAAQ,WAEHC,EAAE,UAAUM,SAAS,SAAWP,IAGzCQ,EAAgB,SAASC,GACvB,MACO,gBADCA,EAAMC,KAIHZ,EAAO,wBAAyB,UAFhCA,EAAO,sBAAuB,YAM3Ca,EAAQ,WACN,IAAIC,EAGJ,OAFAd,EAAO,4BAA6B,WACpCc,EAAUR,OAAOS,MAAMD,QAChBE,WAAWA,WAAWC,cAAcH,GAASI,KAAK,SAASC,GAEhE,OADAhB,EAAE,eAAeiB,IAAIC,KAAKC,UAAUH,IAC7BhB,EAAE,gBAAgBoB,WACjB,MACDb,IAIXP,EAAEqB,UAAUC,MAAM,WAEhB,OADAC,WAAWb,EAAO,KACXV,EAAE,gBAAgBwB,GAAG,QAASd,MAGtCe,KAAKC"}

View File

@ -0,0 +1,126 @@
// Generated by CoffeeScript 1.12.8
/*
LemonLDAP::NG WebAuthn registration script
*/
(function() {
var displayError, register, setMsg, verify, webAuthnError;
setMsg = function(msg, level) {
$('#msg').attr('trspan', msg);
$('#msg').html(window.translate(msg));
$('#color').removeClass('message-positive message-warning message-danger alert-success alert-warning alert-danger');
$('#color').addClass("message-" + level);
if (level === 'positive') {
level = 'success';
}
return $('#color').addClass("alert-" + level);
};
displayError = function(j, status, err) {
var res;
console.log('Error', err);
res = JSON.parse(j.responseText);
if (res && res.error) {
res = res.error.replace(/.* /, '');
console.log('Returned error', res);
return setMsg(res, 'danger');
}
};
webAuthnError = function(error) {
switch (error.name) {
case 'unsupported':
return setMsg('webAuthnUnsupported', 'warning');
default:
return setMsg('webAuthnBrowserFailed', 'danger');
}
};
register = function() {
return $.ajax({
type: "POST",
url: portal + "2fregisters/webauthn/registrationchallenge",
data: {},
dataType: 'json',
error: displayError,
success: function(ch) {
var request;
request = ch.request;
setMsg('webAuthnRegisterInProgress', 'warning');
$('#u2fPermission').show();
return WebAuthnUI.WebAuthnUI.createCredential(request).then(function(response) {
return $.ajax({
type: "POST",
url: portal + "2fregisters/webauthn/registration",
data: {
state_id: ch.state_id,
credential: JSON.stringify(response),
keyName: $('#keyName').val()
},
dataType: 'json',
success: function(resp) {
if (resp.error) {
if (resp.error.match(/badName/)) {
return setMsg(resp.error, 'danger');
} else {
return setMsg('webAuthnRegisterFailed', 'danger');
}
} else if (resp.result) {
return setMsg('yourKeyIsRegistered', 'positive');
}
},
error: displayError
});
})["catch"](function(error) {
return webAuthnError(error);
});
}
});
};
verify = function() {
return $.ajax({
type: "POST",
url: portal + "2fregisters/webauthn/verificationchallenge",
data: {},
dataType: 'json',
error: displayError,
success: function(ch) {
var request;
request = ch.request;
setMsg('webAuthnBrowserInProgress', 'warning');
return WebAuthnUI.WebAuthnUI.getCredential(request).then(function(response) {
return $.ajax({
type: "POST",
url: portal + "2fregisters/webauthn/verification",
data: {
state_id: ch.state_id,
credential: JSON.stringify(response)
},
dataType: 'json',
success: function(resp) {
if (resp.error) {
return setMsg('webAuthnFailed', 'danger');
} else if (resp.result) {
return setMsg('yourKeyIsVerified', 'positive');
}
},
error: displayError
});
})["catch"](function(error) {
return webAuthnError(error);
});
}
});
};
$(document).ready(function() {
$('#u2fPermission').hide();
$('#register').on('click', register);
$('#verify').on('click', verify);
return $('#goback').attr('href', portal);
});
}).call(this);

View File

@ -0,0 +1 @@
!function(){var n=function(e,r){return $("#msg").attr("trspan",e),$("#msg").html(window.translate(e)),$("#color").removeClass("message-positive message-warning message-danger alert-success alert-warning alert-danger"),$("#color").addClass("message-"+r),"positive"===r&&(r="success"),$("#color").addClass("alert-"+r)},t=function(e,r,t){if(console.log("Error",t),(e=JSON.parse(e.responseText))&&e.error)return e=e.error.replace(/.* /,""),console.log("Returned error",e),n(e,"danger")},a=function(e){return"unsupported"!==e.name?n("webAuthnBrowserFailed","danger"):n("webAuthnUnsupported","warning")},e=function(){return $.ajax({type:"POST",url:portal+"2fregisters/webauthn/registrationchallenge",data:{},dataType:"json",error:t,success:function(r){var e=r.request;return n("webAuthnRegisterInProgress","warning"),$("#u2fPermission").show(),WebAuthnUI.WebAuthnUI.createCredential(e).then(function(e){return $.ajax({type:"POST",url:portal+"2fregisters/webauthn/registration",data:{state_id:r.state_id,credential:JSON.stringify(e),keyName:$("#keyName").val()},dataType:"json",success:function(e){return e.error?e.error.match(/badName/)?n(e.error,"danger"):n("webAuthnRegisterFailed","danger"):e.result?n("yourKeyIsRegistered","positive"):void 0},error:t})}).catch(a)}})},r=function(){return $.ajax({type:"POST",url:portal+"2fregisters/webauthn/verificationchallenge",data:{},dataType:"json",error:t,success:function(r){var e=r.request;return n("webAuthnBrowserInProgress","warning"),WebAuthnUI.WebAuthnUI.getCredential(e).then(function(e){return $.ajax({type:"POST",url:portal+"2fregisters/webauthn/verification",data:{state_id:r.state_id,credential:JSON.stringify(e)},dataType:"json",success:function(e){return e.error?n("webAuthnFailed","danger"):e.result?n("yourKeyIsVerified","positive"):void 0},error:t})}).catch(a)}})};$(document).ready(function(){return $("#u2fPermission").hide(),$("#register").on("click",e),$("#verify").on("click",r),$("#goback").attr("href",portal)})}.call(this);

View File

@ -0,0 +1 @@
{"version":3,"sources":["webauthnregistration.js"],"names":["setMsg","msg","level","$","attr","html","window","translate","removeClass","addClass","displayError","j","status","err","console","log","res","JSON","parse","responseText","error","replace","webAuthnError","name","register","ajax","type","url","portal","data","dataType","success","ch","request","show","WebAuthnUI","createCredential","then","response","state_id","credential","stringify","keyName","val","resp","match","result","verify","getCredential","document","ready","hide","on","call","this"],"mappings":"CAMA,WACE,IAEAA,EAAS,SAASC,EAAKC,GAQrB,OAPAC,EAAE,QAAQC,KAAK,SAAUH,GACzBE,EAAE,QAAQE,KAAKC,OAAOC,UAAUN,IAChCE,EAAE,UAAUK,YAAY,4FACxBL,EAAE,UAAUM,SAAS,WAAaP,GACpB,aAAVA,IACFA,EAAQ,WAEHC,EAAE,UAAUM,SAAS,SAAWP,IAGzCQ,EAAe,SAASC,EAAGC,EAAQC,GAIjC,GAFAC,QAAQC,IAAI,QAASF,IACrBG,EAAMC,KAAKC,MAAMP,EAAEQ,gBACRH,EAAII,MAGb,OAFAJ,EAAMA,EAAII,MAAMC,QAAQ,MAAO,IAC/BP,QAAQC,IAAI,iBAAkBC,GACvBhB,EAAOgB,EAAK,WAIvBM,EAAgB,SAASF,GACvB,MACO,gBADCA,EAAMG,KAIHvB,EAAO,wBAAyB,UAFhCA,EAAO,sBAAuB,YAM3CwB,EAAW,WACT,OAAOrB,EAAEsB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,6CACdC,KAAM,GACNC,SAAU,OACVV,MAAOV,EACPqB,QAAS,SAASC,GAChB,IACAC,EAAUD,EAAGC,QAGb,OAFAjC,EAAO,6BAA8B,WACrCG,EAAE,kBAAkB+B,OACbC,WAAWA,WAAWC,iBAAiBH,GAASI,KAAK,SAASC,GACnE,OAAOnC,EAAEsB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,oCACdC,KAAM,CACJU,SAAUP,EAAGO,SACbC,WAAYvB,KAAKwB,UAAUH,GAC3BI,QAASvC,EAAE,YAAYwC,OAEzBb,SAAU,OACVC,QAAS,SAASa,GAChB,OAAIA,EAAKxB,MACHwB,EAAKxB,MAAMyB,MAAM,WACZ7C,EAAO4C,EAAKxB,MAAO,UAEnBpB,EAAO,yBAA0B,UAEjC4C,EAAKE,OACP9C,EAAO,sBAAuB,iBADhC,GAIToB,MAAOV,MAED,MACDY,OAMfyB,EAAS,WACP,OAAO5C,EAAEsB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,6CACdC,KAAM,GACNC,SAAU,OACVV,MAAOV,EACPqB,QAAS,SAASC,GAChB,IACAC,EAAUD,EAAGC,QAEb,OADAjC,EAAO,4BAA6B,WAC7BmC,WAAWA,WAAWa,cAAcf,GAASI,KAAK,SAASC,GAChE,OAAOnC,EAAEsB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,oCACdC,KAAM,CACJU,SAAUP,EAAGO,SACbC,WAAYvB,KAAKwB,UAAUH,IAE7BR,SAAU,OACVC,QAAS,SAASa,GAChB,OAAIA,EAAKxB,MACApB,EAAO,iBAAkB,UACvB4C,EAAKE,OACP9C,EAAO,oBAAqB,iBAD9B,GAIToB,MAAOV,MAED,MACDY,OAMfnB,EAAE8C,UAAUC,MAAM,WAIhB,OAHA/C,EAAE,kBAAkBgD,OACpBhD,EAAE,aAAaiD,GAAG,QAAS5B,GAC3BrB,EAAE,WAAWiD,GAAG,QAASL,GAClB5C,EAAE,WAAWC,KAAK,OAAQwB,WAGlCyB,KAAKC"}

View File

@ -270,6 +270,7 @@
"resentConfirm":"هل تريد إعادة إرسال رسالة التأكيد؟",
"resetPwd":"إعادة تعيين كلمة المرور الخاصة بي",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":" إعادة تحميل الحقوق تحتاج إلى تسجيل الخروج وتسجيل الدخول مرة أخرى",
"rules":"RULES",
"scope":"نطاق",
@ -320,6 +321,16 @@
"wait":"انتظر",
"waitingmessage":"Authentication in progress, please wait",
"warning":"تحذير",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"مرحبا بك على بوابة إثبات الهوية الآمنة.",
"yesResendMail":"نعم، أعد إرسال البريد",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"Your TOTP key",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Möchtest du, dass die Bestätigungsmail erneut gesendet wird ?",
"resetPwd":"Mein Passwort zurücksetzen",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"Zum Neuladen der Rechte musst du dich ab- und wieder anmelden",
"rules":"RULES",
"scope":"Scope",
@ -320,6 +321,16 @@
"wait":"Warten",
"waitingmessage":"Authentication in progress, please wait",
"warning":"Warnung",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Willkommen in Ihrem gesicherten Authentifizierungsportal.",
"yesResendMail":"Ja, Mail erneut senden.",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"Your TOTP key",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Do you want the confirmation mail to be resent?",
"resetPwd":"Reset my password",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"Rights reloads need to logout and login again",
"rules":"RULES",
"scope":"Scope",
@ -320,6 +321,16 @@
"wait":"Wait",
"waitingmessage":"Authentication in progress, please wait",
"warning":"Warning",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Welcome on your secured authentication portal.",
"yesResendMail":"Yes, resend the mail",
"yourAddress":"Know your address",

View File

@ -270,6 +270,7 @@
"resentConfirm":"¿Desea que el e-mail de confirmación sea reenviado?",
"resetPwd":"Reiniciar mi contraseña",
"rest2f":"Código de verificación",
"retry":"Retry",
"rightsReloadNeedsLogout":"La recarga de derechos necesita desconectarse y conectarse de nuevo",
"rules":"RULES",
"scope":"Alcance",
@ -320,6 +321,16 @@
"wait":"Esperar",
"waitingmessage":"Autenticación en progreso, espere por favor",
"warning":"Precaución",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Bienvenido a su portal de autenticación.",
"yesResendMail":"Sí, reenviar el e-mail",
"yourAddress":"Conozca su dirección",
@ -337,4 +348,4 @@
"yourProfile":"Conozca su perfil",
"yourTotpKey":"Su llave TOTP",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Do you want the confirmation mail to be resent?",
"resetPwd":"Palauta salasanani?",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"Rights reloads need to logout and login again",
"rules":"RULES",
"scope":"Scope",
@ -320,6 +321,16 @@
"wait":"Odota",
"waitingmessage":"Authentication in progress, please wait",
"warning":"Varoitus",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Welcome on your secured authentication portal.",
"yesResendMail":"Kyllä, uudelleen lähetä sähköposti",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"Your TOTP key",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Voulez-vous que le message de confirmation soit renvoyé ?",
"resetPwd":"Réinitialiser mon mot de passe",
"rest2f":"Code de vérification",
"retry":"Réessayer",
"rightsReloadNeedsLogout":"Le rechargement des droits nécessite une déconnexion",
"rules":"REGLES",
"scope":"Informations",
@ -320,6 +321,16 @@
"wait":"Attendre",
"waitingmessage":"Authentification en cours, merci de patienter",
"warning":"Attention",
"webAuthnBrowserFailed":"Le navigateur n'a pas pu obtenir d'assertion WebAuthn",
"webAuthnBrowserInProgress":"Authentification WebAuthn en cours, veuillez suivre les instructions de votre navigateur",
"webAuthnFailed":"L'authentification WebAuthn a échoué",
"webAuthnRegisterFailed":"L'enregistrement WebAuthn a échoué",
"webAuthnRegisterInProgress":"Enregistrement WebAuthn en cours, veuillez suivre les instructions de votre navigateur",
"webAuthnRequired":"Authentification WebAuthn nécessaire",
"webAuthnUnsupported":"Votre navigateur ne supporte pas WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Enregistrement d'un périphérique de sécurité",
"webauthnAlreadyRegistered":"Ce périphérique est déjà enregistré",
"welcomeOnPortal":"Bienvenue sur votre portail d'authentification sécurisée.",
"yesResendMail":"Oui, renvoyer le mail",
"yourAddress":"Connaître votre adresse",

View File

@ -270,6 +270,7 @@
"resentConfirm":"האם לשלוח שוב את הודעת האימות בדוא״ל?",
"resetPwd":"איפוס הסיסמה שלי",
"rest2f":"קוד אימות",
"retry":"Retry",
"rightsReloadNeedsLogout":"רענוני זכויות דורשים יציאה וכניסה מחדש",
"rules":"כללים",
"scope":"היקף",
@ -320,6 +321,16 @@
"wait":"המתנה",
"waitingmessage":"מתבצע אימות, נא להמתין",
"warning":"אזהרה",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"ברוך בואך לשער האימות המאובטח שלך.",
"yesResendMail":"כן, לשלוח את ההודעה שוב",
"yourAddress":"היכרות עם הכתובת שלך",
@ -337,4 +348,4 @@
"yourProfile":"היכרות עם הפרופיל שלך",
"yourTotpKey":"מפתח ה־TOTP שלך",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Vuoi inviare di nuovo la mail di conferma?",
"resetPwd":"Reimpostare la password",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"Le ricariche dei diritti necessitano di disconnettersi e di riconnettersi",
"rules":"RULES",
"scope":"Ambito",
@ -320,6 +321,16 @@
"wait":"Attendere",
"waitingmessage":"Autenticazione in corso, attendere prego",
"warning":"Avvertimento",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Benvenuto sul tuo portale di autenticazione protetta.",
"yesResendMail":"Sì, rinvia e-mail",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"La tua chiave TOTP",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Do you want the confirmation mail to be resent?",
"resetPwd":"Reset my password",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"Rights reloads need to logout and login again",
"rules":"RULES",
"scope":"Scope",
@ -320,6 +321,16 @@
"wait":"Wait",
"waitingmessage":"Authentication in progress, please wait",
"warning":"Warning",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Welcome on your secured authentication portal.",
"yesResendMail":"Yes, resend the mail",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"Your TOTP key",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Czy chcesz, aby wiadomość z potwierdzeniem została ponownie wysłana?",
"resetPwd":"Zresetuj moje hasło",
"rest2f":"Kod weryfikacyjny",
"retry":"Retry",
"rightsReloadNeedsLogout":"Przeładowania uprawnień wymaga wylogowania i ponownego zalogowania",
"rules":"RULES",
"scope":"Zakres",
@ -320,6 +321,16 @@
"wait":"Czekaj",
"waitingmessage":"Uwierzytelnianie w toku, proszę czekać",
"warning":"Ostrzeżenie",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Witamy w portalu z bezpiecznym uwierzytelnianiem.",
"yesResendMail":"Tak, wyślij ponownie pocztę",
"yourAddress":"Twój adres",
@ -337,4 +348,4 @@
"yourProfile":"Twój profil",
"yourTotpKey":"Twój klucz TOTP",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Do you want the confirmation mail to be resent?",
"resetPwd":"Reset my password",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"Rights reloads need to logout and login again",
"rules":"RULES",
"scope":"Scope",
@ -320,6 +321,16 @@
"wait":"Wait",
"waitingmessage":"Authentication in progress, please wait",
"warning":"Warning",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Welcome on your secured authentication portal.",
"yesResendMail":"Yes, resend the mail",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"Your TOTP key",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Você deseja que o e-mail de confirmação seja reenviado?",
"resetPwd":"Redefinir minha senha",
"rest2f":"Código de verificação",
"retry":"Retry",
"rightsReloadNeedsLogout":"Recarregamentos de direitos precisam que saia e faça login novamente",
"rules":"REGRAS",
"scope":"Escopo",
@ -320,6 +321,16 @@
"wait":"Espere",
"waitingmessage":"Autenticação em progresso. Por favor, aguarde",
"warning":"Aviso",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Bem-vindo ao seu portal de autenticação seguro.",
"yesResendMail":"Sim, reenvie o e-mail",
"yourAddress":"Saiba o seu endereço",
@ -337,4 +348,4 @@
"yourProfile":"Saiba o seu perfil",
"yourTotpKey":"Sua chave TOTP",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Do you want the confirmation mail to be resent?",
"resetPwd":"Reset my password",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"Rights reloads need to logout and login again",
"rules":"RULES",
"scope":"Scope",
@ -320,6 +321,16 @@
"wait":"Wait",
"waitingmessage":"Authentication in progress, please wait",
"warning":"Warning",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Welcome on your secured authentication portal.",
"yesResendMail":"Yes, resend the mail",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"Your TOTP key",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Onay e-postasının tekrar gönderilmesini ister misiniz?",
"resetPwd":"Parolamı sıfırla",
"rest2f":"Doğrulama kodu",
"retry":"Retry",
"rightsReloadNeedsLogout":"Yetkiler yeniden yüklendiğinde çıkış yapıp tekrar giriş yapmanız gerekir",
"rules":"KURALLAR",
"scope":"Kapsam",
@ -320,6 +321,16 @@
"wait":"Bekleyin",
"waitingmessage":"Kimlik doğrulama işlemi devam ediyor, lütfen bekleyin",
"warning":"Uyarı",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Güvenli kimlik doğrulama portalına hoş geldiniz.",
"yesResendMail":"Evet, tekrar e-posta gönder",
"yourAddress":"Adresini bil",
@ -337,4 +348,4 @@
"yourProfile":"Profilini bil",
"yourTotpKey":"TOTP anahtarınız",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"Bạn có muốn gửi lại thư xác nhận không?",
"resetPwd":"Đặt lại mật khẩu của tôi",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"Tải lại quyền cần đăng xuất và đăng nhập lại",
"rules":"RULES",
"scope":"Phạm vi",
@ -320,6 +321,16 @@
"wait":"Hãy đợi",
"waitingmessage":"Authentication in progress, please wait",
"warning":"Cảnh báo",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"Chào mừng bạn đến với cổng thông tin xác thực được bảo mật của bạn.",
"yesResendMail":"Có, gửi lại thư",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"Your TOTP key",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"您想确认邮件被重新发送吗?",
"resetPwd":"重置我的密码",
"rest2f":"Verification code",
"retry":"Retry",
"rightsReloadNeedsLogout":"重新加载权限需要登出并且再次登录",
"rules":"RULES",
"scope":"Scope",
@ -320,6 +321,16 @@
"wait":"等待",
"waitingmessage":"Authentication in progress, please wait",
"warning":"警告",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"欢迎来到您的加密认证 portal",
"yesResendMail":"好的,重新发送邮件",
"yourAddress":"Know your address",
@ -337,4 +348,4 @@
"yourProfile":"Know your profile",
"yourTotpKey":"Your TOTP key",
"yubikey2f":"Yubikey"
}
}

View File

@ -270,6 +270,7 @@
"resentConfirm":"您是否要重新傳送確認郵件?",
"resetPwd":"重設我的密碼",
"rest2f":"驗證代碼",
"retry":"Retry",
"rightsReloadNeedsLogout":"重新載入權限需要登出然後再次登入",
"rules":"RULES",
"scope":"範圍",
@ -320,6 +321,16 @@
"wait":"等待",
"waitingmessage":"正在進行驗證,請稍候",
"warning":"警告",
"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
"webAuthnFailed":"WebAuthn authentication failed",
"webAuthnRegisterFailed":"WebAuthn registration failed",
"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
"webAuthnRequired":"WebAuthn authentication required",
"webAuthnUnsupported":"Your web browser does not support WebAuthn",
"webauthn2f":"WebAuthn",
"webauthn2fWelcome":"Security device registration",
"webauthnAlreadyRegistered":"This device is already registered",
"welcomeOnPortal":"歡迎使用您的安全驗證首頁。",
"yesResendMail":"是的,重新傳送電子郵件",
"yourAddress":"知道您的地址",
@ -337,4 +348,4 @@
"yourProfile":"知道您的個人檔案",
"yourTotpKey":"您的 TOTP 金鑰",
"yubikey2f":"Yubikey"
}
}

View File

@ -0,0 +1,43 @@
<TMPL_INCLUDE NAME="header.tpl">
<div class="container">
<TMPL_IF NAME="AUTH_ERROR">
<div class="message message-<TMPL_VAR NAME="AUTH_ERROR_TYPE"> alert"><span trmsg="<TMPL_VAR NAME="AUTH_ERROR">"></span></div>
</TMPL_IF>
<TMPL_IF NAME="DATA">
<div id="color" class="message message-positive alert"><span id="msg" trspan="webAuthnRequired"></span></div>
<form id="verify-form" action="/webauthn2fcheck" method="post">
<input type="hidden" id="credential" name="credential" value="" />
<input type="hidden" id="token" name="token" value="<TMPL_VAR NAME="TOKEN">" />
<input type="hidden" id="checkLogins" name="checkLogins" value="<TMPL_VAR NAME="CHECKLOGINS">" />
<input type="hidden" id="stayconnected" name="stayconnected" value="<TMPL_VAR NAME="STAYCONNECTED">" />
<input type="hidden" name="skin" value="<TMPL_VAR NAME="SKIN">" />
</form>
<script type="application/init">
<TMPL_VAR NAME="DATA">
</script>
<!-- //if:jsminified
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/webauthn-ui.min.js"></script>
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/webauthncheck.min.js"></script>
//else -->
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/webauthn-ui.js"></script>
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/webauthncheck.js"></script>
<!-- //endif -->
</TMPL_IF>
</div>
<div class="buttons">
<div class="btn btn-primary" role="button" id="retrybutton">
<span class="fa fa-repeat"></span>
<span trspan="retry">Retry</span>
</div>
<a href="<TMPL_VAR NAME="PORTAL_URL">?cancel=1&skin=<TMPL_VAR NAME="SKIN">" class="btn btn-secondary" role="button">
<span class="fa fa-home"></span>
<span trspan="cancel">Cancel</span>
</a>
</div>
<TMPL_INCLUDE NAME="footer.tpl">

View File

@ -0,0 +1,58 @@
<TMPL_INCLUDE NAME="header.tpl">
<main id="menucontent" class="container">
<div id="color" class="message message-<TMPL_VAR NAME="ALERT"> alert"><span id="msg" trspan="<TMPL_VAR NAME="MSG">"></span></div>
<div class="card">
<div class="card-body">
<div id="u2fPermission" trspan="u2fPermission" class="alert alert-info">You may be prompted to allow the site permission to access your security keys. After granting permission, the device will start to blink.</div>
<div class="row">
<div class="col-md-6 text-center">
<img src="<TMPL_VAR NAME="STATIC_PREFIX"><TMPL_VAR NAME="SKIN">/webauthn.png" alt="WebAuthn" title="WebAuthn" />
</div>
<div class="col-md-6">
<div class="form-group">
<label for="keyName"><span trspan="name">Name</span></label>
<input type="text" class="form-control" id="keyName" name="keyName" value="MyFidoKey" trplaceholder="name" />
</div>
</div>
</div>
<div class="buttons">
<span id="verify" class="btn btn-info" role="button">
<span class="fa fa-check-circle"></span>
<span trspan="verify">Verify</span>
</span>
<span id="register" class="btn btn-success" role="button">
<span class="fa fa-floppy-o"></span>
<span trspan="register">Register</span>
</span>
</div>
</div>
</div>
</main>
<div class="buttons">
<a href="<TMPL_VAR NAME="PORTAL_URL">2fregisters?skin=<TMPL_VAR NAME="SKIN">" class="btn btn-info" role="button">
<span class="fa fa-shield"></span>
<span trspan="sfaManager">sfaManager</span>
</a>
<a id="goback" href="<TMPL_VAR NAME="PORTAL_URL">?cancel=1&skin=<TMPL_VAR NAME="SKIN"><TMPL_IF NAME="AUTH_URL">&url=<TMPL_VAR NAME="AUTH_URL"></TMPL_IF>" class="btn btn-primary" role="button">
<span class="fa fa-home"></span>
<span trspan="goToPortal">Go to portal</span>
</a>
</div>
<!-- //if:jsminified
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/webauthn-ui.min.js"></script>
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/webauthnregistration.min.js"></script>
//else -->
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/webauthn-ui.js"></script>
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/webauthnregistration.js"></script>
<!-- //endif -->
<TMPL_INCLUDE NAME="footer.tpl">

View File

@ -0,0 +1,321 @@
use Test::More;
use strict;
use IO::String;
use MIME::Base64 qw/encode_base64url decode_base64url/;
use JSON;
require 't/test-lib.pm';
SKIP: {
eval "use Authen::WebAuthn::Test; use Authen::WebAuthn;";
if ($@) {
skip 'Authen::WebAuthn not found';
}
my $ecdsa_key = <<ENDKEY;
-----BEGIN EC PRIVATE KEY-----
MIIBUQIBAQQgWEGujn2kkOVckTIKhIJDSqH99bxydPGloXvbeaq9swiggeMwgeAC
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
MEQEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwRBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLesz
oPShOUXYmMKWT+NC4v4af5uO5+tKfA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////
AAAAAP//////////vOb6racXnoTzucrC/GMlUQIBAaFEA0IABM/oQXEUzjPwEhM4
gWmIbCuOXc4Ja8jPDKxbQaZckal7/9a693/nkf7flk1S9AV2tjrtJPF6kg8TCGbF
KoeD9Wc=
-----END EC PRIVATE KEY-----
ENDKEY
my $credential_id_1 = "lZYltP9MtoRNuXK8f8tWf";
my $credential_id_2 = "d2ViYXV0aG5fdGVzdGVyXzI";
my $webauthn_tester_1 = Authen::WebAuthn::Test->new(
origin => "http://auth.example.com",
rp_id => "auth.example.com",
credential_id => $credential_id_1,
aaguid => "00000000-0000-0000-0000-000000000000",
key => $ecdsa_key,
sign_count => 5,
);
my $webauthn_tester_2 = Authen::WebAuthn::Test->new(
origin => "http://auth.example.com",
rp_id => "auth.example.com",
credential_id => $credential_id_2,
aaguid => "00000000-0000-0000-0000-000000000000",
key => $ecdsa_key,
sign_count => 18,
);
#FIXME
my $webauthn_tester = $webauthn_tester_1;
my $res;
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
webauthn2fSelfRegistration => 1,
webauthn2fActivation => 1,
webauthn2fUserCanRemoveKey => 1,
webauthnDisplayNameAttr => 'cn',
}
}
);
my $portal = $client->p;
sub login_and_check_display {
my ($client) = @_;
my $res;
ok(
$res = $client->_post(
'/', IO::String->new('user=dwho&password=dwho'),
length => 23,
),
'Create Session'
);
expectOK($res);
my $id = expectCookie($res);
# Display 2FA Manager
ok(
$res = $client->_get(
'/2fregisters',
cookie => "lemonldap=$id",
accept => "test/html",
),
'Show 2FA Manager'
);
expectRedirection( $res,
'http://auth.example.com//2fregisters/webauthn' );
# Display WebAuthn registration
ok(
$res = $client->_get(
'/2fregisters/webauthn',
cookie => "lemonldap=$id",
accept => "test/html",
),
'Show WebAuthn registration'
);
like(
$res->[2]->[0],
qr%<img src="/static/bootstrap/webauthn.png"%,
"WebAuthn logo found"
);
like(
$res->[2]->[0],
qr%<div id="u2fPermission" trspan="u2fPermission"%,
"Security help message found"
);
return $id;
}
sub register_new_device {
my ( $client, $id, $webauthn_tester, $device_name, $expected_error ) =
@_;
my $res;
ok(
$res = $client->_post(
'/2fregisters/webauthn/registrationchallenge',
IO::String->new('{}'),
cookie => "lemonldap=$id",
length => 2,
),
'Registration challenge'
);
my $reg_challenge = from_json $res->[2]->[0];
is( $reg_challenge->{request}->{rp}->{name},
"LemonLDAP::NG", "rp.name is set" );
is( $reg_challenge->{request}->{user}->{name},
"dwho", "user.name is set" );
is( length( $reg_challenge->{request}->{user}->{id} ),
86, "user.id is set" );
is( $reg_challenge->{request}->{user}->{displayName},
"Doctor Who", "user.displayName is set" );
my $state_id = $reg_challenge->{state_id};
my $challenge = $reg_challenge->{request}->{challenge};
ok( $state_id, "State ID is set" );
ok( $challenge, "Challenge is set" );
my $credential_response =
$webauthn_tester->get_credential_response($reg_challenge);
my $registration_response = buildForm( {
credential =>
$webauthn_tester->encode_credential($credential_response),
state_id => $state_id,
keyName => $device_name,
}
);
# Post registration
ok(
$res = $client->_post(
'/2fregisters/webauthn/registration',
IO::String->new($registration_response),
cookie => "lemonldap=$id",
length => length($registration_response),
),
'Registration challenge'
);
my $reg_response = from_json $res->[2]->[0];
if ($expected_error) {
is( $reg_response->{result} // 0, 0, "Failed registration" );
is( $reg_response->{error}, $expected_error,
"Failed registration" );
is( $res->[0], 400, "Expected failure http code" );
}
else {
is( $reg_response->{result}, 1, "Successful registration" );
}
# return userHandle
return $reg_challenge->{request}->{user}->{id};
}
sub verify_device {
my ( $client, $id, $webauthn_tester, $expected_credentials ) = @_;
my $res;
# Get verification parameters
ok(
$res = $client->_post(
'/2fregisters/webauthn/verificationchallenge',
IO::String->new('{}'),
cookie => "lemonldap=$id",
length => 2,
),
'Registration challenge'
);
my $verif_challenge = from_json $res->[2]->[0];
is_deeply( $verif_challenge->{request}->{allowCredentials},
$expected_credentials );
my $state_id = $verif_challenge->{state_id};
my $challenge = $verif_challenge->{request}->{challenge};
ok( $state_id, "State ID is set" );
ok( $challenge, "Challenge is set" );
# Increment signature to avoid validation error
$webauthn_tester->sign_count( $webauthn_tester->sign_count + 1 );
my $credential_response =
$webauthn_tester->get_assertion_response($verif_challenge);
my $verification_response = buildForm( {
state_id => $state_id,
credential =>
$webauthn_tester->encode_credential($credential_response),
}
);
# Verify registration
ok(
$res = $client->_post(
'/2fregisters/webauthn/verification',
IO::String->new($verification_response),
cookie => "lemonldap=$id",
length => length($verification_response),
),
'Registration challenge'
);
my $verif_response = from_json $res->[2]->[0];
is( $verif_response->{result}, 1, "Successful verification" );
}
sub check_psession {
my ( $portal, $user_handle ) = @_;
# Inspect Psession content
my $psession = $portal->getPersistentSession("dwho");
# userHandle is stored
is( $psession->{data}->{_webAuthnUserHandle},
$user_handle, "User handle saved" );
my $devices = from_json $psession->{data}->{_2fDevices};
is( @{$devices}, 2, "2 devices found" );
my $device1 = $devices->[0];
my $device2 = $devices->[1];
# Epoch will differ
delete $device1->{epoch};
delete $device2->{epoch};
is_deeply(
$device1,
{
'_credentialId' => encode_base64url($credential_id_1),
'_credentialPublicKey' =>
'pQECAyYgASFYIM_oQXEUzjPwEhM4gWmIbCuOXc4Ja8jPDKxbQaZckal7Ilgg_9a693_nkf7flk1S9AV2tjrtJPF6kg8TCGbFKoeD9Wc',
'_signCount' => 5,
'name' => "MyFirstDevice",
'type' => 'WebAuthn'
},
"Registration contains expected data"
);
is_deeply(
$device2,
{
'_credentialId' => encode_base64url($credential_id_2),
'_credentialPublicKey' =>
'pQECAyYgASFYIM_oQXEUzjPwEhM4gWmIbCuOXc4Ja8jPDKxbQaZckal7Ilgg_9a693_nkf7flk1S9AV2tjrtJPF6kg8TCGbFKoeD9Wc',
'_signCount' => 18,
'name' => "MySecondDevice",
'type' => 'WebAuthn'
},
"Registration contains expected data"
);
}
my $id = login_and_check_display($client);
my $user_handle_1 =
register_new_device( $client, $id, $webauthn_tester_1, "MyFirstDevice" );
# Register same device again, fails because credential ID is already taken
register_new_device( $client, $id, $webauthn_tester_1,
"MyAlreadyRegisteredDevice", "webauthnAlreadyRegistered" );
# Register a different device should succeed
my $user_handle_2 =
register_new_device( $client, $id, $webauthn_tester_2, "MySecondDevice" );
# userHandle was kept from first registration
is( $user_handle_2, $user_handle_1,
"userHandle was kept from first registration" );
check_psession( $portal, $user_handle_1 );
verify_device(
$client, $id,
$webauthn_tester_1,
[ {
'id' => encode_base64url($credential_id_1),
'type' => 'public-key'
},
{
'id' => encode_base64url($credential_id_2),
'type' => 'public-key'
}
]
);
}
# TODO delete
clean_sessions();
done_testing();

View File

@ -0,0 +1,134 @@
use Test::More;
use strict;
use IO::String;
use MIME::Base64 qw/encode_base64url decode_base64url/;
use JSON;
require 't/test-lib.pm';
SKIP: {
eval "use Authen::WebAuthn::Test; use Authen::WebAuthn;";
if ($@) {
skip 'Authen::WebAuthn not found';
}
my $ecdsa_key = <<ENDKEY;
-----BEGIN EC PRIVATE KEY-----
MIIBUQIBAQQgWEGujn2kkOVckTIKhIJDSqH99bxydPGloXvbeaq9swiggeMwgeAC
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
MEQEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwRBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLesz
oPShOUXYmMKWT+NC4v4af5uO5+tKfA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////
AAAAAP//////////vOb6racXnoTzucrC/GMlUQIBAaFEA0IABM/oQXEUzjPwEhM4
gWmIbCuOXc4Ja8jPDKxbQaZckal7/9a693/nkf7flk1S9AV2tjrtJPF6kg8TCGbF
KoeD9Wc=
-----END EC PRIVATE KEY-----
ENDKEY
my $webauthn_tester = Authen::WebAuthn::Test->new(
origin => "http://auth.example.com",
rp_id => "auth.example.com",
credential_id => "lZYltP9MtoRNuXK8f8tWf",
aaguid => "00000000-0000-0000-0000-000000000000",
key => $ecdsa_key,
sign_count => 5,
);
my $res;
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
webauthn2fSelfRegistration => 1,
webauthn2fActivation => 1,
webauthn2fUserCanRemoveKey => 1,
}
}
);
my $portal = $client->p;
$portal->getPersistentSession(
"dwho",
{
_2fDevices => to_json [ {
"_credentialId" => "bFpZbHRQOU10b1JOdVhLOGY4dFdm",
"_credentialPublicKey" =>
encode_base64url( $webauthn_tester->encode_cosekey ),
"_signCount" => "1",
"epoch" => "1640015033",
"name" => "MyFidoKey",
"type" => "WebAuthn"
},
],
}
);
# Authenticate with good password
# --------------------------------------
ok(
$res = $client->_post(
'/',
IO::String->new('user=dwho&password=dwho'),
length => 23,
),
'Auth query'
);
expectOK($res);
my ( $host, $url, $query ) =
expectForm( $res, "", '/webauthn2fcheck', 'token', 'credential' );
my ($json) = $res->[2]->[0] =~
m#<script type="application/init">\s*({"request"[^<]*})\s*</script>#ms;
ok( $json, "Found request object in JS data" );
$json = from_json($json);
my $request = $json->{request};
my $challenge = $request->{challenge};
ok( $challenge, "Found challenge" );
is( $request->{extensions}->{appid},
'http://auth.example.com', "Correct U2F AppID" );
is( @{ $request->{allowCredentials} },
1, "Found only one allowed credentials" );
is(
$request->{allowCredentials}->[0]->{id},
"bFpZbHRQOU10b1JOdVhLOGY4dFdm",
"Correct credential ID"
);
is( $request->{allowCredentials}->[0]->{type},
"public-key", "Correct public key" );
my $credential = $webauthn_tester->get_assertion_response( {
request => $request,
}
);
$credential = $webauthn_tester->encode_credential($credential);
#diag $credential;
my $urlencoded_credential = buildForm( {
credential => $credential
}
);
$query =~ s/credential=/$urlencoded_credential/;
ok(
$res = $client->_post(
$url,
IO::String->new($query),
length => length($query),
),
'Auth query'
);
my $id = expectCookie($res);
# Test logout
$client->logout($id);
}
clean_sessions();
done_testing();