KEYCLOAK-9360 Two factor authentication with W3C Web Authentication - 1st impl phase
* KEYCLOAK-9360 Two factor authentication with W3C Web Authentication - 1st impl phase
This commit is contained in:
parent
3b58692d7c
commit
7c75546eac
59 changed files with 5727 additions and 4 deletions
|
@ -106,6 +106,16 @@ public class RealmRepresentation {
|
||||||
protected Integer otpPolicyLookAheadWindow;
|
protected Integer otpPolicyLookAheadWindow;
|
||||||
protected Integer otpPolicyPeriod;
|
protected Integer otpPolicyPeriod;
|
||||||
protected List<String> otpSupportedApplications;
|
protected List<String> otpSupportedApplications;
|
||||||
|
protected String webAuthnPolicyRpEntityName;
|
||||||
|
protected List<String> webAuthnPolicySignatureAlgorithms;
|
||||||
|
protected String webAuthnPolicyRpId;
|
||||||
|
protected String webAuthnPolicyAttestationConveyancePreference;
|
||||||
|
protected String webAuthnPolicyAuthenticatorAttachment;
|
||||||
|
protected String webAuthnPolicyRequireResidentKey;
|
||||||
|
protected String webAuthnPolicyUserVerificationRequirement;
|
||||||
|
protected Integer webAuthnPolicyCreateTimeout;
|
||||||
|
protected Boolean webAuthnPolicyAvoidSameAuthenticatorRegister;
|
||||||
|
protected List<String> webAuthnPolicyAcceptableAaguids;
|
||||||
|
|
||||||
protected List<UserRepresentation> users;
|
protected List<UserRepresentation> users;
|
||||||
protected List<UserRepresentation> federatedUsers;
|
protected List<UserRepresentation> federatedUsers;
|
||||||
|
@ -916,6 +926,86 @@ public class RealmRepresentation {
|
||||||
this.otpSupportedApplications = otpSupportedApplications;
|
this.otpSupportedApplications = otpSupportedApplications;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getWebAuthnPolicyRpEntityName() {
|
||||||
|
return webAuthnPolicyRpEntityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyRpEntityName(String webAuthnPolicyRpEntityName) {
|
||||||
|
this.webAuthnPolicyRpEntityName = webAuthnPolicyRpEntityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getWebAuthnPolicySignatureAlgorithms() {
|
||||||
|
return webAuthnPolicySignatureAlgorithms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicySignatureAlgorithms(List<String> webAuthnPolicySignatureAlgorithms) {
|
||||||
|
this.webAuthnPolicySignatureAlgorithms = webAuthnPolicySignatureAlgorithms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWebAuthnPolicyRpId() {
|
||||||
|
return webAuthnPolicyRpId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyRpId(String webAuthnPolicyRpId) {
|
||||||
|
this.webAuthnPolicyRpId = webAuthnPolicyRpId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWebAuthnPolicyAttestationConveyancePreference() {
|
||||||
|
return webAuthnPolicyAttestationConveyancePreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyAttestationConveyancePreference(String webAuthnPolicyAttestationConveyancePreference) {
|
||||||
|
this.webAuthnPolicyAttestationConveyancePreference = webAuthnPolicyAttestationConveyancePreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWebAuthnPolicyAuthenticatorAttachment() {
|
||||||
|
return webAuthnPolicyAuthenticatorAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyAuthenticatorAttachment(String webAuthnPolicyAuthenticatorAttachment) {
|
||||||
|
this.webAuthnPolicyAuthenticatorAttachment = webAuthnPolicyAuthenticatorAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWebAuthnPolicyRequireResidentKey() {
|
||||||
|
return webAuthnPolicyRequireResidentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyRequireResidentKey(String webAuthnPolicyRequireResidentKey) {
|
||||||
|
this.webAuthnPolicyRequireResidentKey = webAuthnPolicyRequireResidentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWebAuthnPolicyUserVerificationRequirement() {
|
||||||
|
return webAuthnPolicyUserVerificationRequirement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyUserVerificationRequirement(String webAuthnPolicyUserVerificationRequirement) {
|
||||||
|
this.webAuthnPolicyUserVerificationRequirement = webAuthnPolicyUserVerificationRequirement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getWebAuthnPolicyCreateTimeout() {
|
||||||
|
return webAuthnPolicyCreateTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyCreateTimeout(Integer webAuthnPolicyCreateTimeout) {
|
||||||
|
this.webAuthnPolicyCreateTimeout = webAuthnPolicyCreateTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean isWebAuthnPolicyAvoidSameAuthenticatorRegister() {
|
||||||
|
return webAuthnPolicyAvoidSameAuthenticatorRegister;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyAvoidSameAuthenticatorRegister(Boolean webAuthnPolicyAvoidSameAuthenticatorRegister) {
|
||||||
|
this.webAuthnPolicyAvoidSameAuthenticatorRegister = webAuthnPolicyAvoidSameAuthenticatorRegister;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getWebAuthnPolicyAcceptableAaguids() {
|
||||||
|
return webAuthnPolicyAcceptableAaguids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyAcceptableAaguids(List<String> webAuthnPolicyAcceptableAaguids) {
|
||||||
|
this.webAuthnPolicyAcceptableAaguids = webAuthnPolicyAcceptableAaguids;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBrowserFlow() {
|
public String getBrowserFlow() {
|
||||||
return browserFlow;
|
return browserFlow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -798,6 +798,36 @@
|
||||||
<groupId>com.openshift</groupId>
|
<groupId>com.openshift</groupId>
|
||||||
<artifactId>openshift-restclient-java</artifactId>
|
<artifactId>openshift-restclient-java</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthn4j-core</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>*</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthn4j-util</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>*</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-cbor</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>*</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -540,6 +540,39 @@
|
||||||
</license>
|
</license>
|
||||||
</licenses>
|
</licenses>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthnj4-core</artifactId>
|
||||||
|
<version>0.9.7.RELEASE</version>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>Apache Software License 2.0</name>
|
||||||
|
<url>https://github.com/webauthn4j/webauthn4j/blob/0.9.7.RELEASE/LICENSE.txt</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthnj4-util</artifactId>
|
||||||
|
<version>0.9.7.RELEASE</version>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>Apache Software License 2.0</name>
|
||||||
|
<url>https://github.com/webauthn4j/webauthn4j/blob/0.9.7.RELEASE/LICENSE.txt</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-cbor</artifactId>
|
||||||
|
<version>2.9.8</version>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>Apache Software License 2.0</name>
|
||||||
|
<url>https://github.com/FasterXML/jackson-dataformats-binary/wiki</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<others>
|
<others>
|
||||||
<other>
|
<other>
|
||||||
|
@ -780,5 +813,17 @@
|
||||||
</license>
|
</license>
|
||||||
</licenses>
|
</licenses>
|
||||||
</other>
|
</other>
|
||||||
|
<other>
|
||||||
|
<description>rfc4648.js</description>
|
||||||
|
<locations>
|
||||||
|
<file>services/src/main/resources/theme-resources/resources/base64url.js</file>
|
||||||
|
</locations>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>MIT License</name>
|
||||||
|
<url>https://github.com/swansontec/rfc4648.js/blob/master/package.json</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
</other>
|
||||||
</others>
|
</others>
|
||||||
</licenseSummary>
|
</licenseSummary>
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"name": "rfc4648",
|
||||||
|
"version": "1.2.1",
|
||||||
|
"description": "Encoding and decoding for base64, base32, base16, and friends",
|
||||||
|
"keywords": [
|
||||||
|
"Uint8Array",
|
||||||
|
"base16",
|
||||||
|
"base32",
|
||||||
|
"base32hex",
|
||||||
|
"base64",
|
||||||
|
"base64url",
|
||||||
|
"hex"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@github.com:swansontec/rfc4648.js.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "William Swanson",
|
||||||
|
"files": [
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"lib/*",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"main": "lib/index.cjs.js",
|
||||||
|
"module": "lib/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"lint": "standard '*.js' 'src/**/*.js' 'test/**/*.js'",
|
||||||
|
"test": "npm run lint && rollup -c && nyc mocha"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "npm test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.4.3",
|
||||||
|
"@babel/preset-env": "^7.4.3",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"husky": "^2.3.0",
|
||||||
|
"mocha": "^5.0.5",
|
||||||
|
"nyc": "^11.6.0",
|
||||||
|
"rollup": "^1.9.0",
|
||||||
|
"rollup-plugin-babel": "^4.3.2",
|
||||||
|
"rollup-plugin-filesize": "^6.0.1",
|
||||||
|
"rollup-plugin-uglify": "^6.0.2",
|
||||||
|
"standard": "^11.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -540,6 +540,39 @@
|
||||||
</license>
|
</license>
|
||||||
</licenses>
|
</licenses>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthnj4-core</artifactId>
|
||||||
|
<version>0.9.7.RELEASE</version>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>Apache Software License 2.0</name>
|
||||||
|
<url>https://github.com/webauthn4j/webauthn4j/blob/0.9.7.RELEASE/LICENSE.txt</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthnj4-util</artifactId>
|
||||||
|
<version>0.9.7.RELEASE</version>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>Apache Software License 2.0</name>
|
||||||
|
<url>https://github.com/webauthn4j/webauthn4j/blob/0.9.7.RELEASE/LICENSE.txt</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-cbor</artifactId>
|
||||||
|
<version>2.9.8</version>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>Apache Software License 2.0</name>
|
||||||
|
<url>https://github.com/FasterXML/jackson-dataformats-binary/wiki</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<others>
|
<others>
|
||||||
<other>
|
<other>
|
||||||
|
@ -780,5 +813,17 @@
|
||||||
</license>
|
</license>
|
||||||
</licenses>
|
</licenses>
|
||||||
</other>
|
</other>
|
||||||
|
<other>
|
||||||
|
<description>rfc4648.js</description>
|
||||||
|
<locations>
|
||||||
|
<file>services/src/main/resources/theme-resources/resources/base64url.js</file>
|
||||||
|
</locations>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>MIT License</name>
|
||||||
|
<url>https://github.com/swansontec/rfc4648.js/blob/master/package.json</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
</other>
|
||||||
</others>
|
</others>
|
||||||
</licenseSummary>
|
</licenseSummary>
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"name": "rfc4648",
|
||||||
|
"version": "1.2.1",
|
||||||
|
"description": "Encoding and decoding for base64, base32, base16, and friends",
|
||||||
|
"keywords": [
|
||||||
|
"Uint8Array",
|
||||||
|
"base16",
|
||||||
|
"base32",
|
||||||
|
"base32hex",
|
||||||
|
"base64",
|
||||||
|
"base64url",
|
||||||
|
"hex"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@github.com:swansontec/rfc4648.js.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "William Swanson",
|
||||||
|
"files": [
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"lib/*",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"main": "lib/index.cjs.js",
|
||||||
|
"module": "lib/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"lint": "standard '*.js' 'src/**/*.js' 'test/**/*.js'",
|
||||||
|
"test": "npm run lint && rollup -c && nyc mocha"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "npm test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.4.3",
|
||||||
|
"@babel/preset-env": "^7.4.3",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"husky": "^2.3.0",
|
||||||
|
"mocha": "^5.0.5",
|
||||||
|
"nyc": "^11.6.0",
|
||||||
|
"rollup": "^1.9.0",
|
||||||
|
"rollup-plugin-babel": "^4.3.2",
|
||||||
|
"rollup-plugin-filesize": "^6.0.1",
|
||||||
|
"rollup-plugin-uglify": "^6.0.2",
|
||||||
|
"standard": "^11.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<module xmlns="urn:jboss:module:1.3" name="com.fasterxml.jackson.dataformat.jackson-dataformat-cbor">
|
||||||
|
<resources>
|
||||||
|
<artifact name="${com.fasterxml.jackson.dataformat:jackson-dataformat-cbor}"/>
|
||||||
|
</resources>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
||||||
|
</dependencies>
|
||||||
|
</module>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<module xmlns="urn:jboss:module:1.3" name="com.webauthn4j.webauthn4j-core">
|
||||||
|
<resources>
|
||||||
|
<artifact name="${com.webauthn4j:webauthn4j-core}"/>
|
||||||
|
</resources>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<module name="org.slf4j"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
||||||
|
<module name="com.fasterxml.jackson.dataformat.jackson-dataformat-cbor"/>
|
||||||
|
<module name="com.webauthn4j.webauthn4j-util"/>
|
||||||
|
</dependencies>
|
||||||
|
</module>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<module xmlns="urn:jboss:module:1.3" name="com.webauthn4j.webauthn4j-util">
|
||||||
|
<resources>
|
||||||
|
<artifact name="${com.webauthn4j:webauthn4j-util}"/>
|
||||||
|
</resources>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
||||||
|
<module name="com.fasterxml.jackson.dataformat.jackson-dataformat-cbor"/>
|
||||||
|
</dependencies>
|
||||||
|
</module>
|
|
@ -72,5 +72,8 @@
|
||||||
<module name="org.twitter4j"/>
|
<module name="org.twitter4j"/>
|
||||||
<module name="javax.transaction.api"/>
|
<module name="javax.transaction.api"/>
|
||||||
<module name="sun.jdk"/>
|
<module name="sun.jdk"/>
|
||||||
|
<module name="com.webauthn4j.webauthn4j-core"/>
|
||||||
|
<module name="com.webauthn4j.webauthn4j-util"/>
|
||||||
|
<module name="javax.persistence.api"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -621,6 +621,18 @@ public class RealmAdapter implements CachedRealmModel {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnPolicy getWebAuthnPolicy() {
|
||||||
|
if (isUpdated()) return updated.getWebAuthnPolicy();
|
||||||
|
return cached.getWebAuthnPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWebAuthnPolicy(WebAuthnPolicy policy) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.setWebAuthnPolicy(policy);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RoleModel getRoleById(String id) {
|
public RoleModel getRoleById(String id) {
|
||||||
if (isUpdated()) return updated.getRoleById(id);
|
if (isUpdated()) return updated.getRoleById(id);
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredActionProviderModel;
|
import org.keycloak.models.RequiredActionProviderModel;
|
||||||
import org.keycloak.models.RequiredCredentialModel;
|
import org.keycloak.models.RequiredCredentialModel;
|
||||||
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -95,6 +96,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
protected int notBefore;
|
protected int notBefore;
|
||||||
protected PasswordPolicy passwordPolicy;
|
protected PasswordPolicy passwordPolicy;
|
||||||
protected OTPPolicy otpPolicy;
|
protected OTPPolicy otpPolicy;
|
||||||
|
protected WebAuthnPolicy webAuthnPolicy;
|
||||||
|
|
||||||
protected String loginTheme;
|
protected String loginTheme;
|
||||||
protected String accountTheme;
|
protected String accountTheme;
|
||||||
|
@ -203,6 +205,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
notBefore = model.getNotBefore();
|
notBefore = model.getNotBefore();
|
||||||
passwordPolicy = model.getPasswordPolicy();
|
passwordPolicy = model.getPasswordPolicy();
|
||||||
otpPolicy = model.getOTPPolicy();
|
otpPolicy = model.getOTPPolicy();
|
||||||
|
webAuthnPolicy = model.getWebAuthnPolicy();
|
||||||
|
|
||||||
loginTheme = model.getLoginTheme();
|
loginTheme = model.getLoginTheme();
|
||||||
accountTheme = model.getAccountTheme();
|
accountTheme = model.getAccountTheme();
|
||||||
|
@ -598,6 +601,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
return otpPolicy;
|
return otpPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WebAuthnPolicy getWebAuthnPolicy() {
|
||||||
|
return webAuthnPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
public AuthenticationFlowModel getBrowserFlow() {
|
public AuthenticationFlowModel getBrowserFlow() {
|
||||||
return browserFlow;
|
return browserFlow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -919,6 +919,102 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||||
em.flush();
|
em.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnPolicy getWebAuthnPolicy() {
|
||||||
|
WebAuthnPolicy policy = new WebAuthnPolicy();
|
||||||
|
|
||||||
|
// mandatory parameters
|
||||||
|
String rpEntityName = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME);
|
||||||
|
if (rpEntityName == null || rpEntityName.isEmpty())
|
||||||
|
rpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||||
|
policy.setRpEntityName(rpEntityName);
|
||||||
|
|
||||||
|
String signatureAlgorithmsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS);
|
||||||
|
if (signatureAlgorithmsString == null || signatureAlgorithmsString.isEmpty())
|
||||||
|
signatureAlgorithmsString = Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS;
|
||||||
|
List<String> signatureAlgorithms = Arrays.asList(signatureAlgorithmsString.split(","));
|
||||||
|
policy.setSignatureAlgorithm(signatureAlgorithms);
|
||||||
|
|
||||||
|
// optional parameters
|
||||||
|
String rpId = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID);
|
||||||
|
if (rpId == null || rpId.isEmpty()) rpId = "";
|
||||||
|
policy.setRpId(rpId);
|
||||||
|
|
||||||
|
String attestationConveyancePreference = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE);
|
||||||
|
if (attestationConveyancePreference == null || attestationConveyancePreference.isEmpty())
|
||||||
|
attestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
policy.setAttestationConveyancePreference(attestationConveyancePreference);
|
||||||
|
|
||||||
|
String authenticatorAttachment = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT);
|
||||||
|
if (authenticatorAttachment == null || authenticatorAttachment.isEmpty())
|
||||||
|
authenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
policy.setAuthenticatorAttachment(authenticatorAttachment);
|
||||||
|
|
||||||
|
String requireResidentKey = getAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY);
|
||||||
|
if (requireResidentKey == null || requireResidentKey.isEmpty())
|
||||||
|
requireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
policy.setRequireResidentKey(requireResidentKey);
|
||||||
|
|
||||||
|
String userVerificationRequirement = getAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT);
|
||||||
|
if (userVerificationRequirement == null || userVerificationRequirement.isEmpty())
|
||||||
|
userVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
policy.setUserVerificationRequirement(userVerificationRequirement);
|
||||||
|
|
||||||
|
String createTime = getAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT);
|
||||||
|
if (createTime != null) policy.setCreateTimeout(Integer.parseInt(createTime));
|
||||||
|
else policy.setCreateTimeout(0);
|
||||||
|
|
||||||
|
String avoidSameAuthenticatorRegister = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER);
|
||||||
|
if (avoidSameAuthenticatorRegister != null) policy.setAvoidSameAuthenticatorRegister(Boolean.parseBoolean(avoidSameAuthenticatorRegister));
|
||||||
|
|
||||||
|
String acceptableAaguidsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS);
|
||||||
|
List<String> acceptableAaguids = new ArrayList<>();
|
||||||
|
if (acceptableAaguidsString != null && !acceptableAaguidsString.isEmpty())
|
||||||
|
acceptableAaguids = Arrays.asList(acceptableAaguidsString.split(","));
|
||||||
|
policy.setAcceptableAaguids(acceptableAaguids);
|
||||||
|
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWebAuthnPolicy(WebAuthnPolicy policy) {
|
||||||
|
// mandatory parameters
|
||||||
|
String rpEntityName = policy.getRpEntityName();
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME, rpEntityName);
|
||||||
|
|
||||||
|
List<String> signatureAlgorithms = policy.getSignatureAlgorithm();
|
||||||
|
String signatureAlgorithmsString = String.join(",", signatureAlgorithms);
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS, signatureAlgorithmsString);
|
||||||
|
|
||||||
|
// optional parameters
|
||||||
|
String rpId = policy.getRpId();
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID, rpId);
|
||||||
|
|
||||||
|
String attestationConveyancePreference = policy.getAttestationConveyancePreference();
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE, attestationConveyancePreference);
|
||||||
|
|
||||||
|
String authenticatorAttachment = policy.getAuthenticatorAttachment();
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT, authenticatorAttachment);
|
||||||
|
|
||||||
|
String requireResidentKey = policy.getRequireResidentKey();
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY, requireResidentKey);
|
||||||
|
|
||||||
|
String userVerificationRequirement = policy.getUserVerificationRequirement();
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT, userVerificationRequirement);
|
||||||
|
|
||||||
|
int createTime = policy.getCreateTimeout();
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT, Integer.toString(createTime));
|
||||||
|
|
||||||
|
boolean avoidSameAuthenticatorRegister = policy.isAvoidSameAuthenticatorRegister();
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER, Boolean.toString(avoidSameAuthenticatorRegister));
|
||||||
|
|
||||||
|
List<String> acceptableAaguids = policy.getAcceptableAaguids();
|
||||||
|
if (acceptableAaguids != null && !acceptableAaguids.isEmpty()) {
|
||||||
|
String acceptableAaguidsString = String.join(",", acceptableAaguids);
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS, acceptableAaguidsString);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
|
|
|
@ -34,4 +34,17 @@ public interface RealmAttributes {
|
||||||
String OFFLINE_SESSION_MAX_LIFESPAN_ENABLED = "offlineSessionMaxLifespanEnabled";
|
String OFFLINE_SESSION_MAX_LIFESPAN_ENABLED = "offlineSessionMaxLifespanEnabled";
|
||||||
|
|
||||||
String OFFLINE_SESSION_MAX_LIFESPAN = "offlineSessionMaxLifespan";
|
String OFFLINE_SESSION_MAX_LIFESPAN = "offlineSessionMaxLifespan";
|
||||||
|
|
||||||
|
String WEBAUTHN_POLICY_RP_ENTITY_NAME = "webAuthnPolicyRpEntityName";
|
||||||
|
String WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = "webAuthnPolicySignatureAlgorithms";
|
||||||
|
|
||||||
|
String WEBAUTHN_POLICY_RP_ID = "webAuthnPolicyRpId";
|
||||||
|
String WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE = "webAuthnPolicyAttestationConveyancePreference";
|
||||||
|
String WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT = "webAuthnPolicyAuthenticatorAttachment";
|
||||||
|
String WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY = "webAuthnPolicyRequireResidentKey";
|
||||||
|
String WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT = "webAuthnPolicyUserVerificationRequirement";
|
||||||
|
String WEBAUTHN_POLICY_CREATE_TIMEOUT = "webAuthnPolicyCreateTimeout";
|
||||||
|
String WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER = "webAuthnPolicyAvoidSameAuthenticatorRegister";
|
||||||
|
String WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS = "webAuthnPolicyAcceptableAaguids";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
19
pom.xml
19
pom.xml
|
@ -162,6 +162,9 @@
|
||||||
<spring-boot15.version>1.5.20.RELEASE</spring-boot15.version>
|
<spring-boot15.version>1.5.20.RELEASE</spring-boot15.version>
|
||||||
<spring-boot21.version>2.1.3.RELEASE</spring-boot21.version>
|
<spring-boot21.version>2.1.3.RELEASE</spring-boot21.version>
|
||||||
|
|
||||||
|
<!-- webauthn support -->
|
||||||
|
<webauthn4j.version>0.9.7.RELEASE</webauthn4j.version>
|
||||||
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<url>http://keycloak.org</url>
|
<url>http://keycloak.org</url>
|
||||||
|
@ -1413,6 +1416,22 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<type>zip</type>
|
<type>zip</type>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- webauthn support -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthn4j-core</artifactId>
|
||||||
|
<version>${webauthn4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthn4j-util</artifactId>
|
||||||
|
<version>${webauthn4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-cbor</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.models;
|
package org.keycloak.models;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -55,6 +56,11 @@ public final class Constants {
|
||||||
// 60 days
|
// 60 days
|
||||||
public static final int DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN = 5184000;
|
public static final int DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN = 5184000;
|
||||||
|
|
||||||
|
public static final String DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = Algorithm.ES256;
|
||||||
|
public static final String DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME = "keycloak";
|
||||||
|
// it stands for optional parameter not specified in WebAuthn
|
||||||
|
public static final String DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED = "not specified";
|
||||||
|
|
||||||
public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
||||||
public static final String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
|
public static final String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
|
||||||
public static final String EXECUTION = "execution";
|
public static final String EXECUTION = "execution";
|
||||||
|
|
|
@ -343,6 +343,17 @@ public class ModelToRepresentation {
|
||||||
rep.setOtpPolicyType(otpPolicy.getType());
|
rep.setOtpPolicyType(otpPolicy.getType());
|
||||||
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
|
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
|
||||||
rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
|
rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
|
||||||
|
WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy();
|
||||||
|
rep.setWebAuthnPolicyRpEntityName(webAuthnPolicy.getRpEntityName());
|
||||||
|
rep.setWebAuthnPolicySignatureAlgorithms(webAuthnPolicy.getSignatureAlgorithm());
|
||||||
|
rep.setWebAuthnPolicyRpId(webAuthnPolicy.getRpId());
|
||||||
|
rep.setWebAuthnPolicyAttestationConveyancePreference(webAuthnPolicy.getAttestationConveyancePreference());
|
||||||
|
rep.setWebAuthnPolicyAuthenticatorAttachment(webAuthnPolicy.getAuthenticatorAttachment());
|
||||||
|
rep.setWebAuthnPolicyRequireResidentKey(webAuthnPolicy.getRequireResidentKey());
|
||||||
|
rep.setWebAuthnPolicyUserVerificationRequirement(webAuthnPolicy.getUserVerificationRequirement());
|
||||||
|
rep.setWebAuthnPolicyCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
||||||
|
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
||||||
|
rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
|
||||||
if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
|
if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
|
||||||
if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
|
if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
|
||||||
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
|
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.models.utils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -84,6 +85,7 @@ import org.keycloak.models.UserConsentModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserProvider;
|
import org.keycloak.models.UserProvider;
|
||||||
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
import org.keycloak.models.cache.UserCache;
|
import org.keycloak.models.cache.UserCache;
|
||||||
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||||
import org.keycloak.policy.PasswordPolicyNotMetException;
|
import org.keycloak.policy.PasswordPolicyNotMetException;
|
||||||
|
@ -261,6 +263,55 @@ public class RepresentationToModel {
|
||||||
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
|
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
|
||||||
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
||||||
|
|
||||||
|
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
|
||||||
|
|
||||||
|
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyRpEntityName();
|
||||||
|
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
|
||||||
|
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||||
|
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
|
||||||
|
|
||||||
|
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
|
||||||
|
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
|
||||||
|
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
|
||||||
|
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
|
||||||
|
|
||||||
|
String webAuthnPolicyRpId = rep.getWebAuthnPolicyRpId();
|
||||||
|
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
|
||||||
|
webAuthnPolicyRpId = "";
|
||||||
|
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
|
||||||
|
|
||||||
|
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
|
||||||
|
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
|
||||||
|
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
|
||||||
|
|
||||||
|
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
|
||||||
|
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
|
||||||
|
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
|
||||||
|
|
||||||
|
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
|
||||||
|
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
|
||||||
|
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
|
||||||
|
|
||||||
|
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
|
||||||
|
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
|
||||||
|
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
|
||||||
|
|
||||||
|
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyCreateTimeout();
|
||||||
|
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
|
||||||
|
else webAuthnPolicy.setCreateTimeout(0);
|
||||||
|
|
||||||
|
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
|
||||||
|
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||||
|
|
||||||
|
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
||||||
|
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||||
|
|
||||||
|
newRealm.setWebAuthnPolicy(webAuthnPolicy);
|
||||||
|
|
||||||
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
|
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
|
||||||
if (rep.getRequiredActions() != null) {
|
if (rep.getRequiredActions() != null) {
|
||||||
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
|
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
|
||||||
|
@ -961,6 +1012,55 @@ public class RepresentationToModel {
|
||||||
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
|
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
|
||||||
|
|
||||||
|
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyRpEntityName();
|
||||||
|
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
|
||||||
|
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||||
|
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
|
||||||
|
|
||||||
|
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
|
||||||
|
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
|
||||||
|
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
|
||||||
|
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
|
||||||
|
|
||||||
|
String webAuthnPolicyRpId = rep.getWebAuthnPolicyRpId();
|
||||||
|
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
|
||||||
|
webAuthnPolicyRpId = "";
|
||||||
|
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
|
||||||
|
|
||||||
|
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
|
||||||
|
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
|
||||||
|
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
|
||||||
|
|
||||||
|
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
|
||||||
|
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
|
||||||
|
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
|
||||||
|
|
||||||
|
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
|
||||||
|
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
|
||||||
|
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
|
||||||
|
|
||||||
|
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
|
||||||
|
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
|
||||||
|
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
|
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
|
||||||
|
|
||||||
|
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyCreateTimeout();
|
||||||
|
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
|
||||||
|
else webAuthnPolicy.setCreateTimeout(0);
|
||||||
|
|
||||||
|
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
|
||||||
|
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||||
|
|
||||||
|
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
||||||
|
webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||||
|
|
||||||
|
realm.setWebAuthnPolicy(webAuthnPolicy);
|
||||||
|
|
||||||
if (rep.getSmtpServer() != null) {
|
if (rep.getSmtpServer() != null) {
|
||||||
Map<String, String> config = new HashMap(rep.getSmtpServer());
|
Map<String, String> config = new HashMap(rep.getSmtpServer());
|
||||||
if (rep.getSmtpServer().containsKey("password") && ComponentRepresentation.SECRET_VALUE.equals(rep.getSmtpServer().get("password"))) {
|
if (rep.getSmtpServer().containsKey("password") && ComponentRepresentation.SECRET_VALUE.equals(rep.getSmtpServer().get("password"))) {
|
||||||
|
|
|
@ -240,6 +240,9 @@ public interface RealmModel extends RoleContainerModel {
|
||||||
OTPPolicy getOTPPolicy();
|
OTPPolicy getOTPPolicy();
|
||||||
void setOTPPolicy(OTPPolicy policy);
|
void setOTPPolicy(OTPPolicy policy);
|
||||||
|
|
||||||
|
WebAuthnPolicy getWebAuthnPolicy();
|
||||||
|
void setWebAuthnPolicy(WebAuthnPolicy policy);
|
||||||
|
|
||||||
RoleModel getRoleById(String id);
|
RoleModel getRoleById(String id);
|
||||||
|
|
||||||
List<GroupModel> getDefaultGroups();
|
List<GroupModel> getDefaultGroups();
|
||||||
|
|
133
server-spi/src/main/java/org/keycloak/models/WebAuthnPolicy.java
Normal file
133
server-spi/src/main/java/org/keycloak/models/WebAuthnPolicy.java
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
|
||||||
|
public class WebAuthnPolicy implements Serializable {
|
||||||
|
|
||||||
|
protected static final Logger logger = Logger.getLogger(WebAuthnPolicy.class);
|
||||||
|
// required
|
||||||
|
protected String rpEntityName;
|
||||||
|
protected List<String> signatureAlgorithms;
|
||||||
|
// optional
|
||||||
|
protected String rpId;
|
||||||
|
protected String attestationConveyancePreference;
|
||||||
|
protected String authenticatorAttachment;
|
||||||
|
protected String requireResidentKey;
|
||||||
|
protected String userVerificationRequirement;
|
||||||
|
protected int createTimeout = 0; // not specified as option
|
||||||
|
protected boolean avoidSameAuthenticatorRegister = false;
|
||||||
|
protected List<String> acceptableAaguids;
|
||||||
|
|
||||||
|
public WebAuthnPolicy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebAuthnPolicy(List<String> signatureAlgorithms) {
|
||||||
|
this.signatureAlgorithms = signatureAlgorithms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : must be thread safe list
|
||||||
|
public static WebAuthnPolicy DEFAULT_POLICY = new WebAuthnPolicy(new ArrayList<>(Arrays.asList(Algorithm.ES256)));
|
||||||
|
|
||||||
|
public String getRpEntityName() {
|
||||||
|
return rpEntityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRpEntityName(String rpEntityName) {
|
||||||
|
this.rpEntityName = rpEntityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getSignatureAlgorithm() {
|
||||||
|
return signatureAlgorithms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignatureAlgorithm(List<String> signatureAlgorithms) {
|
||||||
|
this.signatureAlgorithms = signatureAlgorithms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRpId() {
|
||||||
|
return rpId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRpId(String rpId) {
|
||||||
|
this.rpId = rpId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAttestationConveyancePreference() {
|
||||||
|
return attestationConveyancePreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttestationConveyancePreference(String attestationConveyancePreference) {
|
||||||
|
this.attestationConveyancePreference = attestationConveyancePreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthenticatorAttachment() {
|
||||||
|
return authenticatorAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthenticatorAttachment(String authenticatorAttachment) {
|
||||||
|
this.authenticatorAttachment = authenticatorAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequireResidentKey() {
|
||||||
|
return requireResidentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequireResidentKey(String requireResidentKey) {
|
||||||
|
this.requireResidentKey = requireResidentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserVerificationRequirement() {
|
||||||
|
return userVerificationRequirement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserVerificationRequirement(String userVerificationRequirement) {
|
||||||
|
this.userVerificationRequirement = userVerificationRequirement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCreateTimeout() {
|
||||||
|
return createTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreateTimeout(int createTimeout) {
|
||||||
|
this.createTimeout = createTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAvoidSameAuthenticatorRegister() {
|
||||||
|
return avoidSameAuthenticatorRegister;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvoidSameAuthenticatorRegister(boolean avoidSameAuthenticatorRegister) {
|
||||||
|
this.avoidSameAuthenticatorRegister = avoidSameAuthenticatorRegister;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAcceptableAaguids() {
|
||||||
|
return acceptableAaguids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAcceptableAaguids(List<String> acceptableAaguids) {
|
||||||
|
this.acceptableAaguids = acceptableAaguids;
|
||||||
|
}
|
||||||
|
}
|
|
@ -186,6 +186,10 @@
|
||||||
<groupId>com.openshift</groupId>
|
<groupId>com.openshift</groupId>
|
||||||
<artifactId>openshift-restclient-java</artifactId>
|
<artifactId>openshift-restclient-java</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.webauthn4j</groupId>
|
||||||
|
<artifactId>webauthn4j-core</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
68
services/src/main/java/org/keycloak/WebAuthnConstants.java
Normal file
68
services/src/main/java/org/keycloak/WebAuthnConstants.java
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak;
|
||||||
|
|
||||||
|
public interface WebAuthnConstants {
|
||||||
|
|
||||||
|
// Interface binded by FreeMarker template between UA and RP
|
||||||
|
final String USER_ID = "userid";
|
||||||
|
final String USER_NAME = "username";
|
||||||
|
final String CHALLENGE = "challenge";
|
||||||
|
final String ORIGIN = "origin";
|
||||||
|
final String ERROR = "error";
|
||||||
|
final String PUBLIC_KEY_CREDENTIAL_ID= "publicKeyCredentialId";
|
||||||
|
final String CREDENTIAL_ID = "credentialId";
|
||||||
|
final String CLIENT_DATA_JSON = "clientDataJSON";
|
||||||
|
final String AUTHENTICATOR_DATA = "authenticatorData";
|
||||||
|
final String SIGNATURE = "signature";
|
||||||
|
final String USER_HANDLE = "userHandle";
|
||||||
|
final String ATTESTATION_OBJECT= "attestationObject";
|
||||||
|
final String AUTHENTICATOR_LABEL = "authenticatorLabel";
|
||||||
|
final String RP_ENTITY_NAME = "rpEntityName";
|
||||||
|
final String SIGNATURE_ALGORITHMS = "signatureAlgorithms";
|
||||||
|
final String RP_ID = "rpId";
|
||||||
|
final String ATTESTATION_CONVEYANCE_PREFERENCE = "attestationConveyancePreference";
|
||||||
|
final String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment";
|
||||||
|
final String REQUIRE_RESIDENT_KEY = "requireResidentKey";
|
||||||
|
final String USER_VERIFICATION_REQUIREMENT = "userVerificationRequirement";
|
||||||
|
final String CREATE_TIMEOUT = "createTimeout";
|
||||||
|
final String EXCLUDE_CREDENTIAL_IDS = "excludeCredentialIds";
|
||||||
|
final String ALLOWED_AUTHENTICATORS = "authenticators";
|
||||||
|
final String IS_USER_IDENTIFIED = "isUserIdentified";
|
||||||
|
final String USER_VERIFICATION = "userVerification";
|
||||||
|
|
||||||
|
|
||||||
|
// key for storing onto UserModel's Attribute public key credential id generated by navigator.credentials.create()
|
||||||
|
final String PUBKEY_CRED_ID_ATTR = "public_key_credential_id";
|
||||||
|
|
||||||
|
// key for storing onto UserModel's Attribute Public Key Credential's user-editable metadata
|
||||||
|
final String PUBKEY_CRED_LABEL_ATTR = "public_key_credential_label";
|
||||||
|
|
||||||
|
// key for storing onto UserModel's Attribute Public Key Credential's AAGUID
|
||||||
|
final String PUBKEY_CRED_AAGUID_ATTR = "public_key_credential_aaguid";
|
||||||
|
|
||||||
|
// key for storing onto AuthenticationSessionModel's Attribute challenge generated by RP(keycloak)
|
||||||
|
final String AUTH_CHALLENGE_NOTE = "WEBAUTH_CHALLENGE";
|
||||||
|
|
||||||
|
// option values on WebAuth API
|
||||||
|
final String OPTION_REQUIRED = "required";
|
||||||
|
final String OPTION_PREFERED = "preferred";
|
||||||
|
final String OPTION_DISCOURAGED = "discouraged";
|
||||||
|
final String OPTION_NOT_SPECIFIED = "";
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,263 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
|
import com.webauthn4j.data.WebAuthnAuthenticationContext;
|
||||||
|
import com.webauthn4j.data.client.Origin;
|
||||||
|
import com.webauthn4j.data.client.challenge.Challenge;
|
||||||
|
import com.webauthn4j.data.client.challenge.DefaultChallenge;
|
||||||
|
import com.webauthn4j.server.ServerProperty;
|
||||||
|
import com.webauthn4j.util.exception.WebAuthnException;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.WebAuthnConstants;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.UriUtils;
|
||||||
|
import org.keycloak.credential.WebAuthnCredentialModel;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
|
import org.keycloak.forms.login.freemarker.model.WebAuthnAuthenticatorsBean;
|
||||||
|
import org.keycloak.models.AuthenticatorConfigModel;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
public class WebAuthnAuthenticator implements Authenticator {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(WebAuthnAuthenticator.class);
|
||||||
|
private KeycloakSession session;
|
||||||
|
|
||||||
|
public WebAuthnAuthenticator(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
LoginFormsProvider form = context.form();
|
||||||
|
|
||||||
|
Challenge challenge = new DefaultChallenge();
|
||||||
|
String challengeValue = Base64Url.encode(challenge.getValue());
|
||||||
|
context.getAuthenticationSession().setAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE, challengeValue);
|
||||||
|
form.setAttribute(WebAuthnConstants.CHALLENGE, challengeValue);
|
||||||
|
|
||||||
|
String rpId = context.getRealm().getWebAuthnPolicy().getRpId();
|
||||||
|
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
|
||||||
|
form.setAttribute(WebAuthnConstants.RP_ID, rpId);
|
||||||
|
|
||||||
|
UserModel user = context.getUser();
|
||||||
|
boolean isUserIdentified = false;
|
||||||
|
if (user != null) {
|
||||||
|
// in 2 Factor Scenario where the user has already been identified
|
||||||
|
WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(user);
|
||||||
|
if (authenticators.getAuthenticators().isEmpty()) {
|
||||||
|
// require the user to register webauthn authenticator
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isUserIdentified = true;
|
||||||
|
form.setAttribute(WebAuthnConstants.ALLOWED_AUTHENTICATORS, authenticators);
|
||||||
|
} else {
|
||||||
|
// in ID-less & Password-less Scenario
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
form.setAttribute(WebAuthnConstants.IS_USER_IDENTIFIED, Boolean.toString(isUserIdentified));
|
||||||
|
|
||||||
|
// read options from policy
|
||||||
|
String userVerificationRequirement = context.getRealm().getWebAuthnPolicy().getUserVerificationRequirement();
|
||||||
|
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
|
||||||
|
|
||||||
|
context.challenge(form.createForm("webauthn-authenticate.ftl"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
|
||||||
|
// receive error from navigator.credentials.get()
|
||||||
|
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
|
||||||
|
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
|
||||||
|
setErrorResponse(context, ERR_WEBAUTHN_API_GET, errorMsgFromWebAuthnApi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseUrl = UriUtils.getOrigin(context.getUriInfo().getBaseUri());
|
||||||
|
String rpId = context.getUriInfo().getBaseUri().getHost();
|
||||||
|
|
||||||
|
Origin origin = new Origin(baseUrl);
|
||||||
|
Challenge challenge = new DefaultChallenge(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE));
|
||||||
|
ServerProperty server = new ServerProperty(origin, rpId, challenge, null);
|
||||||
|
|
||||||
|
byte[] credentialId = Base64Url.decode(params.getFirst(WebAuthnConstants.CREDENTIAL_ID));
|
||||||
|
byte[] clientDataJSON = Base64Url.decode(params.getFirst(WebAuthnConstants.CLIENT_DATA_JSON));
|
||||||
|
byte[] authenticatorData = Base64Url.decode(params.getFirst(WebAuthnConstants.AUTHENTICATOR_DATA));
|
||||||
|
byte[] signature = Base64Url.decode(params.getFirst(WebAuthnConstants.SIGNATURE));
|
||||||
|
|
||||||
|
String userId = params.getFirst(WebAuthnConstants.USER_HANDLE);
|
||||||
|
boolean isUVFlagChecked = false;
|
||||||
|
String userVerificationRequirement = context.getRealm().getWebAuthnPolicy().getUserVerificationRequirement();
|
||||||
|
if (WebAuthnConstants.OPTION_REQUIRED.equals(userVerificationRequirement)) isUVFlagChecked = true;
|
||||||
|
|
||||||
|
// existing User Handle means that the authenticator used Resident Key supported public key credential
|
||||||
|
if (userId == null || userId.isEmpty()) {
|
||||||
|
// Resident Key not supported public key credential was used
|
||||||
|
// so rely on the user that has already been authenticated
|
||||||
|
userId = context.getUser().getId();
|
||||||
|
} else {
|
||||||
|
if (context.getUser() != null) {
|
||||||
|
// Resident Key supported public key credential was used,
|
||||||
|
// so need to confirm whether the already authenticated user is equals to one authenticated by the webauthn authenticator
|
||||||
|
String firstAuthenticatedUserId = context.getUser().getId();
|
||||||
|
if (firstAuthenticatedUserId != null && !firstAuthenticatedUserId.equals(userId)) {
|
||||||
|
context.getEvent()
|
||||||
|
.detail("first_authenticated_user_id", firstAuthenticatedUserId)
|
||||||
|
.detail("web_authn_authenticator_authenticated_user_id", userId);
|
||||||
|
setErrorResponse(context, ERR_DIFFERENT_USER_AUTHENTICATED, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Resident Key supported public key credential was used,
|
||||||
|
// and the user has not yet been identified
|
||||||
|
// so rely on the user authenticated by the webauthn authenticator
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserModel user = session.users().getUserById(userId, context.getRealm());
|
||||||
|
WebAuthnAuthenticationContext authenticationContext = new WebAuthnAuthenticationContext(
|
||||||
|
credentialId,
|
||||||
|
clientDataJSON,
|
||||||
|
authenticatorData,
|
||||||
|
signature,
|
||||||
|
server,
|
||||||
|
isUVFlagChecked
|
||||||
|
);
|
||||||
|
|
||||||
|
WebAuthnCredentialModel cred = new WebAuthnCredentialModel();
|
||||||
|
cred.setAuthenticationContext(authenticationContext);
|
||||||
|
|
||||||
|
boolean result = false;
|
||||||
|
try {
|
||||||
|
result = session.userCredentialManager().isValid(context.getRealm(), user, cred);
|
||||||
|
} catch (WebAuthnException wae) {
|
||||||
|
setErrorResponse(context, ERR_WEBAUTHN_VERIFICATION_FAIL, wae.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String encodedCredentialID = Base64Url.encode(credentialId);
|
||||||
|
if (result) {
|
||||||
|
String isUVChecked = Boolean.toString(isUVFlagChecked);
|
||||||
|
logger.infov("WebAuthn Authentication successed. isUserVerificationChecked = {0}, PublicKeyCredentialID = {1}", isUVChecked, encodedCredentialID);
|
||||||
|
context.setUser(user);
|
||||||
|
context.getEvent()
|
||||||
|
.detail("web_authn_authenticator_user_verification_checked", isUVChecked)
|
||||||
|
.detail("public_key_credential_id", encodedCredentialID);
|
||||||
|
context.success();
|
||||||
|
} else {
|
||||||
|
context.getEvent()
|
||||||
|
.detail("web_authn_authenticated_user_id", userId)
|
||||||
|
.detail("public_key_credential_id", encodedCredentialID);
|
||||||
|
setErrorResponse(context, ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND, null);
|
||||||
|
context.cancelLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean requiresUser() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
return session.userCredentialManager().isConfiguredFor(realm, user, WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
// ask the user to do required action to register webauthn authenticator
|
||||||
|
if (!user.getRequiredActions().contains(WebAuthnRegisterFactory.PROVIDER_ID)) {
|
||||||
|
user.addRequiredAction(WebAuthnRegisterFactory.PROVIDER_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String ERR_LABEL = "web_authn_authentication_error";
|
||||||
|
private static final String ERR_DETAIL_LABEL = "web_authn_authentication_error_detail";
|
||||||
|
private static final String ERR_NO_AUTHENTICATORS_REGISTERED = "No WebAuthn Authenticator registered.";
|
||||||
|
private static final String ERR_WEBAUTHN_API_GET = "Failed to authenticate by the WebAuthn Authenticator";
|
||||||
|
private static final String ERR_DIFFERENT_USER_AUTHENTICATED = "First authenticated user is not the one authenticated by the WebAuthn authenticator.";
|
||||||
|
private static final String ERR_WEBAUTHN_VERIFICATION_FAIL = "WetAuthn Authentication result is invalid.";
|
||||||
|
private static final String ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND = "Unknown user authenticated by the WebAuthen Authenticator";
|
||||||
|
|
||||||
|
private void setErrorResponse(AuthenticationFlowContext context, final String errorCase, final String errorMessage) {
|
||||||
|
Response errorResponse = null;
|
||||||
|
switch (errorCase) {
|
||||||
|
case ERR_NO_AUTHENTICATORS_REGISTERED:
|
||||||
|
logger.warn(errorCase);
|
||||||
|
context.getEvent()
|
||||||
|
.detail(ERR_LABEL, errorCase)
|
||||||
|
.error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
|
errorResponse = context.form()
|
||||||
|
.setError(errorCase)
|
||||||
|
.createErrorPage(Response.Status.BAD_REQUEST);
|
||||||
|
context.failure(AuthenticationFlowError.INVALID_CREDENTIALS, errorResponse);
|
||||||
|
break;
|
||||||
|
case ERR_WEBAUTHN_API_GET:
|
||||||
|
logger.warnv("error returned from navigator.credentials.get(). {0}", errorMessage);
|
||||||
|
context.getEvent()
|
||||||
|
.detail(ERR_LABEL, errorCase)
|
||||||
|
.detail(ERR_DETAIL_LABEL, errorMessage)
|
||||||
|
.error(Errors.NOT_ALLOWED);
|
||||||
|
errorResponse = context.form()
|
||||||
|
.setError(errorCase)
|
||||||
|
.createErrorPage(Response.Status.BAD_REQUEST);
|
||||||
|
context.failure(AuthenticationFlowError.INVALID_USER, errorResponse);
|
||||||
|
break;
|
||||||
|
case ERR_DIFFERENT_USER_AUTHENTICATED:
|
||||||
|
logger.warn(errorCase);
|
||||||
|
context.getEvent()
|
||||||
|
.detail(ERR_LABEL, errorCase)
|
||||||
|
.error(Errors.DIFFERENT_USER_AUTHENTICATED);
|
||||||
|
errorResponse = context.form()
|
||||||
|
.setError(errorCase)
|
||||||
|
.createErrorPage(Response.Status.BAD_REQUEST);
|
||||||
|
context.failure(AuthenticationFlowError.USER_CONFLICT, errorResponse);
|
||||||
|
break;
|
||||||
|
case ERR_WEBAUTHN_VERIFICATION_FAIL:
|
||||||
|
logger.warnv("WebAuthn API .get() response validation failure. {0}", errorMessage);
|
||||||
|
context.getEvent()
|
||||||
|
.detail(ERR_LABEL, errorCase)
|
||||||
|
.detail(ERR_DETAIL_LABEL, errorMessage)
|
||||||
|
.error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
|
errorResponse = context.form()
|
||||||
|
.setError(errorCase)
|
||||||
|
.createErrorPage(Response.Status.BAD_REQUEST);
|
||||||
|
context.failure(AuthenticationFlowError.INVALID_USER, errorResponse);
|
||||||
|
break;
|
||||||
|
case ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND:
|
||||||
|
logger.warn(errorCase);
|
||||||
|
context.getEvent().detail(ERR_LABEL, errorCase);
|
||||||
|
context.getEvent().error(Errors.USER_NOT_FOUND);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.WebAuthnConstants;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "webauthn-authenticator";
|
||||||
|
|
||||||
|
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||||
|
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||||
|
AuthenticationExecutionModel.Requirement.OPTIONAL,
|
||||||
|
AuthenticationExecutionModel.Requirement.DISABLED,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayType() {
|
||||||
|
return "WebAuthn Authenticator";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReferenceCategory() {
|
||||||
|
return "auth";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfigurable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||||
|
return REQUIREMENT_CHOICES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUserSetupAllowed() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Authenticator for WebAuthn";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator create(KeycloakSession session) {
|
||||||
|
return new WebAuthnAuthenticator(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,299 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.WebAuthnConstants;
|
||||||
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.UriUtils;
|
||||||
|
import org.keycloak.credential.WebAuthnCredentialModel;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
|
|
||||||
|
import com.webauthn4j.data.WebAuthnRegistrationContext;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
||||||
|
import com.webauthn4j.data.attestation.statement.AttestationStatement;
|
||||||
|
import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier;
|
||||||
|
import com.webauthn4j.data.client.Origin;
|
||||||
|
import com.webauthn4j.data.client.challenge.Challenge;
|
||||||
|
import com.webauthn4j.data.client.challenge.DefaultChallenge;
|
||||||
|
import com.webauthn4j.server.ServerProperty;
|
||||||
|
import com.webauthn4j.util.exception.WebAuthnException;
|
||||||
|
import com.webauthn4j.validator.WebAuthnRegistrationContextValidationResponse;
|
||||||
|
import com.webauthn4j.validator.WebAuthnRegistrationContextValidator;
|
||||||
|
|
||||||
|
public class WebAuthnRegister implements RequiredActionProvider {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(WebAuthnRegister.class);
|
||||||
|
private KeycloakSession session;
|
||||||
|
|
||||||
|
public WebAuthnRegister(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
|
UserModel userModel = context.getUser();
|
||||||
|
String userid = userModel.getId();
|
||||||
|
String username = userModel.getUsername();
|
||||||
|
Challenge challenge = new DefaultChallenge();
|
||||||
|
String challengeValue = Base64Url.encode(challenge.getValue());
|
||||||
|
context.getAuthenticationSession().setAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE, challengeValue);
|
||||||
|
|
||||||
|
// construct parameters for calling WebAuthn API navigator.credential.create()
|
||||||
|
|
||||||
|
// mandatory
|
||||||
|
WebAuthnPolicy policy = context.getRealm().getWebAuthnPolicy();
|
||||||
|
List<String> signatureAlgorithmsList = policy.getSignatureAlgorithm();
|
||||||
|
String signatureAlgorithms = stringifySignatureAlgorithms(signatureAlgorithmsList);
|
||||||
|
String rpEntityName = policy.getRpEntityName();
|
||||||
|
|
||||||
|
// optional
|
||||||
|
String rpId = policy.getRpId();
|
||||||
|
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
|
||||||
|
String attestationConveyancePreference = policy.getAttestationConveyancePreference();
|
||||||
|
String authenticatorAttachment = policy.getAuthenticatorAttachment();
|
||||||
|
String requireResidentKey = policy.getRequireResidentKey();
|
||||||
|
String userVerificationRequirement = policy.getUserVerificationRequirement();
|
||||||
|
long createTimeout = policy.getCreateTimeout();
|
||||||
|
boolean avoidSameAuthenticatorRegister = policy.isAvoidSameAuthenticatorRegister();
|
||||||
|
String excludeCredentialIds = avoidSameAuthenticatorRegister == true ? stringifyExcludeCredentialIds(userModel.getAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR)) : "";
|
||||||
|
|
||||||
|
Response form = context.form()
|
||||||
|
.setAttribute(WebAuthnConstants.CHALLENGE, challengeValue)
|
||||||
|
.setAttribute(WebAuthnConstants.USER_ID, userid)
|
||||||
|
.setAttribute(WebAuthnConstants.USER_NAME, username)
|
||||||
|
.setAttribute(WebAuthnConstants.RP_ENTITY_NAME, rpEntityName)
|
||||||
|
.setAttribute(WebAuthnConstants.SIGNATURE_ALGORITHMS, signatureAlgorithms)
|
||||||
|
.setAttribute(WebAuthnConstants.RP_ID, rpId)
|
||||||
|
.setAttribute(WebAuthnConstants.ATTESTATION_CONVEYANCE_PREFERENCE, attestationConveyancePreference)
|
||||||
|
.setAttribute(WebAuthnConstants.AUTHENTICATOR_ATTACHMENT, authenticatorAttachment)
|
||||||
|
.setAttribute(WebAuthnConstants.REQUIRE_RESIDENT_KEY, requireResidentKey)
|
||||||
|
.setAttribute(WebAuthnConstants.USER_VERIFICATION_REQUIREMENT, userVerificationRequirement)
|
||||||
|
.setAttribute(WebAuthnConstants.CREATE_TIMEOUT, createTimeout)
|
||||||
|
.setAttribute(WebAuthnConstants.EXCLUDE_CREDENTIAL_IDS, excludeCredentialIds.toString())
|
||||||
|
.createForm("webauthn-register.ftl");
|
||||||
|
context.challenge(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAction(RequiredActionContext context) {
|
||||||
|
|
||||||
|
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
|
||||||
|
// receive error from navigator.credentials.create()
|
||||||
|
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
|
||||||
|
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
|
||||||
|
setErrorResponse(context, ERR_WEBAUTHN_API_CREATE, errorMsgFromWebAuthnApi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebAuthnPolicy policy = context.getRealm().getWebAuthnPolicy();
|
||||||
|
String rpId = policy.getRpId();
|
||||||
|
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
|
||||||
|
String label = params.getFirst(WebAuthnConstants.AUTHENTICATOR_LABEL);
|
||||||
|
byte[] clientDataJSON = Base64.getUrlDecoder().decode(params.getFirst(WebAuthnConstants.CLIENT_DATA_JSON));
|
||||||
|
byte[] attestationObject = Base64.getUrlDecoder().decode(params.getFirst(WebAuthnConstants.ATTESTATION_OBJECT));
|
||||||
|
String publicKeyCredentialId = params.getFirst(WebAuthnConstants.PUBLIC_KEY_CREDENTIAL_ID);
|
||||||
|
|
||||||
|
Origin origin = new Origin(UriUtils.getOrigin(context.getUriInfo().getBaseUri()));
|
||||||
|
Challenge challenge = new DefaultChallenge(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE));
|
||||||
|
ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, null);
|
||||||
|
// check User Verification by considering a malicious user might modify the result of calling WebAuthn API
|
||||||
|
boolean isUserVerificationRequired = policy.getUserVerificationRequirement().equals(WebAuthnConstants.OPTION_REQUIRED) == true ? true : false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
WebAuthnRegistrationContext registrationContext = new WebAuthnRegistrationContext(clientDataJSON, attestationObject, serverProperty, isUserVerificationRequired);
|
||||||
|
// NOTE: not yet verify Attestation Statement based on certificates
|
||||||
|
WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = WebAuthnRegistrationContextValidator.createNonStrictRegistrationContextValidator();
|
||||||
|
WebAuthnRegistrationContextValidationResponse response = webAuthnRegistrationContextValidator.validate(registrationContext);
|
||||||
|
|
||||||
|
showInfoAfterWebAuthnApiCreate(response);
|
||||||
|
|
||||||
|
checkAcceptedAuthenticator(response, policy);
|
||||||
|
|
||||||
|
WebAuthnCredentialModel credential = new WebAuthnCredentialModel();
|
||||||
|
|
||||||
|
credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
|
||||||
|
credential.setAttestationStatement(response.getAttestationObject().getAttestationStatement());
|
||||||
|
credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
|
||||||
|
|
||||||
|
this.session.userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credential);
|
||||||
|
|
||||||
|
// store received Credential ID on Registration onto UserModel in order to be used on Authentication
|
||||||
|
String aaguid = response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData().getAaguid().toString();
|
||||||
|
context.getUser().setSingleAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, publicKeyCredentialId);
|
||||||
|
context.getUser().setSingleAttribute(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, label);
|
||||||
|
context.getUser().setSingleAttribute(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, aaguid);
|
||||||
|
logger.infov("WebAuthn Registration successed. publicKeyCredentialId = {0}, publicKeyCredentialLabel = {1}, publicKeyCredentialAAGUID = {2}",publicKeyCredentialId, label, aaguid);
|
||||||
|
|
||||||
|
context.getEvent()
|
||||||
|
.detail("public_key_credential_id", publicKeyCredentialId)
|
||||||
|
.detail("public_key_credential_label", label)
|
||||||
|
.detail("public_key_credential_aaguid", aaguid);
|
||||||
|
context.success();
|
||||||
|
} catch (WebAuthnException wae) {
|
||||||
|
if (logger.isDebugEnabled()) logger.debug(wae.getMessage(), wae);
|
||||||
|
setErrorResponse(context, ERR_WEBAUTHN_API_CREATE, wae.getMessage());
|
||||||
|
return;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (logger.isDebugEnabled()) logger.debug(e.getMessage(), e);
|
||||||
|
setErrorResponse(context, ERR_WEBAUTHN_API_CREATE, e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stringifySignatureAlgorithms(List<String> signatureAlgorithmsList) {
|
||||||
|
if (signatureAlgorithmsList == null || signatureAlgorithmsList.isEmpty()) return "";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String s : signatureAlgorithmsList) {
|
||||||
|
switch (s) {
|
||||||
|
case Algorithm.ES256 :
|
||||||
|
sb.append(COSEAlgorithmIdentifier.ES256.getValue()).append(",");
|
||||||
|
break;
|
||||||
|
case Algorithm.RS256 :
|
||||||
|
sb.append(COSEAlgorithmIdentifier.RS256.getValue()).append(",");
|
||||||
|
break;
|
||||||
|
case Algorithm.ES384 :
|
||||||
|
sb.append(COSEAlgorithmIdentifier.ES384.getValue()).append(",");
|
||||||
|
break;
|
||||||
|
case Algorithm.RS384 :
|
||||||
|
sb.append(COSEAlgorithmIdentifier.RS384.getValue()).append(",");
|
||||||
|
break;
|
||||||
|
case Algorithm.ES512 :
|
||||||
|
sb.append(COSEAlgorithmIdentifier.ES512.getValue()).append(",");
|
||||||
|
break;
|
||||||
|
case Algorithm.RS512 :
|
||||||
|
sb.append(COSEAlgorithmIdentifier.RS512.getValue()).append(",");
|
||||||
|
break;
|
||||||
|
case "RS1" :
|
||||||
|
sb.append(COSEAlgorithmIdentifier.RS1.getValue()).append(",");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sb.lastIndexOf(",") > -1) sb.deleteCharAt(sb.lastIndexOf(","));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stringifyExcludeCredentialIds(List<String> credentialIdsList) {
|
||||||
|
if (credentialIdsList == null || credentialIdsList.isEmpty()) return "";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String s : credentialIdsList)
|
||||||
|
if (s != null && !s.isEmpty()) sb.append(s).append(",");
|
||||||
|
if (sb.lastIndexOf(",") > -1) sb.deleteCharAt(sb.lastIndexOf(","));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showInfoAfterWebAuthnApiCreate(WebAuthnRegistrationContextValidationResponse response) {
|
||||||
|
AttestedCredentialData attestedCredentialData = response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData();
|
||||||
|
AttestationStatement attestationStatement = response.getAttestationObject().getAttestationStatement();
|
||||||
|
logger.debugv("createad key's algorithm = {0}", attestedCredentialData.getCredentialPublicKey().getAlgorithm());
|
||||||
|
logger.debugv("aaguid = {0}", attestedCredentialData.getAaguid().toString());
|
||||||
|
logger.debugv("attestation format = {0}", attestationStatement.getFormat());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkAcceptedAuthenticator(WebAuthnRegistrationContextValidationResponse response, WebAuthnPolicy policy) throws Exception {
|
||||||
|
String aaguid = response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData().getAaguid().toString();
|
||||||
|
List<String> acceptableAaguids = policy.getAcceptableAaguids();
|
||||||
|
boolean isAcceptedAuthenticator = false;
|
||||||
|
if (acceptableAaguids != null && !acceptableAaguids.isEmpty()) {
|
||||||
|
for(String acceptableAaguid : acceptableAaguids) {
|
||||||
|
if (aaguid.equals(acceptableAaguid)) {
|
||||||
|
isAcceptedAuthenticator = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no accepted authenticators means accepting any kind of authenticator
|
||||||
|
isAcceptedAuthenticator = true;
|
||||||
|
}
|
||||||
|
if (!isAcceptedAuthenticator) {
|
||||||
|
throw new WebAuthnException("not acceptable aaguid = " + aaguid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String ERR_LABEL = "web_authn_registration_error";
|
||||||
|
private static final String ERR_DETAIL_LABEL = "web_authn_registration_error_detail";
|
||||||
|
private static final String ERR_WEBAUTHN_API_CREATE = "Failed to authenticate by the WebAuthn Authenticator";
|
||||||
|
private static final String ERR_WEBAUTHN_VERIFICATION_FAIL = "WetAuthn Registration result is invalid.";
|
||||||
|
private static final String ERR_REGISTRATION_FAIL = "Failed to register your WebAuthn Authenticator.";
|
||||||
|
|
||||||
|
private void setErrorResponse(RequiredActionContext context, final String errorCase, final String errorMessage) {
|
||||||
|
Response errorResponse = null;
|
||||||
|
switch (errorCase) {
|
||||||
|
case ERR_WEBAUTHN_API_CREATE:
|
||||||
|
logger.warnv("error returned from navigator.credentials.create(). {0}", errorMessage);
|
||||||
|
context.getEvent()
|
||||||
|
.detail(ERR_LABEL, errorCase)
|
||||||
|
.detail(ERR_DETAIL_LABEL, errorMessage)
|
||||||
|
.error(Errors.NOT_ALLOWED);
|
||||||
|
errorResponse = context.form()
|
||||||
|
.setError(errorCase)
|
||||||
|
.createErrorPage(Response.Status.BAD_REQUEST);
|
||||||
|
context.challenge(errorResponse);
|
||||||
|
break;
|
||||||
|
case ERR_WEBAUTHN_VERIFICATION_FAIL:
|
||||||
|
logger.warnv("WebAuthn API .create() response validation failure. {0}", errorMessage);
|
||||||
|
context.getEvent()
|
||||||
|
.detail(ERR_LABEL, errorCase)
|
||||||
|
.detail(ERR_DETAIL_LABEL, errorMessage)
|
||||||
|
.error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
|
errorResponse = context.form()
|
||||||
|
.setError(errorCase)
|
||||||
|
.createErrorPage(Response.Status.BAD_REQUEST);
|
||||||
|
context.challenge(errorResponse);
|
||||||
|
break;
|
||||||
|
case ERR_REGISTRATION_FAIL:
|
||||||
|
logger.warn(errorCase);
|
||||||
|
context.getEvent()
|
||||||
|
.detail(ERR_LABEL, errorCase)
|
||||||
|
.detail(ERR_DETAIL_LABEL, errorMessage)
|
||||||
|
.error(Errors.INVALID_REGISTRATION);
|
||||||
|
errorResponse = context.form()
|
||||||
|
.setError(errorCase)
|
||||||
|
.createErrorPage(Response.Status.BAD_REQUEST);
|
||||||
|
context.challenge(errorResponse);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
|
||||||
|
public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTypeRequiredActionFactory {
|
||||||
|
public static final String PROVIDER_ID = "webauthn-register";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider create(KeycloakSession session) {
|
||||||
|
return new WebAuthnRegister(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Scope config) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return create(session);
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
// TODO : write console typed provider?
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayText() {
|
||||||
|
return "Webauthn Register";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.credential;
|
||||||
|
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.AAGUID;
|
||||||
|
|
||||||
|
public class AAGUIDConverter {
|
||||||
|
public byte[] convertToDatabaseColumn(AAGUID aaguid) {
|
||||||
|
return aaguid.getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AAGUID convertToEntityAttribute(byte[] bytes) {
|
||||||
|
return new AAGUID(bytes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.credential;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
|
||||||
|
import com.webauthn4j.converter.util.CborConverter;
|
||||||
|
import com.webauthn4j.data.attestation.statement.AttestationStatement;
|
||||||
|
|
||||||
|
public class AttestationStatementConverter {
|
||||||
|
|
||||||
|
private CborConverter converter;
|
||||||
|
|
||||||
|
public AttestationStatementConverter(CborConverter converter) {
|
||||||
|
this.converter = converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String convertToDatabaseColumn(AttestationStatement attribute) {
|
||||||
|
return Base64Url.encode(converter.writeValueAsBytes(attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttestationStatement convertToEntityAttribute(String dbData) {
|
||||||
|
return converter.readValue(Base64Url.decode(dbData), AttestationStatement.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.credential;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
|
||||||
|
import com.webauthn4j.converter.util.CborConverter;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.CredentialPublicKey;
|
||||||
|
|
||||||
|
public class CredentialPublicKeyConverter {
|
||||||
|
|
||||||
|
private CborConverter converter;
|
||||||
|
|
||||||
|
public CredentialPublicKeyConverter(CborConverter converter) {
|
||||||
|
this.converter = converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String convertToDatabaseColumn(CredentialPublicKey credentialPublicKey) {
|
||||||
|
return Base64Url.encode(converter.writeValueAsBytes(credentialPublicKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CredentialPublicKey convertToEntityAttribute(String s) {
|
||||||
|
return converter.readValue(Base64Url.decode(s), CredentialPublicKey.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.credential;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
|
||||||
|
import com.webauthn4j.data.WebAuthnAuthenticationContext;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.CredentialPublicKey;
|
||||||
|
import com.webauthn4j.data.attestation.statement.AttestationStatement;
|
||||||
|
|
||||||
|
public class WebAuthnCredentialModel implements CredentialInput {
|
||||||
|
|
||||||
|
public static final String WEBAUTHN_CREDENTIAL_TYPE = "webauthn";
|
||||||
|
private AttestedCredentialData attestedCredentialData;
|
||||||
|
private AttestationStatement attestationStatement;
|
||||||
|
private WebAuthnAuthenticationContext authenticationContext;
|
||||||
|
private long count;
|
||||||
|
private String authenticatorId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType() {
|
||||||
|
return WEBAUTHN_CREDENTIAL_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebAuthnCredentialModel() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttestedCredentialData getAttestedCredentialData() {
|
||||||
|
return attestedCredentialData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttestationStatement getAttestationStatement() {
|
||||||
|
return attestationStatement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebAuthnAuthenticationContext getAuthenticationContext() {
|
||||||
|
return authenticationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthenticationContext(WebAuthnAuthenticationContext authenticationContext) {
|
||||||
|
this.authenticationContext = authenticationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttestedCredentialData(AttestedCredentialData attestedCredentialData) {
|
||||||
|
this.attestedCredentialData = attestedCredentialData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttestationStatement(AttestationStatement attestationStatement) {
|
||||||
|
this.attestationStatement = attestationStatement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCount(long count) {
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthenticatorId() {
|
||||||
|
return authenticatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthenticatorId(String authenticatorId) {
|
||||||
|
this.authenticatorId = authenticatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (authenticatorId != null)
|
||||||
|
sb.append("Authenticator Id = ")
|
||||||
|
.append(authenticatorId)
|
||||||
|
.append(",");
|
||||||
|
if (attestationStatement != null)
|
||||||
|
sb.append("Attestation Statement Format = ")
|
||||||
|
.append(attestationStatement.getFormat())
|
||||||
|
.append(",");
|
||||||
|
if (attestedCredentialData != null) {
|
||||||
|
sb.append("AAGUID = ")
|
||||||
|
.append(attestedCredentialData.getAaguid().toString())
|
||||||
|
.append(",");
|
||||||
|
sb.append("CREDENTIAL_ID = ")
|
||||||
|
.append(Base64.encodeBytes(attestedCredentialData.getCredentialId()))
|
||||||
|
.append(",");
|
||||||
|
CredentialPublicKey credPubKey = attestedCredentialData.getCredentialPublicKey();
|
||||||
|
byte[] keyId = credPubKey.getKeyId();
|
||||||
|
if (keyId != null)
|
||||||
|
sb.append("CREDENTIAL_PUBLIC_KEY.key_id = ")
|
||||||
|
.append(Base64.encodeBytes(keyId))
|
||||||
|
.append(",");
|
||||||
|
sb.append("CREDENTIAL_PUBLIC_KEY.algorithm = ")
|
||||||
|
.append(credPubKey.getAlgorithm().name())
|
||||||
|
.append(",");
|
||||||
|
sb.append("CREDENTIAL_PUBLIC_KEY.key_type = ")
|
||||||
|
.append(credPubKey.getKeyType().name())
|
||||||
|
.append(",");
|
||||||
|
}
|
||||||
|
if (authenticationContext != null) {
|
||||||
|
// only set on Authentication
|
||||||
|
sb.append("Credential Id = ")
|
||||||
|
.append(Base64.encodeBytes(authenticationContext.getCredentialId()))
|
||||||
|
.append(",");
|
||||||
|
}
|
||||||
|
if (sb.length() > 0)
|
||||||
|
sb.deleteCharAt(sb.lastIndexOf(","));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.credential;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.WebAuthnConstants;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
import com.webauthn4j.authenticator.Authenticator;
|
||||||
|
import com.webauthn4j.authenticator.AuthenticatorImpl;
|
||||||
|
import com.webauthn4j.converter.util.CborConverter;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.AAGUID;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.CredentialPublicKey;
|
||||||
|
import com.webauthn4j.data.attestation.statement.AttestationStatement;
|
||||||
|
import com.webauthn4j.util.exception.WebAuthnException;
|
||||||
|
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidationResponse;
|
||||||
|
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
|
||||||
|
|
||||||
|
public class WebAuthnCredentialProvider implements CredentialProvider, CredentialInputValidator, CredentialInputUpdater {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class);
|
||||||
|
|
||||||
|
private static final String ATTESTATION_STATEMENT = "ATTESTATION_STATEMENT";
|
||||||
|
private static final String AAGUID = "AAGUID";
|
||||||
|
private static final String CREDENTIAL_ID = "CREDENTIAL_ID";
|
||||||
|
private static final String CREDENTIAL_PUBLIC_KEY = "CREDENTIAL_PUBLIC_KEY";
|
||||||
|
|
||||||
|
private KeycloakSession session;
|
||||||
|
|
||||||
|
private CredentialPublicKeyConverter credentialPublicKeyConverter;
|
||||||
|
private AttestationStatementConverter attestationStatementConverter;
|
||||||
|
|
||||||
|
public WebAuthnCredentialProvider(KeycloakSession session, CborConverter converter) {
|
||||||
|
this.session = session;
|
||||||
|
if (credentialPublicKeyConverter == null)
|
||||||
|
credentialPublicKeyConverter = new CredentialPublicKeyConverter(converter);
|
||||||
|
if (attestationStatementConverter == null)
|
||||||
|
attestationStatementConverter = new AttestationStatementConverter(converter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
|
||||||
|
if (input == null) return false;
|
||||||
|
CredentialModel model = createCredentialModel(input);
|
||||||
|
if (model == null) return false;
|
||||||
|
session.userCredentialManager().createCredential(realm, user, model);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CredentialModel createCredentialModel(CredentialInput input) {
|
||||||
|
if (!supportsCredentialType(input.getType())) return null;
|
||||||
|
|
||||||
|
WebAuthnCredentialModel webAuthnModel = (WebAuthnCredentialModel) input;
|
||||||
|
CredentialModel model = new CredentialModel();
|
||||||
|
model.setType(WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE);
|
||||||
|
model.setCreatedDate(Time.currentTimeMillis());
|
||||||
|
|
||||||
|
MultivaluedHashMap<String, String> credential = new MultivaluedHashMap<>();
|
||||||
|
|
||||||
|
credential.add(ATTESTATION_STATEMENT, attestationStatementConverter.convertToDatabaseColumn(webAuthnModel.getAttestationStatement()));
|
||||||
|
|
||||||
|
credential.add(AAGUID, webAuthnModel.getAttestedCredentialData().getAaguid().toString());
|
||||||
|
|
||||||
|
credential.add(CREDENTIAL_ID, Base64.encodeBytes(webAuthnModel.getAttestedCredentialData().getCredentialId()));
|
||||||
|
|
||||||
|
credential.add(CREDENTIAL_PUBLIC_KEY, credentialPublicKeyConverter.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCredentialPublicKey()));
|
||||||
|
|
||||||
|
model.setId(webAuthnModel.getAuthenticatorId());
|
||||||
|
|
||||||
|
model.setConfig(credential);
|
||||||
|
|
||||||
|
// authenticator's counter
|
||||||
|
model.setValue(String.valueOf(webAuthnModel.getCount()));
|
||||||
|
|
||||||
|
if(logger.isDebugEnabled()) {
|
||||||
|
dumpCredentialModel(model);
|
||||||
|
dumpWebAuthnCredentialModel(webAuthnModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
|
||||||
|
if (!supportsCredentialType(credentialType)) return;
|
||||||
|
// delete webauthn authenticator's credential itself
|
||||||
|
for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType)) {
|
||||||
|
logger.infov("Delete public key credential. username = {0}, credentialType = {1}", user.getUsername(), credentialType);
|
||||||
|
dumpCredentialModel(credential);
|
||||||
|
session.userCredentialManager().removeStoredCredential(realm, user, credential.getId());
|
||||||
|
}
|
||||||
|
// delete webauthn authenticator's metadata
|
||||||
|
user.removeAttribute(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR);
|
||||||
|
user.removeAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||||
|
user.removeAttribute(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
|
||||||
|
return isConfiguredFor(realm, user, WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE) ? Collections.singleton(WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE) : Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsCredentialType(String credentialType) {
|
||||||
|
return WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE.equals(credentialType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
|
||||||
|
if (!supportsCredentialType(credentialType)) return false;
|
||||||
|
return !session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||||
|
if (!WebAuthnCredentialModel.class.isInstance(input)) return false;
|
||||||
|
|
||||||
|
WebAuthnCredentialModel context = WebAuthnCredentialModel.class.cast(input);
|
||||||
|
List<WebAuthnCredentialModel> auths = getWebAuthnCredentialModelList(realm, user);
|
||||||
|
|
||||||
|
WebAuthnAuthenticationContextValidator webAuthnAuthenticationContextValidator =
|
||||||
|
new WebAuthnAuthenticationContextValidator();
|
||||||
|
try {
|
||||||
|
for (WebAuthnCredentialModel auth : auths) {
|
||||||
|
|
||||||
|
byte[] credentialId = auth.getAttestedCredentialData().getCredentialId();
|
||||||
|
if (Arrays.equals(credentialId, context.getAuthenticationContext().getCredentialId())) {
|
||||||
|
Authenticator authenticator = new AuthenticatorImpl(
|
||||||
|
auth.getAttestedCredentialData(),
|
||||||
|
auth.getAttestationStatement(),
|
||||||
|
auth.getCount()
|
||||||
|
);
|
||||||
|
|
||||||
|
// WebAuthnException is thrown if validation fails
|
||||||
|
WebAuthnAuthenticationContextValidationResponse response =
|
||||||
|
webAuthnAuthenticationContextValidator.validate(
|
||||||
|
context.getAuthenticationContext(),
|
||||||
|
authenticator);
|
||||||
|
|
||||||
|
logger.infov("response.getAuthenticatorData().getFlags() = {0}", response.getAuthenticatorData().getFlags());
|
||||||
|
|
||||||
|
// update authenticator counter
|
||||||
|
long count = auth.getCount();
|
||||||
|
auth.setCount(count + 1);
|
||||||
|
CredentialModel cred = createCredentialModel(auth);
|
||||||
|
session.userCredentialManager().updateCredential(realm, user, cred);
|
||||||
|
|
||||||
|
if(logger.isDebugEnabled()) {
|
||||||
|
dumpCredentialModel(cred);
|
||||||
|
dumpWebAuthnCredentialModel(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (WebAuthnException wae) {
|
||||||
|
wae.printStackTrace();
|
||||||
|
throw(wae);
|
||||||
|
}
|
||||||
|
// no authenticator matched
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WebAuthnCredentialModel> getWebAuthnCredentialModelList(RealmModel realm, UserModel user) {
|
||||||
|
List<WebAuthnCredentialModel> auths = new ArrayList<>();
|
||||||
|
for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE)) {
|
||||||
|
WebAuthnCredentialModel auth = new WebAuthnCredentialModel();
|
||||||
|
MultivaluedHashMap<String, String> attributes = credential.getConfig();
|
||||||
|
|
||||||
|
AttestationStatement attrStatement = attestationStatementConverter.convertToEntityAttribute(attributes.getFirst(ATTESTATION_STATEMENT));
|
||||||
|
auth.setAttestationStatement(attrStatement);
|
||||||
|
|
||||||
|
AAGUID aaguid = new AAGUID(attributes.getFirst(AAGUID));
|
||||||
|
|
||||||
|
byte[] credentialId = null;
|
||||||
|
try {
|
||||||
|
credentialId = Base64.decode(attributes.getFirst(CREDENTIAL_ID));
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
CredentialPublicKey pubKey = credentialPublicKeyConverter.convertToEntityAttribute(attributes.getFirst(CREDENTIAL_PUBLIC_KEY));
|
||||||
|
|
||||||
|
AttestedCredentialData attrCredData = new AttestedCredentialData(aaguid, credentialId, pubKey);
|
||||||
|
|
||||||
|
auth.setAttestedCredentialData(attrCredData);
|
||||||
|
|
||||||
|
long count = Long.parseLong(credential.getValue());
|
||||||
|
auth.setCount(count);
|
||||||
|
|
||||||
|
auth.setAuthenticatorId(credential.getId());
|
||||||
|
|
||||||
|
auths.add(auth);
|
||||||
|
}
|
||||||
|
return auths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dumpCredentialModel(CredentialModel credential) {
|
||||||
|
logger.debugv(" Persisted Credential Info::");
|
||||||
|
MultivaluedHashMap<String, String> attributes = credential.getConfig();
|
||||||
|
logger.debugv(" ATTESTATION_STATEMENT = {0}", attributes.getFirst(ATTESTATION_STATEMENT));
|
||||||
|
logger.debugv(" AAGUID = {0}", attributes.getFirst(AAGUID));
|
||||||
|
logger.debugv(" CREDENTIAL_ID = {0}", attributes.getFirst(CREDENTIAL_ID));
|
||||||
|
logger.debugv(" CREDENTIAL_PUBLIC_KEY = {0}", attributes.getFirst(CREDENTIAL_PUBLIC_KEY));
|
||||||
|
logger.debugv(" count = {0}", credential.getValue());
|
||||||
|
logger.debugv(" authenticator_id = {0}", credential.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dumpWebAuthnCredentialModel(WebAuthnCredentialModel auth) {
|
||||||
|
logger.debug(" Context Credential Info::");
|
||||||
|
logger.debug(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.credential;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
import com.webauthn4j.converter.util.CborConverter;
|
||||||
|
|
||||||
|
public class WebAuthnCredentialProviderFactory implements CredentialProviderFactory<WebAuthnCredentialProvider> {
|
||||||
|
|
||||||
|
private static CborConverter converter = new CborConverter();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CredentialProvider create(KeycloakSession session) {
|
||||||
|
return new WebAuthnCredentialProvider(session, converter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "keycloak-webauthn";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.forms.login.freemarker.model;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.WebAuthnConstants;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
public class WebAuthnAuthenticatorsBean {
|
||||||
|
private List<WebAuthnAuthenticatorBean> authenticators = new LinkedList<WebAuthnAuthenticatorBean>();
|
||||||
|
|
||||||
|
public WebAuthnAuthenticatorsBean(UserModel user) {
|
||||||
|
// should consider multiple credentials in the future, but only single credential supported now.
|
||||||
|
List<String> credentialIds = user.getAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||||
|
List<String> labels = user.getAttribute(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR);
|
||||||
|
if (credentialIds != null && credentialIds.size() == 1 && !credentialIds.get(0).isEmpty()) {
|
||||||
|
String credentialId = credentialIds.get(0);
|
||||||
|
String label = (labels.size() == 1 && !labels.get(0).isEmpty()) ? labels.get(0) : "label missing";
|
||||||
|
authenticators.add(new WebAuthnAuthenticatorBean(credentialId, label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<WebAuthnAuthenticatorBean> getAuthenticators() {
|
||||||
|
return authenticators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WebAuthnAuthenticatorBean {
|
||||||
|
private final String credentialId;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
public WebAuthnAuthenticatorBean(String credentialId, String label) {
|
||||||
|
this.credentialId = credentialId;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCredentialId() {
|
||||||
|
return this.credentialId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return this.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,4 +42,5 @@ org.keycloak.protocol.docker.DockerAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
|
org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFactory
|
org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
|
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
|
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
|
||||||
|
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory
|
|
@ -19,4 +19,5 @@ org.keycloak.authentication.requiredactions.UpdatePassword
|
||||||
org.keycloak.authentication.requiredactions.UpdateProfile
|
org.keycloak.authentication.requiredactions.UpdateProfile
|
||||||
org.keycloak.authentication.requiredactions.UpdateTotp
|
org.keycloak.authentication.requiredactions.UpdateTotp
|
||||||
org.keycloak.authentication.requiredactions.VerifyEmail
|
org.keycloak.authentication.requiredactions.VerifyEmail
|
||||||
org.keycloak.authentication.requiredactions.TermsAndConditions
|
org.keycloak.authentication.requiredactions.TermsAndConditions
|
||||||
|
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
|
@ -1,2 +1,3 @@
|
||||||
org.keycloak.credential.OTPCredentialProviderFactory
|
org.keycloak.credential.OTPCredentialProviderFactory
|
||||||
org.keycloak.credential.PasswordCredentialProviderFactory
|
org.keycloak.credential.PasswordCredentialProviderFactory
|
||||||
|
org.keycloak.credential.WebAuthnCredentialProviderFactory
|
|
@ -21,6 +21,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredActionProviderModel;
|
import org.keycloak.models.RequiredActionProviderModel;
|
||||||
import org.keycloak.models.RequiredCredentialModel;
|
import org.keycloak.models.RequiredCredentialModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
import org.keycloak.services.DefaultKeycloakSession;
|
import org.keycloak.services.DefaultKeycloakSession;
|
||||||
import org.keycloak.services.DefaultKeycloakSessionFactory;
|
import org.keycloak.services.DefaultKeycloakSessionFactory;
|
||||||
|
|
||||||
|
@ -627,6 +628,16 @@ public class PlainTextVaultProviderFactoryTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnPolicy getWebAuthnPolicy() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWebAuthnPolicy(WebAuthnPolicy policy) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RoleModel getRoleById(String id) {
|
public RoleModel getRoleById(String id) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.pages.webauthn;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
|
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
import org.openqa.selenium.support.FindBy;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.util.UIUtils.clickLink;
|
||||||
|
|
||||||
|
public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
||||||
|
|
||||||
|
@ArquillianResource
|
||||||
|
protected OAuthClient oauth;
|
||||||
|
|
||||||
|
@FindBy(id = "username")
|
||||||
|
private WebElement usernameInput;
|
||||||
|
|
||||||
|
@FindBy(id = "password")
|
||||||
|
private WebElement passwordInput;
|
||||||
|
|
||||||
|
@FindBy(id = "totp")
|
||||||
|
private WebElement totp;
|
||||||
|
|
||||||
|
@FindBy(id = "rememberMe")
|
||||||
|
private WebElement rememberMe;
|
||||||
|
|
||||||
|
@FindBy(name = "login")
|
||||||
|
private WebElement submitButton;
|
||||||
|
|
||||||
|
@FindBy(name = "cancel")
|
||||||
|
private WebElement cancelButton;
|
||||||
|
|
||||||
|
@FindBy(linkText = "Register")
|
||||||
|
private WebElement registerLink;
|
||||||
|
|
||||||
|
@FindBy(linkText = "Forgot Password?")
|
||||||
|
private WebElement resetPasswordLink;
|
||||||
|
|
||||||
|
@FindBy(linkText = "Username")
|
||||||
|
private WebElement recoverUsernameLink;
|
||||||
|
|
||||||
|
@FindBy(className = "alert-error")
|
||||||
|
private WebElement loginErrorMessage;
|
||||||
|
|
||||||
|
@FindBy(className = "alert-warning")
|
||||||
|
private WebElement loginWarningMessage;
|
||||||
|
|
||||||
|
@FindBy(className = "alert-success")
|
||||||
|
private WebElement loginSuccessMessage;
|
||||||
|
|
||||||
|
|
||||||
|
@FindBy(className = "alert-info")
|
||||||
|
private WebElement loginInfoMessage;
|
||||||
|
|
||||||
|
@FindBy(className = "instruction")
|
||||||
|
private WebElement instruction;
|
||||||
|
|
||||||
|
|
||||||
|
public void login(String username, String password) {
|
||||||
|
driver.findElement(By.id("username")).clear();
|
||||||
|
driver.findElement(By.id("username")).sendKeys(username);
|
||||||
|
|
||||||
|
driver.findElement(By.id("password")).clear();
|
||||||
|
driver.findElement(By.id("password")).sendKeys(password);
|
||||||
|
|
||||||
|
driver.findElement(By.name("login")).click();
|
||||||
|
|
||||||
|
driver.findElement(By.cssSelector("input[type=\"button\"]")).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void login(String password) {
|
||||||
|
driver.findElement(By.id("password")).clear();
|
||||||
|
driver.findElement(By.id("password")).sendKeys(password);
|
||||||
|
|
||||||
|
driver.findElement(By.name("login")).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void missingPassword(String username) {
|
||||||
|
driver.findElement(By.id("username")).clear();
|
||||||
|
driver.findElement(By.id("username")).sendKeys(username);
|
||||||
|
driver.findElement(By.id("password")).clear();
|
||||||
|
driver.findElement(By.name("login")).click();
|
||||||
|
|
||||||
|
}
|
||||||
|
public void missingUsername() {
|
||||||
|
driver.findElement(By.id("username")).clear();
|
||||||
|
driver.findElement(By.name("login")).click();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return driver.findElement(By.id("username")).getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUsernameInputEnabled() {
|
||||||
|
return driver.findElement(By.id("username")).isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return driver.findElement(By.id("password")).getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
driver.findElement(By.name("cancel")).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getError() {
|
||||||
|
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstruction() {
|
||||||
|
return instruction != null ? instruction.getText() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSuccessMessage() {
|
||||||
|
return loginSuccessMessage != null ? loginSuccessMessage.getText() : null;
|
||||||
|
}
|
||||||
|
public String getInfoMessage() {
|
||||||
|
return loginInfoMessage != null ? loginInfoMessage.getText() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isCurrent() {
|
||||||
|
String realm = "test";
|
||||||
|
return isCurrent(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCurrent(String realm) {
|
||||||
|
return driver.getTitle().equals("Log in to " + realm) || driver.getTitle().equals("Anmeldung bei " + realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clickRegister() {
|
||||||
|
driver.findElement(By.linkText("Register")).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clickSocial(String providerId) {
|
||||||
|
WebElement socialButton = findSocialButton(providerId);
|
||||||
|
clickLink(socialButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebElement findSocialButton(String providerId) {
|
||||||
|
String id = "zocial-" + providerId;
|
||||||
|
return this.driver.findElement(By.id(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetPassword() {
|
||||||
|
resetPasswordLink.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recoverUsername() {
|
||||||
|
recoverUsernameLink.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRememberMe(boolean enable) {
|
||||||
|
boolean current = rememberMe.isSelected();
|
||||||
|
if (current != enable) {
|
||||||
|
rememberMe.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRememberMeChecked() {
|
||||||
|
return rememberMe.isSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open() {
|
||||||
|
oauth.openLoginForm();
|
||||||
|
assertCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.pages.webauthn;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.keycloak.testsuite.pages.AbstractPage;
|
||||||
|
import org.keycloak.testsuite.pages.PageUtils;
|
||||||
|
import org.openqa.selenium.Alert;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
import org.openqa.selenium.support.FindBy;
|
||||||
|
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||||
|
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||||
|
|
||||||
|
public class WebAuthnRegisterPage extends AbstractPage {
|
||||||
|
|
||||||
|
@FindBy(id = "firstName")
|
||||||
|
private WebElement firstNameInput;
|
||||||
|
|
||||||
|
@FindBy(id = "lastName")
|
||||||
|
private WebElement lastNameInput;
|
||||||
|
|
||||||
|
@FindBy(id = "email")
|
||||||
|
private WebElement emailInput;
|
||||||
|
|
||||||
|
@FindBy(id = "username")
|
||||||
|
private WebElement usernameInput;
|
||||||
|
|
||||||
|
@FindBy(id = "password")
|
||||||
|
private WebElement passwordInput;
|
||||||
|
|
||||||
|
@FindBy(id = "password-confirm")
|
||||||
|
private WebElement passwordConfirmInput;
|
||||||
|
|
||||||
|
@FindBy(css = "input[type=\"submit\"]")
|
||||||
|
private WebElement submitButton;
|
||||||
|
|
||||||
|
@FindBy(className = "alert-error")
|
||||||
|
private WebElement loginErrorMessage;
|
||||||
|
|
||||||
|
@FindBy(className = "instruction")
|
||||||
|
private WebElement loginInstructionMessage;
|
||||||
|
|
||||||
|
@FindBy(linkText = "« Back to Login")
|
||||||
|
private WebElement backToLoginLink;
|
||||||
|
|
||||||
|
|
||||||
|
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, String authenticatorLabel) {
|
||||||
|
driver.findElement(By.id("firstName")).clear();
|
||||||
|
if (firstName != null) {
|
||||||
|
driver.findElement(By.id("firstName")).sendKeys(firstName);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("lastName")).clear();
|
||||||
|
if (lastName != null) {
|
||||||
|
driver.findElement(By.id("lastName")).sendKeys(lastName);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("email")).clear();
|
||||||
|
if (email != null) {
|
||||||
|
driver.findElement(By.id("email")).sendKeys(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("username")).clear();
|
||||||
|
if (username != null) {
|
||||||
|
driver.findElement(By.id("username")).sendKeys(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("password")).clear();
|
||||||
|
if (password != null) {
|
||||||
|
driver.findElement(By.id("password")).sendKeys(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("password-confirm")).clear();
|
||||||
|
if (passwordConfirm != null) {
|
||||||
|
driver.findElement(By.id("password-confirm")).sendKeys(passwordConfirm);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
|
||||||
|
|
||||||
|
// label edit after registering authenicator by .create()
|
||||||
|
WebDriverWait wait = new WebDriverWait(driver, 60);
|
||||||
|
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
|
||||||
|
//Alert promptDialog = driver.switchTo().alert();
|
||||||
|
promptDialog.sendKeys(authenticatorLabel);
|
||||||
|
promptDialog.accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerWithEmailAsUsername(String firstName, String lastName, String email, String password, String passwordConfirm) {
|
||||||
|
driver.findElement(By.id("firstName")).clear();
|
||||||
|
if (firstName != null) {
|
||||||
|
driver.findElement(By.id("firstName")).sendKeys(firstName);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("lastName")).clear();
|
||||||
|
if (lastName != null) {
|
||||||
|
driver.findElement(By.id("lastName")).sendKeys(lastName);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("email")).clear();
|
||||||
|
if (email != null) {
|
||||||
|
driver.findElement(By.id("email")).sendKeys(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
driver.findElement(By.id("username")).clear();
|
||||||
|
Assert.fail("Form must be without username field");
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("password")).clear();
|
||||||
|
if (password != null) {
|
||||||
|
driver.findElement(By.id("password")).sendKeys(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.findElement(By.id("password-confirm")).clear();
|
||||||
|
if (passwordConfirm != null) {
|
||||||
|
driver.findElement(By.id("password-confirm")).sendKeys(passwordConfirm);
|
||||||
|
}
|
||||||
|
driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clickBackToLogin() {
|
||||||
|
backToLoginLink.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getError() {
|
||||||
|
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstruction() {
|
||||||
|
try {
|
||||||
|
return loginInstructionMessage != null ? loginInstructionMessage.getText() : null;
|
||||||
|
} catch (NoSuchElementException e){
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstName() {
|
||||||
|
return driver.findElement(By.id("firstName")).getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastName() {
|
||||||
|
return driver.findElement(By.id("lastName")).getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return driver.findElement(By.id("email")).getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return driver.findElement(By.id("username")).getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return driver.findElement(By.id("password")).getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPasswordConfirm() {
|
||||||
|
return driver.findElement(By.id("password-confirm")).getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCurrent() {
|
||||||
|
return PageUtils.getPageTitle(driver).equals("Register");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -194,6 +194,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
||||||
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
||||||
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
|
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
|
||||||
"Testsuite Username authenticator. Username parameter sets username");
|
"Testsuite Username authenticator. Username parameter sets username");
|
||||||
|
addProviderInfo(result, "webauthn-authenticator", "WebAuthn Authenticator", "Authenticator for WebAuthn");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
|
||||||
|
|
||||||
// Just Dummy RequiredAction is not registered in the realm
|
// Just Dummy RequiredAction is not registered in the realm
|
||||||
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
|
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
|
||||||
Assert.assertEquals(1, result.size());
|
Assert.assertEquals(2, result.size());
|
||||||
RequiredActionProviderSimpleRepresentation action = result.get(0);
|
RequiredActionProviderSimpleRepresentation action = result.get(0);
|
||||||
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
|
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
|
||||||
Assert.assertEquals("Dummy Action", action.getName());
|
Assert.assertEquals("Dummy Action", action.getName());
|
||||||
|
|
|
@ -0,0 +1,221 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.webauthn;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.WebAuthnConstants;
|
||||||
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
|
import org.keycloak.common.util.RandomString;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.pages.webauthn.WebAuthnLoginPage;
|
||||||
|
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
|
||||||
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
|
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Ignore("not completed yet")
|
||||||
|
public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AppPage appPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected WebAuthnLoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected WebAuthnRegisterPage registerPage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
private List<String> signatureAlgorithms;
|
||||||
|
private String attestationConveyancePreference;
|
||||||
|
private String authenticatorAttachment;
|
||||||
|
private String requireResidentKey;
|
||||||
|
private String rpEntityName;
|
||||||
|
private String userVerificationRequirement;
|
||||||
|
private String rpId;
|
||||||
|
private int createTimeout;
|
||||||
|
private boolean avoidSameAuthenticatorRegister;
|
||||||
|
private List<String> acceptableAaguids;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setupTest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void teardown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
RealmRepresentation realmRepresentation = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/webauthn/testrealm-webauthn.json"), RealmRepresentation.class);
|
||||||
|
testRealms.add(realmRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerUserSuccess() {
|
||||||
|
String username = "registerUserSuccess";
|
||||||
|
String password = "password";
|
||||||
|
String email = "registerUserSuccess@email";
|
||||||
|
|
||||||
|
try {
|
||||||
|
RealmRepresentation rep = backupWebAuthnRealmSettings();
|
||||||
|
rep.setWebAuthnPolicySignatureAlgorithms(Arrays.asList("ES256"));
|
||||||
|
rep.setWebAuthnPolicyAttestationConveyancePreference("none");
|
||||||
|
rep.setWebAuthnPolicyAuthenticatorAttachment("cross-platform");
|
||||||
|
rep.setWebAuthnPolicyRequireResidentKey("No");
|
||||||
|
rep.setWebAuthnPolicyRpId(null);
|
||||||
|
rep.setWebAuthnPolicyUserVerificationRequirement("preferred");
|
||||||
|
rep.setWebAuthnPolicyAcceptableAaguids(Arrays.asList(ALL_ZERO_AAGUID));
|
||||||
|
testRealm().update(rep);
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
String authenticatorLabel = RandomString.randomCode(24);
|
||||||
|
registerPage.register("firstName", "lastName", email, username, password, password, authenticatorLabel);
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
appPage.openAccount();
|
||||||
|
|
||||||
|
// confirm that registration is successfully completed
|
||||||
|
String userId = events.expectRegister(username, email).assertEvent().getUserId();
|
||||||
|
// confirm registration event
|
||||||
|
EventRepresentation eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||||
|
.user(userId)
|
||||||
|
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||||
|
.assertEvent();
|
||||||
|
String regPubKeyCredentialId = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||||
|
//String regPubKeyCredentialAaguid = eventRep.getDetails().get("public_key_credential_aaguid");
|
||||||
|
//String regPubKeyCredentialLabel = eventRep.getDetails().get("public_key_credential_label");
|
||||||
|
|
||||||
|
// confirm login event
|
||||||
|
String sessionId = events.expectLogin()
|
||||||
|
.user(userId)
|
||||||
|
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||||
|
.assertEvent().getSessionId();
|
||||||
|
// confirm user registered
|
||||||
|
assertUserRegistered(userId, username.toLowerCase(), email.toLowerCase());
|
||||||
|
|
||||||
|
// logout by user
|
||||||
|
appPage.logout();
|
||||||
|
// confirm logout event
|
||||||
|
events.expectLogout(sessionId)
|
||||||
|
.user(userId)
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
// login by user
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login(username, password);
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
appPage.openAccount();
|
||||||
|
// confirm login event
|
||||||
|
sessionId = events.expectLogin()
|
||||||
|
.user(userId)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, regPubKeyCredentialId)
|
||||||
|
// .detail("web_authn_authenticator_user_verification_checked", Boolean.FALSE.toString())
|
||||||
|
.assertEvent().getSessionId();
|
||||||
|
|
||||||
|
// logout by user
|
||||||
|
appPage.logout();
|
||||||
|
// confirm logout event
|
||||||
|
events.expectLogout(sessionId)
|
||||||
|
.user(userId)
|
||||||
|
.assertEvent();
|
||||||
|
} finally {
|
||||||
|
restoreWebAuthnRealmSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertUserRegistered(String userId, String username, String email) {
|
||||||
|
UserRepresentation user = getUser(userId);
|
||||||
|
Assert.assertNotNull(user);
|
||||||
|
Assert.assertNotNull(user.getCreatedTimestamp());
|
||||||
|
// test that timestamp is current with 10s tollerance
|
||||||
|
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
|
||||||
|
// test user info is set from form
|
||||||
|
assertEquals(username.toLowerCase(), user.getUsername());
|
||||||
|
assertEquals(email.toLowerCase(), user.getEmail());
|
||||||
|
assertEquals("firstName", user.getFirstName());
|
||||||
|
assertEquals("lastName", user.getLastName());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected UserRepresentation getUser(String userId) {
|
||||||
|
return testRealm().users().get(userId).toRepresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RealmRepresentation backupWebAuthnRealmSettings() {
|
||||||
|
RealmRepresentation rep = testRealm().toRepresentation();
|
||||||
|
signatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
|
||||||
|
attestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
|
||||||
|
authenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
|
||||||
|
requireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
|
||||||
|
rpEntityName = rep.getWebAuthnPolicyRpEntityName();
|
||||||
|
userVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
|
||||||
|
rpId = rep.getWebAuthnPolicyRpId();
|
||||||
|
createTimeout = rep.getWebAuthnPolicyCreateTimeout();
|
||||||
|
avoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
|
||||||
|
acceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restoreWebAuthnRealmSettings() {
|
||||||
|
RealmRepresentation rep = testRealm().toRepresentation();
|
||||||
|
rep.setWebAuthnPolicySignatureAlgorithms(signatureAlgorithms);
|
||||||
|
rep.setWebAuthnPolicyAttestationConveyancePreference(attestationConveyancePreference);
|
||||||
|
rep.setWebAuthnPolicyAuthenticatorAttachment(authenticatorAttachment);
|
||||||
|
rep.setWebAuthnPolicyRequireResidentKey(requireResidentKey);
|
||||||
|
rep.setWebAuthnPolicyRpEntityName(rpEntityName);
|
||||||
|
rep.setWebAuthnPolicyUserVerificationRequirement(userVerificationRequirement);
|
||||||
|
rep.setWebAuthnPolicyRpId(rpId);
|
||||||
|
rep.setWebAuthnPolicyCreateTimeout(createTimeout);
|
||||||
|
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(avoidSameAuthenticatorRegister);
|
||||||
|
rep.setWebAuthnPolicyAcceptableAaguids(acceptableAaguids);
|
||||||
|
testRealm().update(rep);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -56,6 +56,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-2 col-md-2">
|
||||||
|
<label for="publicKeyCredentialId" class="control-label">${msg("Public Key Credential ID")}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-10 col-md-10">
|
||||||
|
<input type="text" class="form-control" id="user.attributes.public_key_credential_id" name="user.attributes.public_key_credential_id" disabled="disabled" value="${(account.attributes.public_key_credential_id!'')}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-2 col-md-2">
|
||||||
|
<label for="publicKeyCredentialLabel" class="control-label">${msg("Public Key Credential Label")}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-10 col-md-10">
|
||||||
|
<input type="text" class="form-control" id="user.attributes.public_key_credential_label" name="user.attributes.public_key_credential_label" value="${(account.attributes.public_key_credential_label!'')}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-2 col-md-2">
|
||||||
|
<label for="publicKeyCredentialAaguid" class="control-label">${msg("Public Key Credential AAGUID")}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-10 col-md-10">
|
||||||
|
<input type="text" class="form-control" id="user.attributes.public_key_credential_aaguid" name="user.attributes.public_key_credential_aaguid" disabled="disabled" value="${(account.attributes.public_key_credential_aaguid!'')}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||||
<div class="">
|
<div class="">
|
||||||
|
|
|
@ -1051,6 +1051,31 @@ table-of-password-policies=Table of Password Policies
|
||||||
add-policy.placeholder=Add policy...
|
add-policy.placeholder=Add policy...
|
||||||
policy-type=Policy Type
|
policy-type=Policy Type
|
||||||
policy-value=Policy Value
|
policy-value=Policy Value
|
||||||
|
webauthn-policy=WebAuthn Policy
|
||||||
|
webauthn-rp-entity-name=Relying Party Entity Name
|
||||||
|
webauthn-rp-entity-name.tooltip=Human-readable server name as WebAuthn Relying Party
|
||||||
|
webauthn-signature-algorithms=Signature Algorithms
|
||||||
|
webauthn-signature-algorithms.tooltip=What signature algorithms should be used for Authentication Assertion.
|
||||||
|
webauthn-rp-id=Relying Party ID
|
||||||
|
webauthn-rp-id.tooltip=This is ID as WebAuthn Relying Party. It must be origin's effective domain.
|
||||||
|
webauthn-attestation-conveyance-preference=Attestation Conveyance Preference
|
||||||
|
webauthn-attestation-conveyance-preference.tooltip=It tells an authenticator the preference of how to generate an attestation statement.
|
||||||
|
webauthn-authenticator-attachment=Authenticator Attachment
|
||||||
|
webauthn-authenticator-attachment.tooltip=It tells an authenticator an acceptable attachment pattern.
|
||||||
|
webauthn-require-resident-key=Require Resident Key
|
||||||
|
webauthn-require-resident-key.tooltip=It tells an authenticator create a public key credential as Resident Key or not.
|
||||||
|
webauthn-user-verification-requirement=User Verification Requirement
|
||||||
|
webauthn-user-verification-requirement.tooltip=It tells an authenticator confirm actually verifying a user.
|
||||||
|
webauthn-create-timeout=Timeout
|
||||||
|
webauthn-create-timeout.tooltip=Timeout value for creating user's public key credential in seconds. if set to 0, this timeout option is not adapted.
|
||||||
|
webauthn-avoid-same-authenticator-register=Avoid Same Authenticator Registration
|
||||||
|
webauthn-avoid-same-authenticator-register.tooltip=avoid registering the authenticator that has already been registered.
|
||||||
|
webauthn-acceptable-aaguids=Acceptable AAGUIDs
|
||||||
|
webauthn-acceptable-aaguids.tooltip=The list of AAGUID of which an authenticator can be registered.
|
||||||
|
manage-webauthn-authenticator=Manage WebAuthn Authenticator
|
||||||
|
public-key-credential-id=Public Key Credential ID
|
||||||
|
public-key-credential-aaguid=Public Key Credential AAGUID
|
||||||
|
public-key-credential-label=Public Key Credential Label
|
||||||
admin-events=Admin Events
|
admin-events=Admin Events
|
||||||
admin-events.tooltip=Displays saved admin events for the realm. Events are related to admin account, for example a realm creation. To enable persisted events go to config.
|
admin-events.tooltip=Displays saved admin events for the realm. Events are related to admin account, for example a realm creation. To enable persisted events go to config.
|
||||||
login-events=Login Events
|
login-events=Login Events
|
||||||
|
|
|
@ -2008,6 +2008,18 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'RealmOtpPolicyCtrl'
|
controller : 'RealmOtpPolicyCtrl'
|
||||||
})
|
})
|
||||||
|
.when('/realms/:realm/authentication/webauthn-policy', {
|
||||||
|
templateUrl : resourceUrl + '/partials/webauthn-policy.html',
|
||||||
|
resolve : {
|
||||||
|
realm : function(RealmLoader) {
|
||||||
|
return RealmLoader();
|
||||||
|
},
|
||||||
|
serverInfo : function(ServerInfo) {
|
||||||
|
return ServerInfo.delay;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller : 'RealmWebAuthnPolicyCtrl'
|
||||||
|
})
|
||||||
.when('/realms/:realm/authentication/flows/:flow/config/:provider/:config', {
|
.when('/realms/:realm/authentication/flows/:flow/config/:provider/:config', {
|
||||||
templateUrl : resourceUrl + '/partials/authenticator-config.html',
|
templateUrl : resourceUrl + '/partials/authenticator-config.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
|
|
|
@ -397,6 +397,19 @@ module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm,
|
||||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy");
|
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.controller('RealmWebAuthnPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
||||||
|
|
||||||
|
$scope.deleteAcceptableAaguid = function(index) {
|
||||||
|
$scope.realm.webAuthnPolicyAcceptableAaguids.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.addAcceptableAaguid = function() {
|
||||||
|
$scope.realm.webAuthnPolicyAcceptableAaguids.push($scope.newAcceptableAaguid);
|
||||||
|
$scope.newAcceptableAaguid = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy");
|
||||||
|
});
|
||||||
|
|
||||||
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
||||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
|
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<kc-tabs-user></kc-tabs-user>
|
<kc-tabs-user></kc-tabs-user>
|
||||||
|
|
||||||
<form class="form-horizontal" name="userForm" novalidate>
|
<form class="form-horizontal" name="userForm" novalidate>
|
||||||
|
|
||||||
<fieldset class="border-top">
|
<fieldset class="border-top">
|
||||||
<legend><span class="text">{{:: 'manage-user-password' | translate}}</span></legend>
|
<legend><span class="text">{{:: 'manage-user-password' | translate}}</span></legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -38,6 +39,34 @@
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="border-top">
|
||||||
|
<legend><span class="text">{{:: 'manage-webauthn-authenticator' | translate}}</span></legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="publicKeyCredentialId" class="control-label">{{:: 'public-key-credential-id' | translate}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input type="text" class="form-control" id=publicKeyCredentialId name="publicKeyCredentialId" data-ng-model="user.attributes.public_key_credential_id" data-ng-readonly="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="publicKeyCredentialAaguid" class="control-label">{{:: 'public-key-credential-aaguid' | translate}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input type="text" class="form-control" id="publicKeyCredentialAaguid" name="publicKeyCredentialAaguid" data-ng-model="user.attributes.public_key_credential_aaguid" data-ng-readonly="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="publicKeyCredentialLabel" class="control-label">{{:: 'public-key-credential-label' | translate}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input type="text" class="form-control" id="publicKeyCredentialLabel" name="publicKeyCredentialLabel" data-ng-model="user.attributes.public_key_credential_label" data-ng-readonly="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="border-top" data-ng-show="user.disableableCredentialTypes && user.disableableCredentialTypes.length > 0">
|
<fieldset class="border-top" data-ng-show="user.disableableCredentialTypes && user.disableableCredentialTypes.length > 0">
|
||||||
<legend><span class="text">{{:: 'disable-credentials' | translate}}</span></legend>
|
<legend><span class="text">{{:: 'disable-credentials' | translate}}</span></legend>
|
||||||
<div class="form-group clearfix">
|
<div class="form-group clearfix">
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||||
|
<h1>{{:: 'authentication' | translate}}</h1>
|
||||||
|
|
||||||
|
<kc-tabs-authentication></kc-tabs-authentication>
|
||||||
|
|
||||||
|
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name" class="col-md-2 control-label"><span class="required">*</span> {{:: 'webauthn-rp-entity-name' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<input id="name" type="text" ng-model="realm.webAuthnPolicyRpEntityName" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-rp-entity-name.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sigalg" class="col-md-2 control-label">{{:: 'webauthn-signature-algorithms' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<select id="sigalg" ng-model="realm.webAuthnPolicySignatureAlgorithms" class="form-control" multiple>
|
||||||
|
<option value="ES256">ES256</option>
|
||||||
|
<option value="ES384">ES384</option>
|
||||||
|
<option value="ES512">ES512</option>
|
||||||
|
<option value="RS256">RS256</option>
|
||||||
|
<option value="RS384">RS384</option>
|
||||||
|
<option value="RS512">RS512</option>
|
||||||
|
<option value="RS1">RS1</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-signature-algorithms.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="rpid" class="col-md-2 control-label">{{:: 'webauthn-rp-id' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<input id="rpid" type="text" ng-model="realm.webAuthnPolicyRpId" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-rp-id.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="attpref" class="col-md-2 control-label">{{:: 'webauthn-attestation-conveyance-preference' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<select id="attpref" ng-model="realm.webAuthnPolicyAttestationConveyancePreference" class="form-control">
|
||||||
|
<option value="not specified"></option>
|
||||||
|
<option value="none">none</option>
|
||||||
|
<!-- not yet supported <option value="indirect">indirect</option> -->
|
||||||
|
<!-- not yet supported <option value="direct">direct</option> -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-attestation-conveyance-preference.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="authnatt" class="col-md-2 control-label">{{:: 'webauthn-authenticator-attachment' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<select id="authnatt" ng-model="realm.webAuthnPolicyAuthenticatorAttachment" class="form-control">
|
||||||
|
<option value="not specified"></option>
|
||||||
|
<option value="platform">platform</option>
|
||||||
|
<option value="cross-platform">cross-platform</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-authenticator-attachment.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reqresident" class="col-md-2 control-label">{{:: 'webauthn-require-resident-key' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<select id="reqresident" ng-model="realm.webAuthnPolicyRequireResidentKey" class="form-control">
|
||||||
|
<option value="not specified"></option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
<option value="No">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-require-resident-key.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="usrverify" class="col-md-2 control-label">{{:: 'webauthn-user-verification-requirement' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<select id="usrverify" ng-model="realm.webAuthnPolicyUserVerificationRequirement" class="form-control">
|
||||||
|
<option value="not specified"></option>
|
||||||
|
<option value="required">required</option>
|
||||||
|
<option value="preferred">preferred</option>
|
||||||
|
<option value="discouraged">discouraged</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-user-verification-requirement.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="timeout" class="col-md-2 control-label">{{:: 'webauthn-create-timeout' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<input id="timeout" type="number" min="0" max="31536" ng-model="realm.webAuthnPolicyCreateTimeout" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-create-timeout.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="avoidsame" class="col-md-2 control-label">{{:: 'webauthn-avoid-same-authenticator-register' | translate}}</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<input id="avoidsame" ng-model="realm.webAuthnPolicyAvoidSameAuthenticatorRegister" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-avoid-same-authenticator-register.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="type" class="col-md-2 control-label">{{:: 'webauthn-acceptable-aaguids' | translate}}</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="input-group" ng-repeat="(i, acceptableAaguid) in realm.webAuthnPolicyAcceptableAaguids track by $index">
|
||||||
|
<input class="form-control" ng-model="realm.webAuthnPolicyAcceptableAaguids[i]">
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" data-ng-click="deleteAcceptableAaguid($index)">
|
||||||
|
<span class="fa fa-minus"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class = "input-group">
|
||||||
|
<input class="form-control" ng-model="newAcceptableAaguid" id="newAcceptableAaguid">
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" data-ng-click="newAcceptableAaguid.length > 0 && addAcceptableAaguid()">
|
||||||
|
<span class="fa fa-plus"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'webauthn-acceptable-aaguids.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" data-ng-show="access.manageRealm">
|
||||||
|
<div class="col-md-10 col-md-offset-2">
|
||||||
|
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||||
|
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<kc-menu></kc-menu>
|
|
@ -4,4 +4,5 @@
|
||||||
<li ng-class="{active: path[3] == 'required-actions'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/required-actions">{{:: 'required-actions' | translate}}</a></li>
|
<li ng-class="{active: path[3] == 'required-actions'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/required-actions">{{:: 'required-actions' | translate}}</a></li>
|
||||||
<li ng-class="{active: path[3] == 'password-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/password-policy">{{:: 'password-policy' | translate}}</a></li>
|
<li ng-class="{active: path[3] == 'password-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/password-policy">{{:: 'password-policy' | translate}}</a></li>
|
||||||
<li ng-class="{active: path[3] == 'otp-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/otp-policy">{{:: 'otp-policy' | translate}}</a></li>
|
<li ng-class="{active: path[3] == 'otp-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/otp-policy">{{:: 'otp-policy' | translate}}</a></li>
|
||||||
|
<li ng-class="{active: path[3] == 'webauthn-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/webauthn-policy">{{:: 'webauthn-policy' | translate}}</a></li>
|
||||||
</ul>
|
</ul>
|
|
@ -0,0 +1,114 @@
|
||||||
|
// for embedded scripts, quoted and modified from https://github.com/swansontec/rfc4648.js by William Swanson
|
||||||
|
'use strict';
|
||||||
|
var base64url = base64url || {};
|
||||||
|
(function(base64url) {
|
||||||
|
|
||||||
|
function parse (string, encoding, opts = {}) {
|
||||||
|
// Build the character lookup table:
|
||||||
|
if (!encoding.codes) {
|
||||||
|
encoding.codes = {};
|
||||||
|
for (let i = 0; i < encoding.chars.length; ++i) {
|
||||||
|
encoding.codes[encoding.chars[i]] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The string must have a whole number of bytes:
|
||||||
|
if (!opts.loose && (string.length * encoding.bits) & 7) {
|
||||||
|
throw new SyntaxError('Invalid padding');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count the padding bytes:
|
||||||
|
let end = string.length;
|
||||||
|
while (string[end - 1] === '=') {
|
||||||
|
--end;
|
||||||
|
|
||||||
|
// If we get a whole number of bytes, there is too much padding:
|
||||||
|
if (!opts.loose && !(((string.length - end) * encoding.bits) & 7)) {
|
||||||
|
throw new SyntaxError('Invalid padding');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate the output:
|
||||||
|
const out = new (opts.out || Uint8Array)(((end * encoding.bits) / 8) | 0);
|
||||||
|
|
||||||
|
// Parse the data:
|
||||||
|
let bits = 0; // Number of bits currently in the buffer
|
||||||
|
let buffer = 0; // Bits waiting to be written out, MSB first
|
||||||
|
let written = 0; // Next byte to write
|
||||||
|
for (let i = 0; i < end; ++i) {
|
||||||
|
// Read one character from the string:
|
||||||
|
const value = encoding.codes[string[i]];
|
||||||
|
if (value === void 0) {
|
||||||
|
throw new SyntaxError('Invalid character ' + string[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the bits to the buffer:
|
||||||
|
buffer = (buffer << encoding.bits) | value;
|
||||||
|
bits += encoding.bits;
|
||||||
|
|
||||||
|
// Write out some bits if the buffer has a byte's worth:
|
||||||
|
if (bits >= 8) {
|
||||||
|
bits -= 8;
|
||||||
|
out[written++] = 0xff & (buffer >> bits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that we have received just enough bits:
|
||||||
|
if (bits >= encoding.bits || 0xff & (buffer << (8 - bits))) {
|
||||||
|
throw new SyntaxError('Unexpected end of data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringify (data, encoding, opts = {}) {
|
||||||
|
const { pad = true } = opts;
|
||||||
|
const mask = (1 << encoding.bits) - 1;
|
||||||
|
let out = '';
|
||||||
|
|
||||||
|
let bits = 0; // Number of bits currently in the buffer
|
||||||
|
let buffer = 0; // Bits waiting to be written out, MSB first
|
||||||
|
for (let i = 0; i < data.length; ++i) {
|
||||||
|
// Slurp data into the buffer:
|
||||||
|
buffer = (buffer << 8) | (0xff & data[i]);
|
||||||
|
bits += 8;
|
||||||
|
|
||||||
|
// Write out as much as we can:
|
||||||
|
while (bits > encoding.bits) {
|
||||||
|
bits -= encoding.bits;
|
||||||
|
out += encoding.chars[mask & (buffer >> bits)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partial character:
|
||||||
|
if (bits) {
|
||||||
|
out += encoding.chars[mask & (buffer << (encoding.bits - bits))];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding characters until we hit a byte boundary:
|
||||||
|
if (pad) {
|
||||||
|
while ((out.length * encoding.bits) & 7) {
|
||||||
|
out += '=';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoding = {
|
||||||
|
chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
|
||||||
|
bits: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
base64url.decode = function (string, opts) {
|
||||||
|
return parse(string, encoding, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
base64url.encode = function (data, opts) {
|
||||||
|
return stringify(data, encoding, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64url;
|
||||||
|
}(base64url));
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<@layout.registrationLayout; section>
|
||||||
|
<#if section = "title">
|
||||||
|
title
|
||||||
|
<#elseif section = "header">
|
||||||
|
${msg("loginTitleHtml", realm.name)}
|
||||||
|
<#elseif section = "form">
|
||||||
|
|
||||||
|
<form id="webauth" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||||
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
|
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
|
||||||
|
<input type="hidden" id="authenticatorData" name="authenticatorData"/>
|
||||||
|
<input type="hidden" id="signature" name="signature"/>
|
||||||
|
<input type="hidden" id="credentialId" name="credentialId"/>
|
||||||
|
<input type="hidden" id="userHandle" name="userHandle"/>
|
||||||
|
<input type="hidden" id="error" name="error"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<#if authenticators??>
|
||||||
|
<form id="authn_select">
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Use</th>
|
||||||
|
<th>Authenticator Label</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<#list authenticators.authenticators as authenticator>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="authn_use_chk" value="${authenticator.credentialId}" checked/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${authenticator.label}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</#list>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<input type="button" value="Authenticate" onclick="checkAllowCredentials();">
|
||||||
|
</form>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="${url.resourcesPath}/node_modules/jquery/dist/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
window.onload = function doAuhenticateAutomatically() {
|
||||||
|
let isUserIdentified = ${isUserIdentified};
|
||||||
|
if (!isUserIdentified) doAuthenticate([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAllowCredentials() {
|
||||||
|
let allowCredentials = [];
|
||||||
|
let authn_use = document.forms['authn_select'].authn_use_chk;
|
||||||
|
if (authn_use !== undefined && authn_use.length === undefined && authn_use.checked) {
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.decode(authn_use.value, { loose: true }),
|
||||||
|
type: 'public-key',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
doAuthenticate(allowCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAuthenticate(allowCredentials) {
|
||||||
|
let challenge = "${challenge}";
|
||||||
|
let userVerification = "${userVerification}";
|
||||||
|
let rpId = "${rpId}";
|
||||||
|
let publicKey = {
|
||||||
|
rpId : rpId,
|
||||||
|
challenge: base64url.decode(challenge, { loose: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allowCredentials.length) {
|
||||||
|
publicKey.allowCredentials = allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userVerification !== 'not specified') publicKey.userVerification = userVerification;
|
||||||
|
|
||||||
|
navigator.credentials.get({publicKey})
|
||||||
|
.then(function(result) {
|
||||||
|
window.result = result;
|
||||||
|
|
||||||
|
let clientDataJSON = result.response.clientDataJSON;
|
||||||
|
let authenticatorData = result.response.authenticatorData;
|
||||||
|
let signature = result.response.signature;
|
||||||
|
|
||||||
|
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), { pad: false }));
|
||||||
|
$("#authenticatorData").val(base64url.encode(new Uint8Array(authenticatorData), { pad: false }));
|
||||||
|
$("#signature").val(base64url.encode(new Uint8Array(signature), { pad: false }));
|
||||||
|
$("#credentialId").val(result.id);
|
||||||
|
if(result.response.userHandle) {
|
||||||
|
$("#userHandle").val(base64url.encode(new Uint8Array(result.response.userHandle), { pad: false }));
|
||||||
|
}
|
||||||
|
$("#webauth").submit();
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
$("#error").val(err);
|
||||||
|
$("#webauth").submit();
|
||||||
|
})
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<#elseif section = "info">
|
||||||
|
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
136
themes/src/main/resources/theme/base/login/webauthn-register.ftl
Normal file
136
themes/src/main/resources/theme/base/login/webauthn-register.ftl
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<@layout.registrationLayout; section>
|
||||||
|
<#if section = "title">
|
||||||
|
title
|
||||||
|
<#elseif section = "header">
|
||||||
|
${msg("loginTitleHtml", realm.name)}
|
||||||
|
<#elseif section = "form">
|
||||||
|
<form id="register" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||||
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
|
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
|
||||||
|
<input type="hidden" id="attestationObject" name="attestationObject"/>
|
||||||
|
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
|
||||||
|
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
|
||||||
|
<input type="hidden" id="error" name="error"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<script type="text/javascript" src="${url.resourcesPath}/node_modules/jquery/dist/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// mandatory parameters
|
||||||
|
let challenge = "${challenge}";
|
||||||
|
let userid = "${userid}";
|
||||||
|
let username = "${username}";
|
||||||
|
|
||||||
|
let signatureAlgorithms = "${signatureAlgorithms}";
|
||||||
|
let pubKeyCredParams = getPubKeyCredParams(signatureAlgorithms);
|
||||||
|
|
||||||
|
let rpEntityName = "${rpEntityName}";
|
||||||
|
let rp = {name: rpEntityName};
|
||||||
|
|
||||||
|
let publicKey = {
|
||||||
|
challenge: base64url.decode(challenge, { loose: true }),
|
||||||
|
rp: rp,
|
||||||
|
user: {
|
||||||
|
id: base64url.decode(userid, { loose: true }),
|
||||||
|
name: username,
|
||||||
|
displayName: username
|
||||||
|
},
|
||||||
|
pubKeyCredParams: pubKeyCredParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional parameters
|
||||||
|
let rpId = "${rpId}";
|
||||||
|
publicKey.rp.id = rpId;
|
||||||
|
|
||||||
|
let attestationConveyancePreference = "${attestationConveyancePreference}";
|
||||||
|
if(attestationConveyancePreference !== 'not specified') publicKey.attestation = attestationConveyancePreference;
|
||||||
|
|
||||||
|
let authenticatorSelection = {};
|
||||||
|
let isAuthenticatorSelectionSpecified = false;
|
||||||
|
|
||||||
|
let authenticatorAttachment = "${authenticatorAttachment}";
|
||||||
|
if(authenticatorAttachment !== 'not specified') {
|
||||||
|
authenticatorSelection.authenticatorAttachment = authenticatorAttachment;
|
||||||
|
isAuthenticatorSelectionSpecified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requireResidentKey = "${requireResidentKey}";
|
||||||
|
if(requireResidentKey !== 'not specified') {
|
||||||
|
if(requireResidentKey === 'Yes')
|
||||||
|
authenticatorSelection.requireResidentKey = true;
|
||||||
|
else
|
||||||
|
authenticatorSelection.requireResidentKey = false;
|
||||||
|
isAuthenticatorSelectionSpecified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userVerificationRequirement = "${userVerificationRequirement}";
|
||||||
|
if(userVerificationRequirement !== 'not specified') {
|
||||||
|
authenticatorSelection.userVerification = userVerificationRequirement;
|
||||||
|
isAuthenticatorSelectionSpecified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isAuthenticatorSelectionSpecified) publicKey.authenticatorSelection = authenticatorSelection;
|
||||||
|
|
||||||
|
let createTimeout = ${createTimeout};
|
||||||
|
if(createTimeout != 0) publicKey.timeout = createTimeout * 1000;
|
||||||
|
|
||||||
|
let excludeCredentialIds = "${excludeCredentialIds}";
|
||||||
|
let excludeCredentials = getExcludeCredentials(excludeCredentialIds);
|
||||||
|
if (excludeCredentials.length > 0) publicKey.excludeCredentials = excludeCredentials;
|
||||||
|
|
||||||
|
navigator.credentials.create({publicKey})
|
||||||
|
.then(function(result) {
|
||||||
|
window.result = result;
|
||||||
|
let clientDataJSON = result.response.clientDataJSON;
|
||||||
|
let attestationObject = result.response.attestationObject;
|
||||||
|
let publicKeyCredentialId = result.rawId;
|
||||||
|
|
||||||
|
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), { pad: false }));
|
||||||
|
$("#attestationObject").val(base64url.encode(new Uint8Array(attestationObject), { pad: false }));
|
||||||
|
$("#publicKeyCredentialId").val(base64url.encode(new Uint8Array(publicKeyCredentialId), { pad: false }));
|
||||||
|
|
||||||
|
let initLabel = "WebAuthn Authenticator (Default Label)";
|
||||||
|
let labelResult = window.prompt("Please input your registered authenticator's label", initLabel);
|
||||||
|
if (labelResult === null) labelResult = initLabel;
|
||||||
|
$("#authenticatorLabel").val(labelResult);
|
||||||
|
|
||||||
|
$("#register").submit();
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
$("#error").val(err);
|
||||||
|
$("#register").submit();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function getPubKeyCredParams(signatureAlgorithms) {
|
||||||
|
let pubKeyCredParams = [];
|
||||||
|
if(signatureAlgorithms === "") {
|
||||||
|
pubKeyCredParams.push({type: "public-key", alg: -7});
|
||||||
|
return pubKeyCredParams;
|
||||||
|
}
|
||||||
|
let signatureAlgorithmsList = signatureAlgorithms.split(',');
|
||||||
|
|
||||||
|
for (let i = 0; i < signatureAlgorithmsList.length; i++) {
|
||||||
|
pubKeyCredParams.push({type: "public-key", alg: signatureAlgorithmsList[i]});
|
||||||
|
}
|
||||||
|
return pubKeyCredParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExcludeCredentials(excludeCredentialIds) {
|
||||||
|
let excludeCredentials = [];
|
||||||
|
if (excludeCredentialIds === "") return excludeCredentials;
|
||||||
|
|
||||||
|
let excludeCredentialIdsList = excludeCredentialIds.split(',');
|
||||||
|
|
||||||
|
for (let i = 0; i < excludeCredentialIdsList.length; i++) {
|
||||||
|
excludeCredentials.push({type: "public-key", id: base64url.decode(excludeCredentialIdsList[i], { loose: true })});
|
||||||
|
}
|
||||||
|
return excludeCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
Loading…
Reference in a new issue