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:
Takashi Norimatsu 2019-10-01 22:17:38 +09:00 committed by Marek Posolda
parent 3b58692d7c
commit 7c75546eac
59 changed files with 5727 additions and 4 deletions

View file

@ -106,6 +106,16 @@ public class RealmRepresentation {
protected Integer otpPolicyLookAheadWindow;
protected Integer otpPolicyPeriod;
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> federatedUsers;
@ -916,6 +926,86 @@ public class RealmRepresentation {
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() {
return browserFlow;
}

View file

@ -798,6 +798,36 @@
<groupId>com.openshift</groupId>
<artifactId>openshift-restclient-java</artifactId>
</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>
<build>

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -540,6 +540,39 @@
</license>
</licenses>
</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>
<others>
<other>
@ -780,5 +813,17 @@
</license>
</licenses>
</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>
</licenseSummary>

View file

@ -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"
}
}

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -540,6 +540,39 @@
</license>
</licenses>
</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>
<others>
<other>
@ -780,5 +813,17 @@
</license>
</licenses>
</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>
</licenseSummary>

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -72,5 +72,8 @@
<module name="org.twitter4j"/>
<module name="javax.transaction.api"/>
<module name="sun.jdk"/>
<module name="com.webauthn4j.webauthn4j-core"/>
<module name="com.webauthn4j.webauthn4j-util"/>
<module name="javax.persistence.api"/>
</dependencies>
</module>

View file

@ -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
public RoleModel getRoleById(String id) {
if (isUpdated()) return updated.getRoleById(id);

View file

@ -33,6 +33,7 @@ import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.WebAuthnPolicy;
import java.util.ArrayList;
import java.util.Collections;
@ -95,6 +96,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected int notBefore;
protected PasswordPolicy passwordPolicy;
protected OTPPolicy otpPolicy;
protected WebAuthnPolicy webAuthnPolicy;
protected String loginTheme;
protected String accountTheme;
@ -203,6 +205,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
notBefore = model.getNotBefore();
passwordPolicy = model.getPasswordPolicy();
otpPolicy = model.getOTPPolicy();
webAuthnPolicy = model.getWebAuthnPolicy();
loginTheme = model.getLoginTheme();
accountTheme = model.getAccountTheme();
@ -598,6 +601,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return otpPolicy;
}
public WebAuthnPolicy getWebAuthnPolicy() {
return webAuthnPolicy;
}
public AuthenticationFlowModel getBrowserFlow() {
return browserFlow;
}

View file

@ -919,6 +919,102 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
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
public boolean equals(Object o) {

View file

@ -34,4 +34,17 @@ public interface RealmAttributes {
String OFFLINE_SESSION_MAX_LIFESPAN_ENABLED = "offlineSessionMaxLifespanEnabled";
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
View file

@ -162,6 +162,9 @@
<spring-boot15.version>1.5.20.RELEASE</spring-boot15.version>
<spring-boot21.version>2.1.3.RELEASE</spring-boot21.version>
<!-- webauthn support -->
<webauthn4j.version>0.9.7.RELEASE</webauthn4j.version>
</properties>
<url>http://keycloak.org</url>
@ -1413,6 +1416,22 @@
<version>${project.version}</version>
<type>zip</type>
</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>
</dependencyManagement>

View file

@ -18,6 +18,7 @@
package org.keycloak.models;
import org.keycloak.OAuth2Constants;
import org.keycloak.crypto.Algorithm;
import java.util.Arrays;
import java.util.Collection;
@ -55,6 +56,11 @@ public final class Constants {
// 60 days
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_CODE = "VERIFY_EMAIL_CODE";
public static final String EXECUTION = "execution";

View file

@ -343,6 +343,17 @@ public class ModelToRepresentation {
rep.setOtpPolicyType(otpPolicy.getType());
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
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.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());

View file

@ -19,6 +19,7 @@ package org.keycloak.models.utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@ -84,6 +85,7 @@ import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.policy.PasswordPolicyNotMetException;
@ -261,6 +263,55 @@ public class RepresentationToModel {
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
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);
if (rep.getRequiredActions() != null) {
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
@ -961,6 +1012,55 @@ public class RepresentationToModel {
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) {
Map<String, String> config = new HashMap(rep.getSmtpServer());
if (rep.getSmtpServer().containsKey("password") && ComponentRepresentation.SECRET_VALUE.equals(rep.getSmtpServer().get("password"))) {

View file

@ -240,6 +240,9 @@ public interface RealmModel extends RoleContainerModel {
OTPPolicy getOTPPolicy();
void setOTPPolicy(OTPPolicy policy);
WebAuthnPolicy getWebAuthnPolicy();
void setWebAuthnPolicy(WebAuthnPolicy policy);
RoleModel getRoleById(String id);
List<GroupModel> getDefaultGroups();

View 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;
}
}

View file

@ -186,6 +186,10 @@
<groupId>com.openshift</groupId>
<artifactId>openshift-restclient-java</artifactId>
</dependency>
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

View 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 = "";
}

View file

@ -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
}
}
}

View file

@ -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;
}
}

View file

@ -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
}
}
}

View 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.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";
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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";
}
}

View file

@ -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;
}
}
}

View file

@ -42,4 +42,5 @@ org.keycloak.protocol.docker.DockerAuthenticatorFactory
org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory

View file

@ -19,4 +19,5 @@ org.keycloak.authentication.requiredactions.UpdatePassword
org.keycloak.authentication.requiredactions.UpdateProfile
org.keycloak.authentication.requiredactions.UpdateTotp
org.keycloak.authentication.requiredactions.VerifyEmail
org.keycloak.authentication.requiredactions.TermsAndConditions
org.keycloak.authentication.requiredactions.TermsAndConditions
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory

View file

@ -1,2 +1,3 @@
org.keycloak.credential.OTPCredentialProviderFactory
org.keycloak.credential.PasswordCredentialProviderFactory
org.keycloak.credential.PasswordCredentialProviderFactory
org.keycloak.credential.WebAuthnCredentialProviderFactory

View file

@ -21,6 +21,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.services.DefaultKeycloakSession;
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
public RoleModel getRoleById(String id) {
return null;

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -194,6 +194,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
"Testsuite Username authenticator. Username parameter sets username");
addProviderInfo(result, "webauthn-authenticator", "WebAuthn Authenticator", "Authenticator for WebAuthn");
return result;
}

View file

@ -79,7 +79,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
// Just Dummy RequiredAction is not registered in the realm
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
Assert.assertEquals(1, result.size());
Assert.assertEquals(2, result.size());
RequiredActionProviderSimpleRepresentation action = result.get(0);
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
Assert.assertEquals("Dummy Action", action.getName());

View file

@ -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);
}
}

View file

@ -56,6 +56,37 @@
</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 id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
<div class="">

View file

@ -1051,6 +1051,31 @@ table-of-password-policies=Table of Password Policies
add-policy.placeholder=Add policy...
policy-type=Policy Type
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.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

View file

@ -2008,6 +2008,18 @@ module.config([ '$routeProvider', function($routeProvider) {
},
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', {
templateUrl : resourceUrl + '/partials/authenticator-config.html',
resolve : {

View file

@ -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");
});
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) {
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");

View file

@ -7,6 +7,7 @@
<kc-tabs-user></kc-tabs-user>
<form class="form-horizontal" name="userForm" novalidate>
<fieldset class="border-top">
<legend><span class="text">{{:: 'manage-user-password' | translate}}</span></legend>
<div class="form-group">
@ -38,6 +39,34 @@
</div>
</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">
<legend><span class="text">{{:: 'disable-credentials' | translate}}</span></legend>
<div class="form-group clearfix">

View file

@ -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>

View file

@ -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] == '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] == 'webauthn-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/webauthn-policy">{{:: 'webauthn-policy' | translate}}</a></li>
</ul>

View file

@ -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));

View file

@ -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>

View 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>