Merge branch 'fix-webauthn-1411' into v2.0
This commit is contained in:
commit
af7abe8d19
27
COPYING
27
COPYING
|
@ -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 application’s 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
|
||||
|
|
|
@ -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 application’s 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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -41,4 +41,5 @@ Authentication, users and password databases
|
|||
radius2f
|
||||
rest2f
|
||||
yubikey2f
|
||||
webauthn2f
|
||||
sfextra
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]_ ✔
|
||||
==================================================================== ==============
|
||||
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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) ) {
|
||||
|
|
|
@ -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' );
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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" );
|
||||
|
|
|
@ -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();
|
|
@ -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}
|
||||
|
|
|
@ -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" };
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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":"القائمة البيضاء",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
@ -22,6 +22,9 @@
|
|||
&
|
||||
<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>
|
||||
&
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' );
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 user’s 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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 |
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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"}
|
|
@ -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
|
@ -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);
|
|
@ -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);
|
|
@ -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"}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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"}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
@ -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">
|
|
@ -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();
|
|
@ -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();
|
Loading…
Reference in New Issue