├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── pom.xml
├── webauthn4j-ear
├── pom.xml
└── src
│ └── main
│ └── application
│ └── META-INF
│ └── jboss-deployment-structure.xml
└── webauthn4j-ejb
├── pom.xml
└── src
├── main
├── java
│ └── org
│ │ └── keycloak
│ │ ├── WebAuthnConstants.java
│ │ ├── authentication
│ │ ├── authenticators
│ │ │ └── browser
│ │ │ │ ├── WebAuthn4jAuthenticator.java
│ │ │ │ └── WebAuthn4jAuthenticatorFactory.java
│ │ └── requiredactions
│ │ │ ├── RegisterAuthenticator.java
│ │ │ └── RegisterAuthenticatorFactory.java
│ │ ├── credential
│ │ ├── WebAuthnCredentialModel.java
│ │ ├── WebAuthnCredentialProvider.java
│ │ └── WebAuthnCredentialProviderFactory.java
│ │ ├── forms
│ │ └── login
│ │ │ └── freemarker
│ │ │ └── model
│ │ │ └── WebAuthnAuthenticatorsBean.java
│ │ └── models
│ │ └── jpa
│ │ └── converter
│ │ ├── AAGUIDConverter.java
│ │ ├── AttestationStatementConverter.java
│ │ └── CredentialPublicKeyConverter.java
└── resources
│ ├── META-INF
│ ├── keycloak-themes.json
│ └── services
│ │ ├── org.keycloak.authentication.AuthenticatorFactory
│ │ ├── org.keycloak.authentication.RequiredActionFactory
│ │ └── org.keycloak.credential.CredentialProviderFactory
│ ├── theme-resources
│ ├── resources
│ │ └── base64url.js
│ └── templates
│ │ ├── webauthn-register.ftl
│ │ └── webauthn.ftl
│ └── theme
│ └── webauthn
│ └── account
│ ├── account.ftl
│ └── theme.properties
└── test
└── java
└── org
└── keycloak
├── authentication
├── authenticators
│ └── browser
│ │ ├── WebAuthn4jAuthenticatorFactoryTest.java
│ │ └── WebAuthn4jAuthenticatorTest.java
└── requiredactions
│ ├── RegisterAuthenticatorFactoryTest.java
│ └── RegisterAuthenticatorTest.java
├── credential
├── WebAuthnCredentialProviderFactoryTest.java
└── WebAuthnCredentialProviderTest.java
└── models
└── jpa
└── converter
├── AAGUIDConverterTest.java
├── AttestationStatementConverterTest.java
└── CredentialPublicKeyConverterTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # Intellij
2 | ###################
3 | .idea
4 | *.iml
5 |
6 | # Eclipse #
7 | ###########
8 | .project
9 | .settings
10 | .classpath
11 | .factorypath
12 |
13 |
14 | # NetBeans #
15 | ############
16 | nbactions.xml
17 | nb-configuration.xml
18 | catalog.xml
19 | nbproject
20 |
21 | # Compiled source #
22 | ###################
23 | *.com
24 | *.class
25 | *.dll
26 | *.exe
27 | *.o
28 | *.so
29 |
30 | # Packages #
31 | ############
32 | *.7z
33 | *.dmg
34 | *.gz
35 | *.iso
36 | *.jar
37 | *.rar
38 | *.tar
39 | *.zip
40 |
41 | # Logs and databases #
42 | ######################
43 | *.log
44 |
45 | # Maven #
46 | #########
47 | target
48 | target/
49 |
50 | # Maven shade
51 | #############
52 | *dependency-reduced-pom.xml
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | jdk:
4 | - openjdk8
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## tag: 0.3.RELEASE
4 |
5 | - 10 July 2019 released
6 |
7 | - Bug Fix
8 |
9 | - [Issue#26 Fail to authenticate using Resident Key capable authenticator in Resident Key supported Authenticator Scenario](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/26)
10 |
11 | - Remove RegisterAuthenticator implemented as Authenticator considering [Issue#17](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/17).
12 |
13 | ## tag: 0.2-SNAPSHOT
14 |
15 | - 13 May 2019 released
16 |
17 | - Bug Fix
18 |
19 | - [Issue#10 IndexOutOfBoundsException on authentication if a user without registering 2nd factor authenticator's credential](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/10)
20 |
21 | - Enhancement
22 |
23 | - [Issue#13 Add Unit Tests](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/13)
24 |
25 | - [Issue#15 WebAuthn4j 0.9.4.RELEASE support](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/13)
26 |
27 | - [Issue#16 Don't require clicking Authenticate button on Authentication](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/16)
28 |
29 | - [Issue#17 Use a required action for registration](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/17)
30 |
31 | ## tag: 0.1-SNAPSHOT
32 |
33 | - 15 April 2019 released
34 |
35 | - Initial Release
36 |
37 | - Enhancement
38 |
39 | - [Issue#8 WebAuthn4j 0.9.2.RELEASE support](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/8)
40 |
41 | - [Issue#7 credential storage : avoid creating a new table for credentials](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/7)
42 |
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Keycloak WebAuthn Authenticator
2 |
3 | [](https://travis-ci.org/webauthn4j/keycloak-webauthn-authenticator)
4 | [](https://github.com/webauthn4j/keycloak-webauthn-authenticator/blob/master/LICENSE)
5 |
6 | [Web Authentication](https://www.w3.org/TR/webauthn/)(WebAuthn) sample plugin for [Keycloak](https://www.keycloak.org) , implements with [webauthn4j](https://github.com/webauthn4j/webauthn4j).
7 |
8 | The webauthn's support based on this repository has been merged into keycloak master and released in [keycloak-8.0.0](https://www.keycloak.org/2019/11/keycloak-800-released.html). Therefore, this project will not be maintained basically and can not work on keycloak-8.0.0 and later version.
9 |
10 | This sample plugin is developed in order to implement features defined in [the design document for WebAuthn support onto keycloak](https://github.com/keycloak/keycloak-community/blob/master/design/web-authn-authenticator.md), clarify issues for realizing these features and give feedback onto this design document.
11 |
12 |
13 | ## Important Notice
14 |
15 | If you use the previous commits or versions, please first undeploy it, and after that, deploy the ear of the current version or commit.
16 |
17 | - `$ mvn wildfly:undeploy`
18 |
19 | - `$ mvn clean install wildfly:deploy`
20 |
21 | If not undeploy the existing ear, an error occurs. This is because the current version removed RegisterAuthenticator implemented as Authenticator considering [the issue](https://github.com/webauthn4j/keycloak-webauthn-authenticator/issues/17).
22 |
23 | ## Environment
24 |
25 | We've confirmed that this demo had worked well under the following environments:
26 |
27 | - 2 Factor Authentication with Resident Key Not supported Authenticator Scenario
28 |
29 | - OS : Windows 10 (v1903)
30 | - Browser : Google Chrome (ver 73), Mozilla FireFox (ver 66, 68)
31 | - Authenticator : Yubico Security Key NFC (5.1.2), Yubikey 5C Nano
32 | - Server(RP) : keycloak-5.0.0, 6.0.1 on localhost
33 |
34 | - 2 Factor Authentication with Resident Key Not supported Authenticator Scenario
35 |
36 | - OS : macOS Mojave (ver 10.14.3)
37 | - Browser : Google Chrome (ver 73), Mozilla FireFox (ver 66)
38 | - Authenticator : Yubico Security Key NFC (5.1.2)
39 | - Server(RP) : keycloak-5.0.0 on localhost
40 |
41 | - 2 Factor Authentication with Resident Key supported Authenticator Scenario
42 |
43 | - OS : Windows 10
44 | - Browser : Microsoft Edge (ver 44)
45 | - Authenticator : Internal Fingerprint Authentication Device
46 | - Server(RP) : keycloak-5.0.0 on localhost
47 |
48 | - 2 Factor Authentication with Resident Key supported Authenticator Scenario
49 |
50 | - OS : macOS Mojave (ver 10.14.4)
51 | - Browser : Google Chrome (ver 75)
52 | - Authenticator : Touch ID
53 | - Server(RP) : keycloak-6.0.1 on localhost
54 |
55 | - Authentication with Resident Key supported Authenticator Scenario
56 |
57 | - OS : Windows 10
58 | - Browser : Microsoft Edge (ver 44)
59 | - Authenticator : Internal Fingerprint Authentication Device
60 | - Server(RP) : keycloak-5.0.0 on localhost
61 |
62 | - Authentication with Resident Key supported Authenticator Scenario
63 |
64 | - OS : Windows 10
65 | - Browser : Microsoft Edge (ver 44)
66 | - Authenticator : Yubico Security Key NFC (5.1.2)
67 | - Server(RP) : keycloak-5.0.0 on localhost
68 |
69 |
70 | ## Install
71 |
72 | - Build:
73 |
74 | - `$ mvn install`
75 |
76 | - Add the EAR file to the Keycloak Server:
77 |
78 | - `$ cp webauthn4j-ear/target/keycloak-webauthn4j-ear-*.ear $KEYCLOAK_HOME/standalone/deployments/`
79 |
80 | - Or deploy the EAR file dynamically when the Keycloak Server is running:
81 |
82 | - `$ mvn clean install wildfly:deploy`
83 |
84 | - Report coverage
85 |
86 | - `$ mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent test`
87 | - `$ mvn org.jacoco:jacoco-maven-plugin:report`
88 |
89 | ## Overview
90 |
91 | This prototype consists of two components:
92 |
93 | - WebAuthn Register
94 |
95 | This enables users to register their accounts on keycloak with their authenticators' generating public key credentials. It is implemented as `Required Action`.
96 |
97 | - WebAuthn Authenticator
98 |
99 | This enables users to authenticate themselves on keycloak by their authenticators. It is implemented as `Authenticator`.
100 |
101 | ## Realm Settings
102 |
103 | To enable user without their accounts on keycloak to register them on the authentication flow:
104 |
105 | - Enable `User registration` in 'Realm Settings' - 'Login'
106 |
107 | ## Authentication Required Actions Settings
108 |
109 | To enable users to register their accounts with their authenticators' creating public key credentials:
110 |
111 | - register `Webauthn Register` Required Action in 'Required Actons' - 'Register'
112 |
113 | - check `Enabled` and `Default Action` for registered `Webauthn Register` Required Action
114 |
115 |
116 | ## Authentication Flow Settings
117 |
118 | To enable users having their accounts on keycloak to authenticate themselves on keycloak by their authenticators:
119 |
120 | ### Browser Flow (2 Factor Authentication)
121 |
122 | | Auth Type | | Requirement |
123 | | ---------------------------- | ---------------------- | ----------- |
124 | | Cookie | | ALTERNATIVE |
125 | | Kerberos | | DISABLED |
126 | | Identity Provider Redirector | | ALTERNATIVE |
127 | | Copy of Browser Forms | | ALTERNATIVE |
128 | | | Username Password Form | REQUIRED |
129 | | | OTP Form | OPTIONAL |
130 | | | WebAuthn Authenticator | REQUIRED |
131 |
132 | ### Browser Flow (Use `Resident Key`)
133 |
134 | | Auth Type | | Requirement |
135 | | ---------------------------- | --- | ----------- |
136 | | Cookie | | ALTERNATIVE |
137 | | Kerberos | | DISABLED |
138 | | Identity Provider Redirector | | ALTERNATIVE |
139 | | WebAuthn Authenticator | | REQUIRED |
140 |
141 |
142 | ## Authenticator Management
143 |
144 | The user can only register their own authenticator. The user and the administrator can manage the registered authenticator. For the user to do so, the administrator set `Realm Settings -> Themes -> Account Theme` to "webauthn".
145 |
146 | ### User Editable Label for Registered Authenticator
147 |
148 | As the metadata of the authenticator, the user can put the editable label onto their authenticator to identify it when registering it.
149 |
150 | The user and the administrator can edit this label.
151 |
152 | If the user wants to edit this label, please access to [User Account Service](https://www.keycloak.org/docs/latest/server_admin/index.html#_account-service) and edit `Public Key Credential Label`.
153 |
154 | If the administrator wants to edit some user's registered authenticator's label, please access to `Users -> (User Name) -> Attributes` and edit `public_key_credential_label`.
155 |
156 |
157 | ### AAGUID for Registered Authenticator
158 |
159 | As the metadata of the authenticator, its AAGUID is stored onto keycloak when registering it.
160 |
161 | The user and the administrator can view this AAGUID.
162 |
163 | If the user wants to view this AAGUID, please access to [User Account Service](https://www.keycloak.org/docs/latest/server_admin/index.html#_account-service) and look up `Public Key Credential AAGUID`.
164 |
165 | If the administrator wants to view some user's registered authenticator's label, please access to `Users -> (User Name) -> Attributes` and look up `public_key_credential_aaguid`.
166 |
167 |
168 | ### Delete Registered Authenticator
169 |
170 | The user and the administrator can delete the registered authenticator.
171 |
172 | If the user wants to delete its own regestered authenticator, please access to [User Account Service](https://www.keycloak.org/docs/latest/server_admin/index.html#_account-service) and clean up `Public Key Credential ID`, `Public Key Credential Label` and `Public Key Credential AAGUID`.
173 |
174 | If the administrator wants to delete some user's registered authenticator, please access to `Users -> (User Name) -> Attributes` and delete `public_key_credential_id`, `public_key_credential_label` and `public_key_credential_aaguid`.
175 |
176 |
177 | ## Re-Register Authenticator
178 |
179 | The user can re-register the authenticator.
180 |
181 | - The administrator goes to `Users -> (User Name) -> Details` and add `WebAuthn Register` as `Required User Actions`.
182 |
183 | - The user logs onto keycoak. After authentication in the login form, keycloak asks them to register ther authenticator.
184 |
185 |
186 | ## Notes
187 |
188 | ### User Registration in Authentication with Resident Key supported Authenticator Scenario
189 |
190 | Browser Flow (Use `Resident Key`) automatically asks users to authenticate on their authenticators. Therefore, the users without their accounts have no chance to register them on this flow.
191 |
192 | For such the users to register their accounts, please use the default Browser Flow. It is helpful to user `Authentication Flow Overrides` on Client Settings. You can set the default Browser Flow for User Account Service (Client ID: account) to let users register their accounts at first.
193 |
194 | ### Requiring Resident Key in Registration
195 |
196 | On registration, the browser asks you if you would like to store ID and its credential on your authenticator(namely, Resident Key). If you push OK button, the browser tells your authenticator to do so explicitly. If not, whether ID and its credential is Resident Key or not depends on authenticators.
197 |
198 | Please note the followings:
199 |
200 |
201 | - In Authentication with Resident Key supported Authenticator Scenario, only user's ID and its credential as Resident Key can be valid. Therefore, if you register ID and its credential that is not as Resident Key and try to authenticate with them, you fail to authenticate.
202 |
203 |
204 | - Not all authenticators are capable of this Resident Key. The Authenticator lack of Resident Key capability fails to register user's ID and its credential when Resident Key is required explicitly.
205 |
206 |
207 | - Not all browsers support this Resident Key. At least, I've confirmed that Microsoft Edge (ver.44) supports Resident Key.
208 |
209 |
210 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.webauthn4j
8 | keycloak-webauthn
9 | 0.3.RELEASE
10 |
11 | WebAuthn for Keycloak
12 | pom
13 |
14 |
15 | webauthn4j-ear
16 | webauthn4j-ejb
17 |
18 |
19 |
20 | 0.9.7.RELEASE
21 | 4.8.3.Final
22 | 1.8
23 | ${java.version}
24 | ${java.version}
25 | UTF-8
26 | UTF-8
27 |
28 |
29 |
30 |
31 | com.webauthn4j
32 | keycloak-webauthn4j-ejb
33 | ${project.version}
34 |
35 |
36 | com.webauthn4j
37 | webauthn4j-core
38 | ${webauthn4j.version}
39 |
40 |
41 | org.keycloak
42 | keycloak-services
43 | ${keycloak.version}
44 | provided
45 |
46 |
47 | org.keycloak
48 | keycloak-server-spi
49 | ${keycloak.version}
50 | provided
51 |
52 |
53 | org.keycloak
54 | keycloak-server-spi-private
55 | ${keycloak.version}
56 | provided
57 |
58 |
59 | org.keycloak
60 | keycloak-core
61 | ${keycloak.version}
62 | provided
63 |
64 |
65 | org.keycloak
66 | keycloak-model-jpa
67 | ${keycloak.version}
68 | provided
69 |
70 |
71 | org.jboss.spec.javax.ws.rs
72 | jboss-jaxrs-api_2.1_spec
73 | 1.0.1.Final
74 | provided
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | org.wildfly.plugins
83 | wildfly-maven-plugin
84 | 1.2.0.Final
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/webauthn4j-ear/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.webauthn4j
8 | keycloak-webauthn
9 | 0.3.RELEASE
10 |
11 | com.webauthn4j
12 | keycloak-webauthn4j-ear
13 | ear
14 |
15 |
16 | com.webauthn4j
17 | keycloak-webauthn4j-ejb
18 | ${project.version}
19 | ejb
20 |
21 |
22 |
23 |
24 |
25 | org.apache.maven.plugins
26 | maven-ear-plugin
27 | 2.10
28 |
29 | 7
30 | lib
31 | no-version
32 |
33 |
34 |
35 | org.wildfly.plugins
36 | wildfly-maven-plugin
37 |
38 | false
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/webauthn4j-ear/src/main/application/META-INF/jboss-deployment-structure.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.webauthn4j
8 | keycloak-webauthn
9 | 0.3.RELEASE
10 |
11 |
12 | com.webauthn4j
13 | keycloak-webauthn4j-ejb
14 | jar
15 |
16 |
17 | 4.12
18 | 1.9.5
19 | 3.8.0
20 | 2.22.1
21 | 0.8.3
22 |
23 |
24 |
25 |
26 |
27 | org.apache.maven.plugins
28 | maven-compiler-plugin
29 | ${maven.compiler.version}
30 |
31 |
32 | org.apache.maven.plugins
33 | maven-surefire-plugin
34 | ${maven.surefire.version}
35 |
36 |
37 | org.jacoco
38 | jacoco-maven-plugin
39 | ${jacoco.version}
40 |
41 |
42 |
43 |
44 |
45 |
46 | org.keycloak
47 | keycloak-core
48 | provided
49 |
50 |
51 | org.keycloak
52 | keycloak-server-spi
53 | provided
54 |
55 |
56 | org.keycloak
57 | keycloak-server-spi-private
58 | provided
59 |
60 |
61 | org.keycloak
62 | keycloak-services
63 | provided
64 |
65 |
66 | org.keycloak
67 | keycloak-model-jpa
68 | provided
69 |
70 |
71 | com.webauthn4j
72 | webauthn4j-core
73 |
74 |
75 | junit
76 | junit
77 | ${junit.version}
78 | test
79 |
80 |
81 | org.mockito
82 | mockito-all
83 | ${mockito.version}
84 | test
85 |
86 |
87 |
88 |
89 | ojo-snapshots
90 | OJO Snapshots
91 | https://oss.jfrog.org/artifactory/libs-snapshot
92 |
93 | true
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/WebAuthnConstants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak;
18 |
19 | public interface WebAuthnConstants {
20 |
21 | // Interface binded by FreeMarker template between UA and RP
22 | final String USER_ID = "userid";
23 | final String USER_NAME = "username";
24 | final String CHALLENGE = "challenge";
25 | final String RPID = "rpId";
26 | final String ORIGIN = "origin";
27 | final String ERROR = "error";
28 | final String PUBLIC_KEY_CREDENTIAL_ID= "publicKeyCredentialId";
29 | final String CREDENTIAL_ID = "credentialId";
30 | final String CLIENT_DATA_JSON = "clientDataJSON";
31 | final String AUTHENTICATOR_DATA = "authenticatorData";
32 | final String SIGNATURE = "signature";
33 | final String USER_HANDLE = "userHandle";
34 | final String ATTESTATION_OBJECT= "attestationObject";
35 | final String AUTHENTICATOR_LABEL = "authenticatorLabel";
36 |
37 | // key for storing onto UserModel's Attribute public key credential id generated by navigator.credentials.create()
38 | final String PUBKEY_CRED_ID_ATTR = "public_key_credential_id";
39 |
40 | // key for storing onto UserModel's Attribute Public Key Credential's user-editable metadata
41 | final String PUBKEY_CRED_LABEL_ATTR = "public_key_credential_label";
42 |
43 | // key for storing onto UserModel's Attribute Public Key Credential's AAGUID
44 | final String PUBKEY_CRED_AAGUID_ATTR = "public_key_credential_aaguid";
45 |
46 | // key for storing onto AuthenticationSessionModel's Attribute challenge generated by RP(keycloak)
47 | final String AUTH_CHALLENGE_NOTE = "WEBAUTH_CHALLENGE";
48 | }
49 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthn4jAuthenticator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.authentication.authenticators.browser;
18 |
19 | import com.webauthn4j.data.WebAuthnAuthenticationContext;
20 | import com.webauthn4j.data.client.Origin;
21 | import com.webauthn4j.data.client.challenge.Challenge;
22 | import com.webauthn4j.data.client.challenge.DefaultChallenge;
23 | import com.webauthn4j.server.ServerProperty;
24 |
25 | import org.jboss.logging.Logger;
26 | import org.keycloak.WebAuthnConstants;
27 | import org.keycloak.authentication.AuthenticationFlowContext;
28 | import org.keycloak.authentication.AuthenticationFlowError;
29 | import org.keycloak.authentication.AuthenticationFlowException;
30 | import org.keycloak.authentication.Authenticator;
31 | import org.keycloak.common.util.Base64Url;
32 | import org.keycloak.common.util.UriUtils;
33 | import org.keycloak.credential.WebAuthnCredentialModel;
34 | import org.keycloak.forms.login.LoginFormsProvider;
35 | import org.keycloak.forms.login.freemarker.model.WebAuthnAuthenticatorsBean;
36 | import org.keycloak.models.KeycloakSession;
37 | import org.keycloak.models.RealmModel;
38 | import org.keycloak.models.UserModel;
39 |
40 | import javax.ws.rs.core.MultivaluedMap;
41 | import java.net.URI;
42 | import java.util.HashMap;
43 | import java.util.Map;
44 |
45 | public class WebAuthn4jAuthenticator implements Authenticator {
46 |
47 | private static final Logger logger = Logger.getLogger(WebAuthn4jAuthenticator.class);
48 |
49 | private KeycloakSession session;
50 |
51 | public WebAuthn4jAuthenticator(KeycloakSession session) {
52 | this.session = session;
53 | }
54 |
55 | private Map generateParameters(RealmModel realm, URI baseUri) {
56 | Map params = new HashMap<>();
57 | Challenge challenge = new DefaultChallenge();
58 | params.put(WebAuthnConstants.CHALLENGE, Base64Url.encode(challenge.getValue()));
59 | params.put(WebAuthnConstants.RPID, baseUri.getHost());
60 | params.put(WebAuthnConstants.ORIGIN, UriUtils.getOrigin(baseUri));
61 | return params;
62 | }
63 |
64 | public void authenticate(AuthenticationFlowContext context) {
65 | LoginFormsProvider form = context.form();
66 | Map params = generateParameters(context.getRealm(), context.getUriInfo().getBaseUri());
67 | context.getAuthenticationSession().setAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE, params.get(WebAuthnConstants.CHALLENGE));
68 | UserModel user = context.getUser();
69 | boolean isUserIdentified = false;
70 | if (user != null) {
71 | // in 2 Factor Scenario where the user has already identified
72 | isUserIdentified = true;
73 | form.setAttribute("authenticators", new WebAuthnAuthenticatorsBean(user));
74 | } else {
75 | // in ID-less & Password-less Scenario
76 | // NOP
77 | }
78 | params.put("isUserIdentified", Boolean.toString(isUserIdentified));
79 | params.forEach(form::setAttribute);
80 | context.challenge(form.createForm("webauthn.ftl"));
81 | }
82 |
83 | public void action(AuthenticationFlowContext context) {
84 | MultivaluedMap params = context.getHttpRequest().getDecodedFormParameters();
85 |
86 | // receive error from navigator.credentials.get()
87 | String error = params.getFirst(WebAuthnConstants.ERROR);
88 | if (error != null && !error.isEmpty()) {
89 | throw new AuthenticationFlowException("exception raised from navigator.credentials.get() : " + error, AuthenticationFlowError.INVALID_USER);
90 | }
91 |
92 | String baseUrl = UriUtils.getOrigin(context.getUriInfo().getBaseUri());
93 | String rpId = context.getUriInfo().getBaseUri().getHost();
94 |
95 | Origin origin = new Origin(baseUrl);
96 | Challenge challenge = new DefaultChallenge(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE));
97 | ServerProperty server = new ServerProperty(origin, rpId, challenge, null);
98 |
99 | byte[] credentialId = Base64Url.decode(params.getFirst(WebAuthnConstants.CREDENTIAL_ID));
100 | byte[] clientDataJSON = Base64Url.decode(params.getFirst(WebAuthnConstants.CLIENT_DATA_JSON));
101 | byte[] authenticatorData = Base64Url.decode(params.getFirst(WebAuthnConstants.AUTHENTICATOR_DATA));
102 | byte[] signature = Base64Url.decode(params.getFirst(WebAuthnConstants.SIGNATURE));
103 |
104 | String userId = params.getFirst(WebAuthnConstants.USER_HANDLE);
105 | boolean isUVFlagChecked = true;
106 | logger.debugv("userId = {0}", userId);
107 |
108 | if (userId == null || userId.isEmpty()) {
109 | // in 2 Factor with Resident Key not supported Authenticator Scenario
110 | userId = context.getUser().getId();
111 | isUVFlagChecked = false;
112 | } else {
113 | if (context.getUser() != null) {
114 | // in 2 Factor with Resident Key supported Authenticator Scenario
115 | String firstAuthenticatedUserId = context.getUser().getId();
116 | logger.debugv("firstAuthenticatedUserId = {0}", firstAuthenticatedUserId);
117 | if (firstAuthenticatedUserId != null && !firstAuthenticatedUserId.equals(userId)) {
118 | throw new AuthenticationFlowException("First authenticated user is not the one authenticated by 2nd factor authenticator", AuthenticationFlowError.USER_CONFLICT);
119 | }
120 | } else {
121 | // in Passwordless with Resident Key supported Authenticator Scenario
122 | // NOP
123 | }
124 | }
125 | UserModel user = session.users().getUserById(userId, context.getRealm());
126 | WebAuthnAuthenticationContext authenticationContext = new WebAuthnAuthenticationContext(
127 | credentialId,
128 | clientDataJSON,
129 | authenticatorData,
130 | signature,
131 | server,
132 | isUVFlagChecked
133 | );
134 |
135 | WebAuthnCredentialModel cred = new WebAuthnCredentialModel();
136 | cred.setAuthenticationContext(authenticationContext);
137 |
138 | boolean result = false;
139 | try {
140 | result = session.userCredentialManager().isValid(context.getRealm(), user, cred);
141 | } catch (Exception e) {
142 | e.printStackTrace();
143 | throw new AuthenticationFlowException("unknown user authenticated by the authenticator", AuthenticationFlowError.UNKNOWN_USER);
144 | }
145 | if (result) {
146 | context.setUser(user);
147 | context.success();
148 | } else {
149 | context.cancelLogin();
150 | }
151 | }
152 |
153 | public boolean requiresUser() {
154 | return false;
155 | }
156 |
157 | public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
158 | return true;
159 | }
160 |
161 | public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
162 | }
163 |
164 | public void close() {
165 |
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthn4jAuthenticatorFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.authentication.authenticators.browser;
18 |
19 | import org.keycloak.Config;
20 | import org.keycloak.authentication.Authenticator;
21 | import org.keycloak.authentication.AuthenticatorFactory;
22 | import org.keycloak.models.AuthenticationExecutionModel;
23 | import org.keycloak.models.KeycloakSession;
24 | import org.keycloak.models.KeycloakSessionFactory;
25 | import org.keycloak.provider.ProviderConfigProperty;
26 |
27 | import java.util.ArrayList;
28 | import java.util.List;
29 |
30 | public class WebAuthn4jAuthenticatorFactory implements AuthenticatorFactory {
31 |
32 | public static final String PROVIDER_ID = "webauthn-authenticator";
33 |
34 | private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
35 | AuthenticationExecutionModel.Requirement.REQUIRED,
36 | AuthenticationExecutionModel.Requirement.ALTERNATIVE,
37 | AuthenticationExecutionModel.Requirement.DISABLED,
38 | };
39 |
40 | @Override
41 | public String getDisplayType() {
42 | return "WebAuthn Authenticator";
43 | }
44 |
45 | @Override
46 | public String getReferenceCategory() {
47 | return "auth";
48 | }
49 |
50 | @Override
51 | public boolean isConfigurable() {
52 | return false;
53 | }
54 |
55 | @Override
56 | public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
57 | return REQUIREMENT_CHOICES;
58 | }
59 |
60 | @Override
61 | public boolean isUserSetupAllowed() {
62 | return true;
63 | }
64 |
65 | @Override
66 | public String getHelpText() {
67 | return "Authenticator for WebAuthn";
68 | }
69 |
70 | @Override
71 | public List getConfigProperties() {
72 | return new ArrayList<>();
73 | }
74 |
75 | @Override
76 | public Authenticator create(KeycloakSession session) {
77 | return new WebAuthn4jAuthenticator(session);
78 | }
79 |
80 | @Override
81 | public void init(Config.Scope config) {
82 |
83 | }
84 |
85 | @Override
86 | public void postInit(KeycloakSessionFactory factory) {
87 |
88 | }
89 |
90 | @Override
91 | public void close() {
92 |
93 | }
94 |
95 | @Override
96 | public String getId() {
97 | return PROVIDER_ID;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/authentication/requiredactions/RegisterAuthenticator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.authentication.requiredactions;
18 |
19 | import java.util.Base64;
20 |
21 | import javax.ws.rs.core.MultivaluedMap;
22 | import javax.ws.rs.core.Response;
23 |
24 | import org.jboss.logging.Logger;
25 | import org.keycloak.WebAuthnConstants;
26 | import org.keycloak.authentication.AuthenticationFlowError;
27 | import org.keycloak.authentication.AuthenticationFlowException;
28 | import org.keycloak.authentication.RequiredActionContext;
29 | import org.keycloak.authentication.RequiredActionProvider;
30 | import org.keycloak.common.util.Base64Url;
31 | import org.keycloak.common.util.UriUtils;
32 | import org.keycloak.credential.WebAuthnCredentialModel;
33 | import org.keycloak.models.KeycloakSession;
34 |
35 | import com.webauthn4j.data.WebAuthnRegistrationContext;
36 | import com.webauthn4j.data.client.Origin;
37 | import com.webauthn4j.data.client.challenge.Challenge;
38 | import com.webauthn4j.data.client.challenge.DefaultChallenge;
39 | import com.webauthn4j.server.ServerProperty;
40 | import com.webauthn4j.validator.WebAuthnRegistrationContextValidationResponse;
41 | import com.webauthn4j.validator.WebAuthnRegistrationContextValidator;
42 |
43 | public class RegisterAuthenticator implements RequiredActionProvider {
44 | private static final Logger logger = Logger.getLogger(RegisterAuthenticator.class);
45 | private KeycloakSession session;
46 |
47 | public RegisterAuthenticator(KeycloakSession session) {
48 | this.session = session;
49 | }
50 |
51 | @Override
52 | public void close() {
53 | // NOP
54 | }
55 |
56 | @Override
57 | public void evaluateTriggers(RequiredActionContext context) {
58 | // NOP
59 | }
60 |
61 | @Override
62 | public void requiredActionChallenge(RequiredActionContext context) {
63 | String userid = context.getUser().getId();
64 | String username = context.getUser().getUsername();
65 | Challenge challenge = new DefaultChallenge();
66 | String challengeValue = Base64Url.encode(challenge.getValue());
67 | String origin = context.getUriInfo().getBaseUri().getHost();
68 | context.getAuthenticationSession().setAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE, challengeValue);
69 |
70 | Response form = context.form()
71 | .setAttribute(WebAuthnConstants.ORIGIN, origin)
72 | .setAttribute(WebAuthnConstants.CHALLENGE, challengeValue)
73 | .setAttribute(WebAuthnConstants.USER_ID, userid)
74 | .setAttribute(WebAuthnConstants.USER_NAME, username)
75 | .createForm("webauthn-register.ftl");
76 | context.challenge(form);
77 | }
78 |
79 | @Override
80 | public void processAction(RequiredActionContext context) {
81 |
82 | MultivaluedMap params = context.getHttpRequest().getDecodedFormParameters();
83 |
84 | // receive error from navigator.credentials.create()
85 | String error = params.getFirst(WebAuthnConstants.ERROR);
86 | if (error != null && !error.isEmpty()) {
87 | throw new AuthenticationFlowException("exception raised from navigator.credentials.create() : " + error, AuthenticationFlowError.INVALID_CREDENTIALS);
88 | }
89 |
90 | String baseUrl = UriUtils.getOrigin(context.getUriInfo().getBaseUri());
91 | String rpId = context.getUriInfo().getBaseUri().getHost();
92 | String label = params.getFirst(WebAuthnConstants.AUTHENTICATOR_LABEL);
93 | byte[] clientDataJSON = Base64.getUrlDecoder().decode(params.getFirst(WebAuthnConstants.CLIENT_DATA_JSON));
94 | byte[] attestationObject = Base64.getUrlDecoder().decode(params.getFirst(WebAuthnConstants.ATTESTATION_OBJECT));
95 | String publicKeyCredentialId = params.getFirst(WebAuthnConstants.PUBLIC_KEY_CREDENTIAL_ID);
96 |
97 | Origin origin = new Origin(baseUrl);
98 | Challenge challenge = new DefaultChallenge(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE));
99 | ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, null);
100 |
101 | try {
102 | WebAuthnRegistrationContext registrationContext = new WebAuthnRegistrationContext(clientDataJSON, attestationObject, serverProperty, false);
103 | WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = WebAuthnRegistrationContextValidator.createNonStrictRegistrationContextValidator();
104 | WebAuthnRegistrationContextValidationResponse response = webAuthnRegistrationContextValidator.validate(registrationContext);
105 |
106 | WebAuthnCredentialModel credential = new WebAuthnCredentialModel();
107 |
108 | credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
109 | credential.setAttestationStatement(response.getAttestationObject().getAttestationStatement());
110 | credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
111 |
112 | this.session.userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credential);
113 |
114 | // store received Credential ID on Registration onto UserModel in order to be used on Authentication
115 | context.getUser().setSingleAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, publicKeyCredentialId);
116 | context.getUser().setSingleAttribute(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, label);
117 | context.getUser().setSingleAttribute(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData().getAaguid().toString());
118 | logger.infov("publicKeyCredentialId = {0}", context.getUser().getAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR));
119 | logger.infov("publicKeyCredentialLabel = {0}", context.getUser().getAttribute(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR));
120 | logger.infov("publicKeyCredentialAAGUID = {0}", context.getUser().getAttribute(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR));
121 |
122 | context.success();
123 | } catch (Exception me) {
124 | me.printStackTrace();
125 | throw new AuthenticationFlowException("failed to update credential.", AuthenticationFlowError.INVALID_CREDENTIALS);
126 | }
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/authentication/requiredactions/RegisterAuthenticatorFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.authentication.requiredactions;
18 |
19 | import org.keycloak.OAuth2Constants;
20 | import org.keycloak.Config.Scope;
21 | import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
22 | import org.keycloak.authentication.RequiredActionFactory;
23 | import org.keycloak.authentication.RequiredActionProvider;
24 | import org.keycloak.models.KeycloakSession;
25 | import org.keycloak.models.KeycloakSessionFactory;
26 |
27 | public class RegisterAuthenticatorFactory implements RequiredActionFactory, DisplayTypeRequiredActionFactory {
28 | public static final String PROVIDER_ID = "webauthn-register";
29 |
30 | @Override
31 | public RequiredActionProvider create(KeycloakSession session) {
32 | return new RegisterAuthenticator(session);
33 | }
34 |
35 | @Override
36 | public void init(Scope config) {
37 | // NOP
38 | }
39 |
40 | @Override
41 | public void postInit(KeycloakSessionFactory factory) {
42 | // NOP
43 | }
44 |
45 | @Override
46 | public void close() {
47 | // NOP
48 | }
49 |
50 | @Override
51 | public String getId() {
52 | return PROVIDER_ID;
53 | }
54 |
55 | @Override
56 | public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
57 | if (displayType == null) return create(session);
58 | if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
59 | // TODO : write console typed provider?
60 | return null;
61 | }
62 |
63 | @Override
64 | public String getDisplayText() {
65 | return "Webauthn Register";
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/credential/WebAuthnCredentialModel.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.credential;
18 |
19 | import com.webauthn4j.data.WebAuthnAuthenticationContext;
20 | import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
21 | import com.webauthn4j.data.attestation.statement.AttestationStatement;
22 |
23 | public class WebAuthnCredentialModel implements CredentialInput {
24 |
25 | public static final String WEBAUTHN_CREDENTIAL_TYPE = "webauthn";
26 | private AttestedCredentialData attestedCredentialData;
27 | private AttestationStatement attestationStatement;
28 | private WebAuthnAuthenticationContext authenticationContext;
29 | private long count;
30 | private String authenticatorId;
31 |
32 | @Override
33 | public String getType() {
34 | return WEBAUTHN_CREDENTIAL_TYPE;
35 | }
36 |
37 | public WebAuthnCredentialModel() {
38 |
39 | }
40 |
41 | public AttestedCredentialData getAttestedCredentialData() {
42 | return attestedCredentialData;
43 | }
44 |
45 | public AttestationStatement getAttestationStatement() {
46 | return attestationStatement;
47 | }
48 |
49 | public long getCount() {
50 | return count;
51 | }
52 |
53 | public WebAuthnAuthenticationContext getAuthenticationContext() {
54 | return authenticationContext;
55 | }
56 |
57 | public void setAuthenticationContext(WebAuthnAuthenticationContext authenticationContext) {
58 | this.authenticationContext = authenticationContext;
59 | }
60 |
61 | public void setAttestedCredentialData(AttestedCredentialData attestedCredentialData) {
62 | this.attestedCredentialData = attestedCredentialData;
63 | }
64 |
65 | public void setAttestationStatement(AttestationStatement attestationStatement) {
66 | this.attestationStatement = attestationStatement;
67 | }
68 |
69 | public void setCount(long count) {
70 | this.count = count;
71 | }
72 |
73 | public String getAuthenticatorId() {
74 | return authenticatorId;
75 | }
76 |
77 | public void setAuthenticatorId(String authenticatorId) {
78 | this.authenticatorId = authenticatorId;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/credential/WebAuthnCredentialProvider.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.credential;
18 |
19 | import java.io.IOException;
20 | import java.util.ArrayList;
21 | import java.util.Arrays;
22 | import java.util.Collections;
23 | import java.util.List;
24 | import java.util.Set;
25 |
26 | import org.jboss.logging.Logger;
27 | import org.keycloak.common.util.Base64;
28 | import org.keycloak.common.util.MultivaluedHashMap;
29 | import org.keycloak.common.util.Time;
30 | import org.keycloak.models.KeycloakSession;
31 | import org.keycloak.models.RealmModel;
32 | import org.keycloak.models.UserModel;
33 | import org.keycloak.models.jpa.converter.AttestationStatementConverter;
34 | import org.keycloak.models.jpa.converter.CredentialPublicKeyConverter;
35 |
36 | import com.webauthn4j.authenticator.Authenticator;
37 | import com.webauthn4j.authenticator.AuthenticatorImpl;
38 | import com.webauthn4j.data.WebAuthnAuthenticationContext;
39 | import com.webauthn4j.data.attestation.authenticator.AAGUID;
40 | import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
41 | import com.webauthn4j.data.attestation.authenticator.CredentialPublicKey;
42 | import com.webauthn4j.data.attestation.statement.AttestationStatement;
43 | import com.webauthn4j.validator.WebAuthnAuthenticationContextValidationResponse;
44 | import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
45 |
46 | public class WebAuthnCredentialProvider implements CredentialProvider, CredentialInputValidator, CredentialInputUpdater {
47 |
48 | private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class);
49 |
50 | private static final String ATTESTATION_STATEMENT = "ATTESTATION_STATEMENT";
51 | private static final String AAGUID = "AAGUID";
52 | private static final String CREDENTIAL_ID = "CREDENTIAL_ID";
53 | private static final String CREDENTIAL_PUBLIC_KEY = "CREDENTIAL_PUBLIC_KEY";
54 |
55 | private KeycloakSession session;
56 |
57 | public WebAuthnCredentialProvider(KeycloakSession session) {
58 | this.session = session;
59 | }
60 |
61 | @Override
62 | public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
63 | if (input == null) return false;
64 | CredentialModel model = createCredentialModel(input);
65 | if (model == null) return false;
66 | session.userCredentialManager().createCredential(realm, user, model);
67 | return true;
68 | }
69 |
70 | private CredentialModel createCredentialModel(CredentialInput input) {
71 | if (!supportsCredentialType(input.getType())) return null;
72 |
73 | WebAuthnCredentialModel webAuthnModel = (WebAuthnCredentialModel) input;
74 | CredentialModel model = new CredentialModel();
75 | model.setType(WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE);
76 | model.setCreatedDate(Time.currentTimeMillis());
77 |
78 | MultivaluedHashMap credential = new MultivaluedHashMap<>();
79 |
80 | AttestationStatementConverter attConv = new AttestationStatementConverter();
81 | credential.add(ATTESTATION_STATEMENT, attConv.convertToDatabaseColumn(webAuthnModel.getAttestationStatement()));
82 |
83 | credential.add(AAGUID, webAuthnModel.getAttestedCredentialData().getAaguid().toString());
84 |
85 | credential.add(CREDENTIAL_ID, Base64.encodeBytes(webAuthnModel.getAttestedCredentialData().getCredentialId()));
86 |
87 | CredentialPublicKeyConverter credConv = new CredentialPublicKeyConverter();
88 | credential.add(CREDENTIAL_PUBLIC_KEY, credConv.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCredentialPublicKey()));
89 |
90 | model.setId(webAuthnModel.getAuthenticatorId());
91 |
92 | model.setConfig(credential);
93 |
94 | // authenticator's counter
95 | model.setValue(String.valueOf(webAuthnModel.getCount()));
96 |
97 | dumpCredentialModel(model);
98 | dumpWebAuthnCredentialModel(webAuthnModel);
99 |
100 | return model;
101 | }
102 |
103 | @Override
104 | public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
105 | if (!supportsCredentialType(credentialType)) return;
106 | for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType)) {
107 | session.userCredentialManager().removeStoredCredential(realm, user, credential.getId());
108 | }
109 | }
110 |
111 | @Override
112 | public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) {
113 | return isConfiguredFor(realm, user, WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE) ? Collections.singleton(WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE) : Collections.emptySet();
114 | }
115 |
116 | @Override
117 | public boolean supportsCredentialType(String credentialType) {
118 | return WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE.equals(credentialType);
119 | }
120 |
121 | @Override
122 | public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
123 | if (!supportsCredentialType(credentialType)) return false;
124 | return !session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
125 | }
126 |
127 | @Override
128 | public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
129 | if (!WebAuthnCredentialModel.class.isInstance(input)) return false;
130 |
131 | WebAuthnCredentialModel context = WebAuthnCredentialModel.class.cast(input);
132 | List auths = getWebAuthnCredentialModelList(realm, user);
133 |
134 | WebAuthnAuthenticationContextValidator webAuthnAuthenticationContextValidator =
135 | new WebAuthnAuthenticationContextValidator();
136 | try {
137 | for (WebAuthnCredentialModel auth : auths) {
138 |
139 | byte[] credentialId = auth.getAttestedCredentialData().getCredentialId();
140 | if (Arrays.equals(credentialId, context.getAuthenticationContext().getCredentialId())) {
141 | Authenticator authenticator = new AuthenticatorImpl(
142 | auth.getAttestedCredentialData(),
143 | auth.getAttestationStatement(),
144 | auth.getCount()
145 | );
146 |
147 | WebAuthnAuthenticationContextValidationResponse response =
148 | webAuthnAuthenticationContextValidator.validate(
149 | context.getAuthenticationContext(),
150 | authenticator);
151 |
152 | // update authenticator counter
153 | long count = auth.getCount();
154 | auth.setCount(count + 1);
155 | CredentialModel cred = createCredentialModel(auth);
156 | session.userCredentialManager().updateCredential(realm, user, cred);
157 |
158 | dumpCredentialModel(cred);
159 | dumpWebAuthnCredentialModel(auth);
160 |
161 | return true;
162 | }
163 | }
164 | } catch (Exception e) {
165 | e.printStackTrace();
166 | }
167 | return false;
168 | }
169 |
170 | private List getWebAuthnCredentialModelList(RealmModel realm, UserModel user) {
171 | List auths = new ArrayList<>();
172 | for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE)) {
173 | WebAuthnCredentialModel auth = new WebAuthnCredentialModel();
174 | MultivaluedHashMap attributes = credential.getConfig();
175 |
176 | AttestationStatementConverter attConv = new AttestationStatementConverter();
177 | AttestationStatement attrStatement = attConv.convertToEntityAttribute(attributes.getFirst(ATTESTATION_STATEMENT));
178 | auth.setAttestationStatement(attrStatement);
179 |
180 | AAGUID aaguid = new AAGUID(attributes.getFirst(AAGUID));
181 |
182 | byte[] credentialId = null;
183 | try {
184 | credentialId = Base64.decode(attributes.getFirst(CREDENTIAL_ID));
185 | } catch (IOException ioe) {
186 | // NOP
187 | }
188 |
189 | CredentialPublicKeyConverter credConv = new CredentialPublicKeyConverter();
190 | CredentialPublicKey pubKey = credConv.convertToEntityAttribute(attributes.getFirst(CREDENTIAL_PUBLIC_KEY));
191 |
192 | AttestedCredentialData attrCredData = new AttestedCredentialData(aaguid, credentialId, pubKey);
193 |
194 | auth.setAttestedCredentialData(attrCredData);
195 |
196 | long count = Long.parseLong(credential.getValue());
197 | auth.setCount(count);
198 |
199 | auth.setAuthenticatorId(credential.getId());
200 |
201 | auths.add(auth);
202 | }
203 | return auths;
204 | }
205 |
206 | private void dumpCredentialModel(CredentialModel credential) {
207 | logger.debugv(" Persisted Credential Info::");
208 | MultivaluedHashMap attributes = credential.getConfig();
209 | logger.debugv(" ATTESTATION_STATEMENT = {0}", attributes.getFirst(ATTESTATION_STATEMENT));
210 | logger.debugv(" AAGUID = {0}", attributes.getFirst(AAGUID));
211 | logger.debugv(" CREDENTIAL_ID = {0}", attributes.getFirst(CREDENTIAL_ID));
212 | logger.debugv(" CREDENTIAL_PUBLIC_KEY = {0}", attributes.getFirst(CREDENTIAL_PUBLIC_KEY));
213 | logger.debugv(" count = {0}", credential.getValue());
214 | logger.debugv(" authenticator_id = {0}", credential.getId());
215 | }
216 |
217 | private void dumpWebAuthnCredentialModel(WebAuthnCredentialModel auth) {
218 | logger.debugv(" Context Credential Info::");
219 | String id = auth.getAuthenticatorId();
220 | AttestationStatement attrStatement = auth.getAttestationStatement();
221 | AttestedCredentialData attrCredData = auth.getAttestedCredentialData();
222 | WebAuthnAuthenticationContext context = auth.getAuthenticationContext();
223 | if (id != null)
224 | logger.debugv(" Authenticator Id = {0}", id);
225 | if (attrStatement != null)
226 | logger.debugv(" Attestation Statement Format = {0}", attrStatement.getFormat());
227 | if (attrCredData != null) {
228 | CredentialPublicKey credPubKey = attrCredData.getCredentialPublicKey();
229 | byte[] keyId = credPubKey.getKeyId();
230 | logger.debugv(" AAGUID = {0}", attrCredData.getAaguid().toString());
231 | logger.debugv(" CREDENTIAL_ID = {0}", Base64.encodeBytes(attrCredData.getCredentialId()));
232 | if (keyId != null)
233 | logger.debugv(" CREDENTIAL_PUBLIC_KEY.key_id = {0}", Base64.encodeBytes(keyId));
234 | logger.debugv(" CREDENTIAL_PUBLIC_KEY.algorithm = {0}", credPubKey.getAlgorithm().name());
235 | logger.debugv(" CREDENTIAL_PUBLIC_KEY.key_type = {0}", credPubKey.getKeyType().name());
236 | }
237 | if (context != null) {
238 | // only set on Authentication
239 | logger.debugv(" Credential Id = {0}", Base64.encodeBytes(context.getCredentialId()));
240 | }
241 |
242 | }
243 |
244 | }
245 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/credential/WebAuthnCredentialProviderFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.credential;
18 |
19 | import org.keycloak.models.KeycloakSession;
20 |
21 | public class WebAuthnCredentialProviderFactory implements CredentialProviderFactory {
22 | @Override
23 | public CredentialProvider create(KeycloakSession session) {
24 | return new WebAuthnCredentialProvider(session);
25 | }
26 |
27 | @Override
28 | public String getId() {
29 | return "keycloak-webauthn";
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/forms/login/freemarker/model/WebAuthnAuthenticatorsBean.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.keycloak.forms.login.freemarker.model;
17 |
18 | import java.util.LinkedList;
19 | import java.util.List;
20 |
21 | import org.keycloak.WebAuthnConstants;
22 | import org.keycloak.models.UserModel;
23 |
24 | public class WebAuthnAuthenticatorsBean {
25 | private List authenticators = new LinkedList();
26 |
27 | public WebAuthnAuthenticatorsBean(UserModel user) {
28 | // should consider multiple credentials in the future, but only single credential supported now.
29 | List credentialIds = user.getAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
30 | List labels = user.getAttribute(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR);
31 | if (credentialIds != null && credentialIds.size() == 1 && !credentialIds.get(0).isEmpty()) {
32 | String credentialId = credentialIds.get(0);
33 | String label = (labels.size() == 1 && !labels.get(0).isEmpty()) ? labels.get(0) : "label missing";
34 | authenticators.add(new WebAuthnAuthenticatorBean(credentialId, label));
35 | }
36 | }
37 |
38 | public List getAuthenticators() {
39 | return authenticators;
40 | }
41 |
42 | public static class WebAuthnAuthenticatorBean {
43 | private final String credentialId;
44 | private final String label;
45 |
46 | public WebAuthnAuthenticatorBean(String credentialId, String label) {
47 | this.credentialId = credentialId;
48 | this.label = label;
49 | }
50 |
51 | public String getCredentialId() {
52 | return this.credentialId;
53 | }
54 |
55 | public String getLabel() {
56 | return this.label;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/models/jpa/converter/AAGUIDConverter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.models.jpa.converter;
18 |
19 | import com.webauthn4j.data.attestation.authenticator.AAGUID;
20 |
21 | import javax.persistence.AttributeConverter;
22 |
23 | public class AAGUIDConverter implements AttributeConverter {
24 | @Override
25 | public byte[] convertToDatabaseColumn(AAGUID aaguid) {
26 | return aaguid.getBytes();
27 | }
28 |
29 | @Override
30 | public AAGUID convertToEntityAttribute(byte[] bytes) {
31 | return new AAGUID(bytes);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/models/jpa/converter/AttestationStatementConverter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.models.jpa.converter;
18 |
19 | import com.webauthn4j.converter.util.CborConverter;
20 | import com.webauthn4j.data.attestation.statement.AttestationStatement;
21 | import com.webauthn4j.util.Base64UrlUtil;
22 |
23 | import javax.persistence.AttributeConverter;
24 |
25 | public class AttestationStatementConverter implements AttributeConverter {
26 |
27 | private CborConverter converter = new CborConverter(); //TODO: Inject by CDI to make it singleton
28 |
29 | @Override
30 | public String convertToDatabaseColumn(AttestationStatement attribute) {
31 | return Base64UrlUtil.encodeToString(converter.writeValueAsBytes(attribute));
32 | }
33 |
34 | @Override
35 | public AttestationStatement convertToEntityAttribute(String dbData) {
36 | return converter.readValue(Base64UrlUtil.decode(dbData), AttestationStatement.class);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/java/org/keycloak/models/jpa/converter/CredentialPublicKeyConverter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.models.jpa.converter;
18 |
19 | import com.webauthn4j.converter.util.CborConverter;
20 | import com.webauthn4j.data.attestation.authenticator.CredentialPublicKey;
21 | import com.webauthn4j.util.Base64UrlUtil;
22 |
23 | import javax.persistence.AttributeConverter;
24 |
25 | public class CredentialPublicKeyConverter implements AttributeConverter {
26 |
27 | private CborConverter converter = new CborConverter(); //TODO: Inject by CDI to make it singleton
28 |
29 | @Override
30 | public String convertToDatabaseColumn(CredentialPublicKey credentialPublicKey) {
31 | return Base64UrlUtil.encodeToString(converter.writeValueAsBytes(credentialPublicKey));
32 | }
33 |
34 | @Override
35 | public CredentialPublicKey convertToEntityAttribute(String s) {
36 | return converter.readValue(Base64UrlUtil.decode(s), CredentialPublicKey.class);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/META-INF/keycloak-themes.json:
--------------------------------------------------------------------------------
1 | {
2 | "themes" : [{
3 | "name" : "webauthn",
4 | "types" : [ "account" ]
5 | }]
6 | }
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory:
--------------------------------------------------------------------------------
1 | org.keycloak.authentication.authenticators.browser.WebAuthn4jAuthenticatorFactory
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory:
--------------------------------------------------------------------------------
1 | org.keycloak.authentication.requiredactions.RegisterAuthenticatorFactory
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory:
--------------------------------------------------------------------------------
1 | org.keycloak.credential.WebAuthnCredentialProviderFactory
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/theme-resources/resources/base64url.js:
--------------------------------------------------------------------------------
1 | // for embedded scripts, quoted and modified from https://github.com/swansontec/rfc4648.js by William Swanson
2 | 'use strict';
3 | var base64url = base64url || {};
4 | (function(base64url) {
5 |
6 | function parse (string, encoding, opts = {}) {
7 | // Build the character lookup table:
8 | if (!encoding.codes) {
9 | encoding.codes = {};
10 | for (let i = 0; i < encoding.chars.length; ++i) {
11 | encoding.codes[encoding.chars[i]] = i;
12 | }
13 | }
14 |
15 | // The string must have a whole number of bytes:
16 | if (!opts.loose && (string.length * encoding.bits) & 7) {
17 | throw new SyntaxError('Invalid padding');
18 | }
19 |
20 | // Count the padding bytes:
21 | let end = string.length;
22 | while (string[end - 1] === '=') {
23 | --end;
24 |
25 | // If we get a whole number of bytes, there is too much padding:
26 | if (!opts.loose && !(((string.length - end) * encoding.bits) & 7)) {
27 | throw new SyntaxError('Invalid padding');
28 | }
29 | }
30 |
31 | // Allocate the output:
32 | const out = new (opts.out || Uint8Array)(((end * encoding.bits) / 8) | 0);
33 |
34 | // Parse the data:
35 | let bits = 0; // Number of bits currently in the buffer
36 | let buffer = 0; // Bits waiting to be written out, MSB first
37 | let written = 0; // Next byte to write
38 | for (let i = 0; i < end; ++i) {
39 | // Read one character from the string:
40 | const value = encoding.codes[string[i]];
41 | if (value === void 0) {
42 | throw new SyntaxError('Invalid character ' + string[i]);
43 | }
44 |
45 | // Append the bits to the buffer:
46 | buffer = (buffer << encoding.bits) | value;
47 | bits += encoding.bits;
48 |
49 | // Write out some bits if the buffer has a byte's worth:
50 | if (bits >= 8) {
51 | bits -= 8;
52 | out[written++] = 0xff & (buffer >> bits);
53 | }
54 | }
55 |
56 | // Verify that we have received just enough bits:
57 | if (bits >= encoding.bits || 0xff & (buffer << (8 - bits))) {
58 | throw new SyntaxError('Unexpected end of data');
59 | }
60 |
61 | return out
62 | }
63 |
64 | function stringify (data, encoding, opts = {}) {
65 | const { pad = true } = opts;
66 | const mask = (1 << encoding.bits) - 1;
67 | let out = '';
68 |
69 | let bits = 0; // Number of bits currently in the buffer
70 | let buffer = 0; // Bits waiting to be written out, MSB first
71 | for (let i = 0; i < data.length; ++i) {
72 | // Slurp data into the buffer:
73 | buffer = (buffer << 8) | (0xff & data[i]);
74 | bits += 8;
75 |
76 | // Write out as much as we can:
77 | while (bits > encoding.bits) {
78 | bits -= encoding.bits;
79 | out += encoding.chars[mask & (buffer >> bits)];
80 | }
81 | }
82 |
83 | // Partial character:
84 | if (bits) {
85 | out += encoding.chars[mask & (buffer << (encoding.bits - bits))];
86 | }
87 |
88 | // Add padding characters until we hit a byte boundary:
89 | if (pad) {
90 | while ((out.length * encoding.bits) & 7) {
91 | out += '=';
92 | }
93 | }
94 |
95 | return out
96 | }
97 |
98 | const encoding = {
99 | chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
100 | bits: 6
101 | }
102 |
103 | base64url.decode = function (string, opts) {
104 | return parse(string, encoding, opts);
105 | }
106 |
107 | base64url.encode = function (data, opts) {
108 | return stringify(data, encoding, opts)
109 | }
110 |
111 | return base64url;
112 | }(base64url));
113 |
114 |
115 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/theme-resources/templates/webauthn-register.ftl:
--------------------------------------------------------------------------------
1 | <#import "template.ftl" as layout>
2 | <@layout.registrationLayout; section>
3 | <#if section = "title">
4 | title
5 | <#elseif section = "header">
6 | ${msg("loginTitleHtml", realm.name)}
7 | <#elseif section = "form">
8 |
17 |
18 |
19 |
81 |
82 | #if>
83 | @layout.registrationLayout>
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/theme-resources/templates/webauthn.ftl:
--------------------------------------------------------------------------------
1 | <#import "template.ftl" as layout>
2 | <@layout.registrationLayout; section>
3 | <#if section = "title">
4 | title
5 | <#elseif section = "header">
6 | ${msg("loginTitleHtml", realm.name)}
7 | <#elseif section = "form">
8 |
9 |
19 |
20 | <#if authenticators??>
21 |
44 | #if>
45 |
46 |
47 |
48 |
108 | <#elseif section = "info">
109 |
110 | #if>
111 | @layout.registrationLayout>
112 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/theme/webauthn/account/account.ftl:
--------------------------------------------------------------------------------
1 | <#import "template.ftl" as layout>
2 | <@layout.mainLayout active='account' bodyClass='user'; section>
3 |
4 |
5 |
6 |
${msg("editAccountHtmlTitle")}
7 |
8 |
9 | * ${msg("requiredFields")}
10 |
11 |
12 |
13 |
99 |
100 | @layout.mainLayout>
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/main/resources/theme/webauthn/account/theme.properties:
--------------------------------------------------------------------------------
1 | parent=keycloak
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/authentication/authenticators/browser/WebAuthn4jAuthenticatorFactoryTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.authentication.authenticators.browser;
18 |
19 | import org.keycloak.authentication.authenticators.browser.WebAuthn4jAuthenticatorFactory;
20 | import org.keycloak.models.AuthenticationExecutionModel;
21 | import org.keycloak.models.KeycloakSession;
22 |
23 | import org.junit.Test;
24 | import org.junit.Assert;
25 |
26 | import static org.mockito.Mockito.mock;
27 | import org.mockito.Mockito;
28 |
29 | public class WebAuthn4jAuthenticatorFactoryTest {
30 |
31 | @Test
32 | public void test_getDisplayType() throws Exception {
33 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
34 | Assert.assertEquals("WebAuthn Authenticator", factory.getDisplayType());
35 | }
36 |
37 | @Test
38 | public void test_getReferenceCategory() throws Exception {
39 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
40 | Assert.assertEquals("auth", factory.getReferenceCategory());
41 | }
42 |
43 | @Test
44 | public void test_isConfigurable() throws Exception {
45 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
46 | Assert.assertFalse(factory.isConfigurable());
47 | }
48 |
49 | @Test
50 | public void test_getRequirementChoices() throws Exception {
51 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
52 | AuthenticationExecutionModel.Requirement[] requirements = {
53 | AuthenticationExecutionModel.Requirement.REQUIRED,
54 | AuthenticationExecutionModel.Requirement.ALTERNATIVE,
55 | AuthenticationExecutionModel.Requirement.DISABLED,
56 | };
57 | Assert.assertArrayEquals(requirements, factory.getRequirementChoices());
58 | }
59 |
60 | @Test
61 | public void test_isUserSetupAllowed() throws Exception {
62 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
63 | Assert.assertTrue(factory.isUserSetupAllowed());
64 | }
65 |
66 | @Test
67 | public void test_getHelpText() throws Exception {
68 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
69 | Assert.assertEquals("Authenticator for WebAuthn", factory.getHelpText());
70 | }
71 |
72 | @Test
73 | public void test_getConfigProperties() throws Exception {
74 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
75 | Assert.assertTrue(factory.getConfigProperties().isEmpty());
76 | }
77 |
78 | @Test
79 | public void test_init() throws Exception {
80 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
81 | try {
82 | factory.init(null);
83 | } catch (Exception e) {
84 | Assert.fail(e.getMessage());
85 | }
86 | }
87 |
88 | @Test
89 | public void test_create() throws Exception {
90 | KeycloakSession session = mock(KeycloakSession.class, Mockito.RETURNS_DEEP_STUBS);
91 |
92 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
93 | Assert.assertNotNull(factory.create(session));
94 | }
95 |
96 | @Test
97 | public void test_postInit() throws Exception {
98 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
99 | try {
100 | factory.postInit(null);
101 | } catch (Exception e) {
102 | Assert.fail(e.getMessage());
103 | }
104 | }
105 |
106 | @Test
107 | public void test_close() throws Exception {
108 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
109 | try {
110 | factory.close();
111 | } catch (Exception e) {
112 | Assert.fail(e.getMessage());
113 | }
114 | }
115 |
116 | @Test
117 | public void test_getId() throws Exception {
118 | WebAuthn4jAuthenticatorFactory factory = new WebAuthn4jAuthenticatorFactory();
119 | Assert.assertEquals(WebAuthn4jAuthenticatorFactory.PROVIDER_ID, factory.getId());
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/authentication/authenticators/browser/WebAuthn4jAuthenticatorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.authentication.authenticators.browser;
18 |
19 | import java.net.URI;
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | import javax.ws.rs.core.Response;
24 | import javax.ws.rs.core.MultivaluedHashMap;
25 | import javax.ws.rs.core.MultivaluedMap;
26 |
27 | import org.keycloak.WebAuthnConstants;
28 | import org.keycloak.authentication.AuthenticationFlowContext;
29 | import org.keycloak.authentication.AuthenticationFlowError;
30 | import org.keycloak.authentication.AuthenticationFlowException;
31 | import org.keycloak.authentication.authenticators.browser.WebAuthn4jAuthenticator;
32 | import org.keycloak.common.util.Base64Url;
33 | import org.keycloak.credential.CredentialInput;
34 | import org.keycloak.models.KeycloakSession;
35 | import org.keycloak.models.RealmModel;
36 | import org.keycloak.models.UserModel;
37 | import org.keycloak.models.utils.KeycloakModelUtils;
38 |
39 | import org.junit.Test;
40 | import org.junit.Assert;
41 | import org.junit.Before;
42 |
43 | import static org.mockito.Mockito.mock;
44 | import static org.mockito.Mockito.when;
45 | import static org.mockito.Mockito.any;
46 | import static org.mockito.Mockito.verify;
47 | import static org.mockito.Mockito.times;
48 | import org.mockito.Mockito;
49 |
50 | public class WebAuthn4jAuthenticatorTest {
51 |
52 | private KeycloakSession session;
53 | private WebAuthn4jAuthenticator authenticator;
54 | private AuthenticationFlowContext context;
55 |
56 | @Before
57 | public void setupMock() throws Exception {
58 | this.session = mock(KeycloakSession.class, Mockito.RETURNS_DEEP_STUBS);
59 | this.authenticator = new WebAuthn4jAuthenticator(session);
60 | this.context = mock(AuthenticationFlowContext.class, Mockito.RETURNS_DEEP_STUBS);
61 | // avoid NPE
62 | when(context.getUriInfo().getBaseUri()).thenReturn(new URI("http://localhost:8080"));
63 | when(context.getRealm().getName()).thenReturn("webauthn");
64 | }
65 |
66 | @Test
67 | public void test_authenticate_2factor() throws Exception {
68 | // set up mock
69 | List publicKeyCredentialIds = new ArrayList<>();
70 | publicKeyCredentialIds.add(getRandomString(32));
71 | when(context.getUser().getAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR)).thenReturn(publicKeyCredentialIds);
72 |
73 | // test
74 | try {
75 | authenticator.authenticate(context);
76 | } catch (Exception e) {
77 | Assert.fail(e.getMessage());
78 | }
79 | verify(context).challenge(any(Response.class));
80 | }
81 |
82 | @Test
83 | public void test_authenticate_2factor_publickey_not_registered() throws Exception {
84 | // set up mock
85 | when(context.getUser().getAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR)).thenReturn(null);
86 |
87 | // test
88 | try {
89 | authenticator.authenticate(context);
90 | Assert.fail();
91 | } catch (AuthenticationFlowException e) {
92 | // NOP
93 | }
94 | when(context.getUser().getAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR)).thenReturn(new ArrayList());
95 | try {
96 | authenticator.authenticate(context);
97 | Assert.fail();
98 | } catch (AuthenticationFlowException e) {
99 | // NOP
100 | }
101 | }
102 |
103 | @Test
104 | public void test_authenticate_2factor_multiple_publicKey_registered() throws Exception {
105 | // set up mock
106 | List publicKeyCredentialIds = new ArrayList<>();
107 | publicKeyCredentialIds.add(getRandomString(32));
108 | publicKeyCredentialIds.add(getRandomString(32));
109 | when(context.getUser().getAttribute(WebAuthnConstants.PUBKEY_CRED_ID_ATTR)).thenReturn(publicKeyCredentialIds);
110 |
111 | // test
112 | try {
113 | authenticator.authenticate(context);
114 | Assert.fail();
115 | } catch (AuthenticationFlowException e) {
116 | // NOP
117 | }
118 | }
119 |
120 | @Test
121 | public void test_authenticate_passwordless() throws Exception {
122 | // set up mock
123 | when(context.getUser()).thenReturn(null);
124 |
125 | // test
126 | try {
127 | authenticator.authenticate(context);
128 | } catch (Exception e) {
129 | Assert.fail(e.getMessage());
130 | }
131 | verify(context).challenge(any(Response.class));
132 | }
133 |
134 | @Test
135 | public void test_action_passwordless() throws Exception {
136 | // set up mock
137 | when(session.userCredentialManager()
138 | .isValid(any(RealmModel.class), any(UserModel.class), Mockito.anyVararg()))
139 | .thenReturn(true);
140 |
141 | MultivaluedMap params = getSimulatedParametersFromAuthenticationResponse();
142 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
143 |
144 | when(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE))
145 | .thenReturn(getRandomString(32));
146 |
147 | // test
148 | try {
149 | authenticator.action(context);
150 | } catch (Exception e) {
151 | Assert.fail(e.getMessage());
152 | }
153 | verify(context, times(1)).setUser(any(UserModel.class));
154 | verify(context).success();
155 | }
156 |
157 |
158 | @Test
159 | public void test_action_credential_not_valid() throws Exception {
160 | // set up mock
161 | when(session.userCredentialManager()
162 | .isValid(Mockito.any(RealmModel.class), any(UserModel.class), Mockito.anyVararg()))
163 | .thenThrow(new AuthenticationFlowException("unknown user authenticated by the authenticator", AuthenticationFlowError.UNKNOWN_USER));
164 |
165 | MultivaluedMap params = getSimulatedParametersFromAuthenticationResponse();
166 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
167 |
168 | when(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE))
169 | .thenReturn(getRandomString(32));
170 |
171 | // test
172 | try {
173 | authenticator.action(context);
174 | Assert.fail();
175 | } catch (AuthenticationFlowException e) {
176 | // NOP
177 | }
178 | }
179 |
180 | @Test
181 | public void test_action_credential_validation_fail() throws Exception {
182 | // set up mock
183 | when(session.userCredentialManager()
184 | .isValid(any(RealmModel.class), any(UserModel.class), Mockito.anyVararg()))
185 | .thenReturn(false);
186 |
187 | MultivaluedMap params = getSimulatedParametersFromAuthenticationResponse();
188 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
189 |
190 | when(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE))
191 | .thenReturn(getRandomString(32));
192 |
193 | // test
194 | try {
195 | authenticator.action(context);
196 | } catch (Exception e) {
197 | Assert.fail(e.getMessage());
198 | }
199 | Mockito.verify(context).cancelLogin();
200 | }
201 |
202 | @Test
203 | public void test_action_navigator_credentials_get_error() throws Exception {
204 | // set up mock
205 | MultivaluedMap params = new MultivaluedHashMap<>();
206 | params.add("error", "The user attempted to use an authenticator that recognized none of the provided credentials");
207 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
208 |
209 | // test
210 | try {
211 | authenticator.action(context);
212 | Assert.fail();
213 | } catch (AuthenticationFlowException e) {
214 | // NOP
215 | }
216 | }
217 |
218 | @Test
219 | public void test_action_2factor_residentkey() throws Exception {
220 | // set up mock
221 | when(session.userCredentialManager()
222 | .isValid(any(RealmModel.class), any(UserModel.class), Mockito.anyVararg()))
223 | .thenReturn(true);
224 |
225 | String userId = getRandomString(32);
226 | MultivaluedMap params = getSimulatedParametersFromAuthenticationResponse(userId);
227 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
228 |
229 | when(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE))
230 | .thenReturn(getRandomString(32));
231 |
232 | UserModel user = mock(UserModel.class);
233 | when(user.getId()).thenReturn(userId);
234 | when(context.getUser()).thenReturn(user);
235 |
236 | // test
237 | try {
238 | authenticator.action(context);
239 | } catch (Exception e) {
240 | Assert.fail(e.getMessage());
241 | }
242 | verify(context, times(1)).setUser(any(UserModel.class));
243 | verify(context).success();
244 | }
245 |
246 | @Test
247 | public void test_action_2factor_residentkey_different_user_authenticated() throws Exception {
248 | // set up mock
249 | String userId = getRandomString(32);
250 | MultivaluedMap params = getSimulatedParametersFromAuthenticationResponse(userId);
251 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
252 |
253 | when(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE))
254 | .thenReturn(getRandomString(32));
255 |
256 | UserModel user = mock(UserModel.class);
257 | when(user.getId()).thenReturn(getRandomString(32));
258 | when(context.getUser()).thenReturn(user);
259 |
260 | // test
261 | try {
262 | authenticator.action(context);
263 | Assert.fail();
264 | } catch (AuthenticationFlowException e) {
265 | // NOP
266 | }
267 | }
268 |
269 | @Test
270 | public void test_requiresUser() throws Exception {
271 | // test
272 | Assert.assertFalse(authenticator.requiresUser());
273 | }
274 |
275 | @Test
276 | public void test_close() throws Exception {
277 | // test
278 | try {
279 | authenticator.close();
280 | } catch (Exception e) {
281 | Assert.fail(e.getMessage());
282 | }
283 | }
284 |
285 | @Test
286 | public void test_configuredFor() throws Exception {
287 | // test
288 | Assert.assertTrue(authenticator.configuredFor(session, mock(RealmModel.class), mock(UserModel.class)));
289 | }
290 |
291 | @Test
292 | public void test_setRequiredActions() throws Exception {
293 | // test
294 | try {
295 | authenticator.setRequiredActions(session, mock(RealmModel.class), mock(UserModel.class));
296 | } catch (Exception e) {
297 | Assert.fail(e.getMessage());
298 | }
299 | }
300 |
301 | private String getRandomString(int sizeInByte) {
302 | return Base64Url.encode(KeycloakModelUtils.generateSecret(sizeInByte));
303 | }
304 |
305 | private MultivaluedMap getSimulatedParametersFromAuthenticationResponse(String userHandle) {
306 | return getSimulatedParametersFromAuthenticationResponse(null, null, null, null, userHandle);
307 | }
308 |
309 | private MultivaluedMap getSimulatedParametersFromAuthenticationResponse() {
310 | return getSimulatedParametersFromAuthenticationResponse(null, null, null, null, null);
311 | }
312 |
313 | private MultivaluedMap getSimulatedParametersFromAuthenticationResponse(
314 | String clientDataJSON,
315 | String authenticatorData,
316 | String signature,
317 | String credentialId,
318 | String userHandle) {
319 | MultivaluedMap params = new MultivaluedHashMap<>();
320 | if (clientDataJSON == null) clientDataJSON = getRandomString(32);
321 | params.add(WebAuthnConstants.CLIENT_DATA_JSON, clientDataJSON);
322 | if (authenticatorData == null) authenticatorData = getRandomString(32);
323 | params.add(WebAuthnConstants.AUTHENTICATOR_DATA, authenticatorData);
324 | if (signature == null) signature = getRandomString(32);
325 | params.add(WebAuthnConstants.SIGNATURE, signature);
326 | if (credentialId == null) credentialId = getRandomString(32);
327 | params.add(WebAuthnConstants.CREDENTIAL_ID, credentialId);
328 | if (userHandle != null) params.add(WebAuthnConstants.USER_HANDLE, userHandle);
329 | return params;
330 | }
331 |
332 | }
333 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/authentication/requiredactions/RegisterAuthenticatorFactoryTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.authentication.requiredactions;
18 |
19 | import org.keycloak.OAuth2Constants;
20 | import org.keycloak.models.AuthenticationExecutionModel;
21 | import org.keycloak.models.KeycloakSession;
22 |
23 | import org.junit.Test;
24 | import org.junit.Assert;
25 |
26 | import static org.mockito.Mockito.mock;
27 | import org.mockito.Mockito;
28 |
29 | public class RegisterAuthenticatorFactoryTest {
30 |
31 | @Test
32 | public void test_init() throws Exception {
33 | RegisterAuthenticatorFactory factory = new RegisterAuthenticatorFactory();
34 | try {
35 | factory.init(null);
36 | } catch (Exception e) {
37 | Assert.fail(e.getMessage());
38 | }
39 | }
40 |
41 | @Test
42 | public void test_create() throws Exception {
43 | KeycloakSession session = mock(KeycloakSession.class, Mockito.RETURNS_DEEP_STUBS);
44 |
45 | RegisterAuthenticatorFactory factory = new RegisterAuthenticatorFactory();
46 | Assert.assertNotNull(factory.create(session));
47 | }
48 |
49 | @Test
50 | public void test_postInit() throws Exception {
51 | RegisterAuthenticatorFactory factory = new RegisterAuthenticatorFactory();
52 | try {
53 | factory.postInit(null);
54 | } catch (Exception e) {
55 | Assert.fail(e.getMessage());
56 | }
57 | }
58 |
59 | @Test
60 | public void test_close() throws Exception {
61 | RegisterAuthenticatorFactory factory = new RegisterAuthenticatorFactory();
62 | try {
63 | factory.close();
64 | } catch (Exception e) {
65 | Assert.fail(e.getMessage());
66 | }
67 | }
68 |
69 | @Test
70 | public void test_getId() throws Exception {
71 | RegisterAuthenticatorFactory factory = new RegisterAuthenticatorFactory();
72 | Assert.assertEquals(RegisterAuthenticatorFactory.PROVIDER_ID, factory.getId());
73 | }
74 |
75 | @Test
76 | public void test_createDispley() throws Exception {
77 | KeycloakSession session = mock(KeycloakSession.class, Mockito.RETURNS_DEEP_STUBS);
78 |
79 | RegisterAuthenticatorFactory factory = new RegisterAuthenticatorFactory();
80 | Assert.assertNotNull(factory.createDisplay(session, null));
81 | Assert.assertNull(factory.createDisplay(session, OAuth2Constants.DISPLAY_CONSOLE));
82 | Assert.assertNull(factory.createDisplay(session, "fujiyama"));
83 | }
84 |
85 | @Test
86 | public void test_getDisplayText() throws Exception {
87 | RegisterAuthenticatorFactory factory = new RegisterAuthenticatorFactory();
88 | Assert.assertEquals("Webauthn Register", factory.getDisplayText());
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/authentication/requiredactions/RegisterAuthenticatorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.authentication.requiredactions;
18 |
19 | import java.net.URI;
20 |
21 | import javax.ws.rs.core.MultivaluedHashMap;
22 | import javax.ws.rs.core.MultivaluedMap;
23 | import javax.ws.rs.core.Response;
24 |
25 | import org.keycloak.WebAuthnConstants;
26 | import org.keycloak.authentication.AuthenticationFlowException;
27 | import org.keycloak.authentication.RequiredActionContext;
28 | import org.keycloak.models.KeycloakSession;
29 |
30 | import org.junit.Test;
31 | import org.junit.Assert;
32 | import org.junit.Before;
33 |
34 | import static org.mockito.Mockito.mock;
35 | import static org.mockito.Mockito.verify;
36 | import static org.mockito.Mockito.when;
37 | import static org.mockito.Matchers.any;
38 | import org.mockito.Mockito;
39 |
40 | public class RegisterAuthenticatorTest {
41 | private KeycloakSession session;
42 | private RegisterAuthenticator authenticator;
43 | private RequiredActionContext context;
44 |
45 | private static final String ChallengeSample = "q8b_snApQBGDSlHKrUPXDA"; // used to generate the sample below
46 | private static final String ClientDataJSONSample
47 | = "eyJjaGFsbGVuZ2UiOiJxOGJfc25BcFFCR0RTbEhLclVQWERBIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9";
48 | private static final String AttestationObjectSample
49 | = "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQNl5cq57gFloyTRaRzspkmVtaFjseFuas8LzmCa9_M40tZHwnOxuDFLj__IQkmCi9bwtXfxGU8L3IbXoJf-R1v6lAQIDJiABIVggHRj3_pRuFc4STvzzqO3WgO9cnj7u9R4OogbtOc4qA5kiWCAniOpK656_61Qnmx4hkWffohlH4JDbuytCpCtf9jrruA";
50 | private static final String PublicKeyCredentialIdSample
51 | = "2XlyrnuAWWjJNFpHOymSZW1oWOx4W5qzwvOYJr38zjS1kfCc7G4MUuP_8hCSYKL1vC1d_EZTwvchtegl_5HW_g";
52 |
53 | @Before
54 | public void setupMock() throws Exception {
55 | this.session = mock(KeycloakSession.class, Mockito.RETURNS_DEEP_STUBS);
56 | this.authenticator = new RegisterAuthenticator(session);
57 | this.context = mock(RequiredActionContext.class, Mockito.RETURNS_DEEP_STUBS);
58 | // avoid NPE
59 | when(context.getUriInfo().getBaseUri()).thenReturn(new URI("http://localhost:8080"));
60 | when(context.getRealm().getName()).thenReturn("webauthn");
61 | }
62 |
63 | @Test
64 | public void test_evaluateTriggers() throws Exception {
65 | // test
66 | try {
67 | authenticator.evaluateTriggers(context);
68 | } catch (Exception e) {
69 | Assert.fail(e.getMessage());
70 | }
71 | }
72 |
73 | @Test
74 | public void test_close() throws Exception {
75 | // test
76 | try {
77 | authenticator.close();
78 | } catch (Exception e) {
79 | Assert.fail(e.getMessage());
80 | }
81 | }
82 |
83 | @Test
84 | public void test_requiredActionChallenge() throws Exception {
85 | // test
86 | try {
87 | authenticator.requiredActionChallenge(context);
88 | } catch (Exception e) {
89 | Assert.fail(e.getMessage());
90 | }
91 | verify(context).challenge(any(Response.class));
92 | }
93 |
94 | @Test
95 | public void test_action_navigator_credentials_create_error() throws Exception {
96 | // set up mock
97 | MultivaluedMap params = new MultivaluedHashMap<>();
98 | params.add(WebAuthnConstants.ERROR, "NotAllowedError");
99 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
100 |
101 | // test
102 | try {
103 | authenticator.processAction(context);
104 | Assert.fail();
105 | } catch (AuthenticationFlowException e) {
106 | // NOP
107 | }
108 | }
109 |
110 | @Test
111 | public void test_action() throws Exception {
112 | // set up mock
113 | MultivaluedMap params = getSimulatedParametersFromRegistrationResponse();
114 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
115 |
116 | when(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE))
117 | .thenReturn(RegisterAuthenticatorTest.ChallengeSample);
118 |
119 | // test
120 | try {
121 | authenticator.processAction(context);
122 | } catch (Exception e) {
123 | Assert.fail(e.getMessage());
124 | }
125 | verify(context).success();
126 | }
127 |
128 | @Test
129 | public void test_action_webauthn4j_validation_fails() throws Exception {
130 | // setup mock
131 | MultivaluedMap params = getSimulatedParametersFromRegistrationResponse();
132 | when(context.getHttpRequest().getDecodedFormParameters()).thenReturn(params);
133 |
134 | when(context.getAuthenticationSession().getAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE)).thenReturn("7777777777777777");
135 |
136 | // test
137 | try {
138 | authenticator.processAction(context);
139 | Assert.fail();
140 | } catch (AuthenticationFlowException e) {
141 | // NOP
142 | }
143 | }
144 |
145 | private MultivaluedMap getSimulatedParametersFromRegistrationResponse() {
146 | return getSimulatedParametersFromRegistrationResponse(null, null, null);
147 | }
148 |
149 | private MultivaluedMap getSimulatedParametersFromRegistrationResponse(
150 | String clientDataJSON,
151 | String attestationObject,
152 | String publicKeyCredentialId) {
153 | MultivaluedMap params = new MultivaluedHashMap<>();
154 | if (clientDataJSON == null) clientDataJSON = ClientDataJSONSample;
155 | params.add(WebAuthnConstants.CLIENT_DATA_JSON, clientDataJSON);
156 | if (attestationObject == null) attestationObject = AttestationObjectSample;
157 | params.add(WebAuthnConstants.ATTESTATION_OBJECT, attestationObject);
158 | if (publicKeyCredentialId == null) publicKeyCredentialId = PublicKeyCredentialIdSample;
159 | params.add(WebAuthnConstants.PUBLIC_KEY_CREDENTIAL_ID, publicKeyCredentialId);
160 | return params;
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/credential/WebAuthnCredentialProviderFactoryTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.credential;
18 |
19 | import org.keycloak.models.KeycloakSession;
20 |
21 | import org.junit.Test;
22 | import org.junit.Assert;
23 | import org.junit.Before;
24 |
25 | import static org.mockito.Mockito.mock;
26 |
27 | public class WebAuthnCredentialProviderFactoryTest {
28 |
29 | private KeycloakSession session;
30 | private WebAuthnCredentialProviderFactory factory;
31 |
32 | @Before
33 | public void setupMock() throws Exception {
34 | this.session = mock(KeycloakSession.class);
35 | this.factory = new WebAuthnCredentialProviderFactory();
36 | }
37 |
38 | @Test
39 | public void test_create() throws Exception {
40 | Assert.assertNotNull(factory.create(session));
41 | }
42 |
43 | @Test
44 | public void test_getId() {
45 | Assert.assertEquals("keycloak-webauthn", factory.getId());
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/credential/WebAuthnCredentialProviderTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.credential;
18 |
19 | import java.lang.reflect.Method;
20 | import java.util.Arrays;
21 | import java.util.Base64;
22 |
23 | import org.keycloak.common.util.Base64Url;
24 | import org.keycloak.models.KeycloakSession;
25 | import org.keycloak.models.RealmModel;
26 | import org.keycloak.models.UserModel;
27 |
28 | import org.junit.Test;
29 | import org.junit.Assert;
30 | import org.junit.Before;
31 |
32 | import static org.mockito.Matchers.any;
33 | import static org.mockito.Mockito.mock;
34 | import static org.mockito.Mockito.times;
35 | import static org.mockito.Mockito.verify;
36 | import static org.mockito.Mockito.when;
37 | import static org.mockito.Mockito.anyString;
38 | import static org.mockito.Mockito.never;
39 | import org.mockito.Mockito;
40 |
41 | import com.webauthn4j.data.WebAuthnAuthenticationContext;
42 | import com.webauthn4j.data.WebAuthnRegistrationContext;
43 | import com.webauthn4j.data.client.Origin;
44 | import com.webauthn4j.data.client.challenge.Challenge;
45 | import com.webauthn4j.data.client.challenge.DefaultChallenge;
46 | import com.webauthn4j.server.ServerProperty;
47 | import com.webauthn4j.validator.WebAuthnRegistrationContextValidationResponse;
48 | import com.webauthn4j.validator.WebAuthnRegistrationContextValidator;
49 |
50 | public class WebAuthnCredentialProviderTest {
51 |
52 | private KeycloakSession session;
53 | private WebAuthnCredentialProvider provider;
54 |
55 | @Before
56 | public void setupMock() throws Exception {
57 | this.session = mock(KeycloakSession.class, Mockito.RETURNS_DEEP_STUBS);
58 | this.provider = new WebAuthnCredentialProvider(session);
59 | }
60 |
61 | @Test
62 | public void test_updateCredential() throws Exception {
63 | // set up mock
64 | CredentialModel credModel = mock(CredentialModel.class);
65 | WebAuthnCredentialModel model = getValidWebAuthnCredentialModel();
66 | when(session.userCredentialManager()
67 | .createCredential(any(RealmModel.class), any(UserModel.class), any(CredentialModel.class)))
68 | .thenReturn(credModel);
69 |
70 | // test
71 | Assert.assertTrue(provider.updateCredential(mock(RealmModel.class), mock(UserModel.class), model));
72 | }
73 |
74 | @Test
75 | public void test_updateCredential_input_null() throws Exception {
76 | // test
77 | Assert.assertFalse(provider.updateCredential(mock(RealmModel.class), mock(UserModel.class), null));
78 | }
79 |
80 | @Test
81 | public void test_updateCredential_input_invalid_type() throws Exception {
82 | // set up mock
83 | WebAuthnCredentialModel model = mock(WebAuthnCredentialModel.class);
84 | when(model.getType()).thenReturn("unknown");
85 |
86 | // test
87 | Assert.assertFalse(provider.updateCredential(mock(RealmModel.class), mock(UserModel.class), model));
88 | }
89 |
90 | @Test
91 | public void test_disableCredentialType() throws Exception {
92 | // set up mock
93 | when(session.userCredentialManager()
94 | .getStoredCredentialsByType(any(RealmModel.class), any(UserModel.class), anyString()))
95 | .thenReturn(Arrays.asList(mock(CredentialModel.class)));
96 | when(session.userCredentialManager()
97 | .removeStoredCredential(any(RealmModel.class), any(UserModel.class), anyString()))
98 | .thenReturn(true);
99 |
100 | // test
101 | try {
102 | provider.disableCredentialType(mock(RealmModel.class), mock(UserModel.class), WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE);
103 | } catch (Exception e) {
104 | Assert.fail(e.getMessage());
105 | }
106 | verify(session.userCredentialManager(), times(1)).removeStoredCredential(any(RealmModel.class), any(UserModel.class), anyString());
107 | }
108 |
109 | @Test
110 | public void test_disableCredentialType_unknown_type() throws Exception {
111 | // test
112 | try {
113 | provider.disableCredentialType(mock(RealmModel.class), mock(UserModel.class), "unknown");
114 | } catch (Exception e) {
115 | Assert.fail(e.getMessage());
116 | }
117 | verify(session.userCredentialManager(), never()).removeStoredCredential(any(RealmModel.class), any(UserModel.class), anyString());
118 | }
119 |
120 | @Test
121 | public void test_isConfiguredFor() {
122 | // set up mock
123 | when(session.userCredentialManager()
124 | .getStoredCredentialsByType(any(RealmModel.class), any(UserModel.class), anyString())
125 | .isEmpty())
126 | .thenReturn(false);
127 |
128 | // test
129 | try {
130 | provider.isConfiguredFor(mock(RealmModel.class), mock(UserModel.class), WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE);
131 | } catch (Exception e) {
132 | Assert.fail(e.getMessage());
133 | }
134 | }
135 |
136 | @Test
137 | public void test_isConfiguredFor_empty() {
138 | // set up mock
139 | when(session.userCredentialManager()
140 | .getStoredCredentialsByType(any(RealmModel.class), any(UserModel.class), anyString())
141 | .isEmpty())
142 | .thenReturn(true);
143 |
144 | // test
145 | try {
146 | provider.isConfiguredFor(mock(RealmModel.class), mock(UserModel.class), WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE);
147 | } catch (Exception e) {
148 | Assert.fail(e.getMessage());
149 | }
150 | }
151 |
152 | @Test
153 | public void test_getDisableableCredentialTypes() {
154 | // set up mock
155 | when(session.userCredentialManager()
156 | .getStoredCredentialsByType(any(RealmModel.class), any(UserModel.class), anyString())
157 | .isEmpty())
158 | .thenReturn(false);
159 |
160 | // test
161 | Assert.assertFalse(provider.getDisableableCredentialTypes(mock(RealmModel.class), mock(UserModel.class)).isEmpty());
162 | }
163 |
164 | @Test
165 | public void test_getDisableableCredentialTypes_empty() {
166 | // set up mock
167 | when(session.userCredentialManager()
168 | .getStoredCredentialsByType(any(RealmModel.class), any(UserModel.class), anyString())
169 | .isEmpty())
170 | .thenReturn(true);
171 |
172 | // test
173 | Assert.assertTrue(provider.getDisableableCredentialTypes(mock(RealmModel.class), mock(UserModel.class)).isEmpty());
174 | }
175 |
176 | @Test
177 | public void test_isConfiguredFor_unknown_type() {
178 | // test
179 | try {
180 | provider.isConfiguredFor(mock(RealmModel.class), mock(UserModel.class), "unknown");
181 | } catch (Exception e) {
182 | Assert.fail(e.getMessage());
183 | }
184 | }
185 |
186 | @Test
187 | public void test_isValid() throws Exception {
188 | // set up mock
189 | // mimic valid model created on Authentication
190 | WebAuthnAuthenticationContext authenticationContext = getValidWebAuthnAuthenticationContext();
191 | WebAuthnCredentialModel input = new WebAuthnCredentialModel();
192 | input.setAuthenticationContext(authenticationContext);
193 |
194 | // mimic valid model created on Registration
195 | WebAuthnCredentialModel model = getValidWebAuthnCredentialModel();
196 | Method method = WebAuthnCredentialProvider.class.getDeclaredMethod("createCredentialModel", CredentialInput.class);
197 | method.setAccessible(true);
198 | CredentialModel credModel = (CredentialModel)method.invoke(provider, model);
199 |
200 | when(session.userCredentialManager()
201 | .getStoredCredentialsByType(any(RealmModel.class), any(UserModel.class), anyString()))
202 | .thenReturn(Arrays.asList(credModel));
203 |
204 | // test
205 | Assert.assertTrue(provider.isValid(mock(RealmModel.class), mock(UserModel.class), input));
206 | }
207 |
208 | @Test
209 | public void test_isValid_credential_id_unmatch() throws Exception {
210 | // set up mock
211 | // mimic invalid model created on Authentication
212 | WebAuthnAuthenticationContext authenticationContext = getValidWebAuthnAuthenticationContext("atgcatgcatgcatgc");
213 | WebAuthnCredentialModel input = new WebAuthnCredentialModel();
214 | input.setAuthenticationContext(authenticationContext);
215 |
216 | // mimic valid model created on Registration
217 | WebAuthnCredentialModel model = getValidWebAuthnCredentialModel();
218 | Method method = WebAuthnCredentialProvider.class.getDeclaredMethod("createCredentialModel", CredentialInput.class);
219 | method.setAccessible(true);
220 | CredentialModel credModel = (CredentialModel)method.invoke(provider, model);
221 |
222 | when(session.userCredentialManager()
223 | .getStoredCredentialsByType(any(RealmModel.class), any(UserModel.class), anyString()))
224 | .thenReturn(Arrays.asList(credModel));
225 |
226 | // test
227 | Assert.assertFalse(provider.isValid(mock(RealmModel.class), mock(UserModel.class), input));
228 | }
229 |
230 | private WebAuthnCredentialModel getValidWebAuthnCredentialModel() {
231 | // mimic valid model created on Registration
232 | byte[] clientDataJSON = Base64.getUrlDecoder().decode("eyJjaGFsbGVuZ2UiOiJxOGJfc25BcFFCR0RTbEhLclVQWERBIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9");
233 | byte[] attestationObject = Base64.getUrlDecoder().decode("o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQNl5cq57gFloyTRaRzspkmVtaFjseFuas8LzmCa9_M40tZHwnOxuDFLj__IQkmCi9bwtXfxGU8L3IbXoJf-R1v6lAQIDJiABIVggHRj3_pRuFc4STvzzqO3WgO9cnj7u9R4OogbtOc4qA5kiWCAniOpK656_61Qnmx4hkWffohlH4JDbuytCpCtf9jrruA");
234 |
235 | Origin origin = new Origin("http://localhost:8080");
236 | Challenge challenge = new DefaultChallenge("q8b_snApQBGDSlHKrUPXDA");
237 | ServerProperty serverProperty = new ServerProperty(origin, "localhost", challenge, null);
238 |
239 | WebAuthnRegistrationContext registrationContext = new WebAuthnRegistrationContext(clientDataJSON, attestationObject, serverProperty, false);
240 | WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = WebAuthnRegistrationContextValidator.createNonStrictRegistrationContextValidator();
241 | WebAuthnRegistrationContextValidationResponse response = webAuthnRegistrationContextValidator.validate(registrationContext);
242 |
243 | WebAuthnCredentialModel credential = new WebAuthnCredentialModel();
244 | credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
245 | credential.setAttestationStatement(response.getAttestationObject().getAttestationStatement());
246 | credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
247 |
248 | return credential;
249 | }
250 |
251 | private WebAuthnAuthenticationContext getValidWebAuthnAuthenticationContext() {
252 | // mimic valid model created on Authentication
253 | return getValidWebAuthnAuthenticationContext("2XlyrnuAWWjJNFpHOymSZW1oWOx4W5qzwvOYJr38zjS1kfCc7G4MUuP_8hCSYKL1vC1d_EZTwvchtegl_5HW_g");
254 | }
255 |
256 | private WebAuthnAuthenticationContext getValidWebAuthnAuthenticationContext(String base64UrlCredentialId) {
257 | // mimic valid or invalid model created on Authentication
258 | byte[] credentialId = Base64Url.decode(base64UrlCredentialId);
259 | byte[] clientDataJSON = Base64Url.decode("eyJjaGFsbGVuZ2UiOiJ0R3o3R3RUQVE2T3FwVHpoOEtLQnFRIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9");
260 | byte[] authenticatorData = Base64Url.decode("SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAdg");
261 | byte[] signature = Base64Url.decode("MEUCIEaZhQ5dXi_C3IxU68ujLLt0DEcyk2EFPz_y45wYUA7AAiEAwkX86OFwpNzPRjSljTaTJVvZ_x9E6xnKhSmsKkUgmlo");
262 | Origin origin = new Origin("http://localhost:8080");
263 | Challenge challenge = new DefaultChallenge("tGz7GtTAQ6OqpTzh8KKBqQ");
264 | ServerProperty server = new ServerProperty(origin, "localhost", challenge, null);
265 | WebAuthnAuthenticationContext authenticationContext = new WebAuthnAuthenticationContext(
266 | credentialId,
267 | clientDataJSON,
268 | authenticatorData,
269 | signature,
270 | server,
271 | false
272 | );
273 | return authenticationContext;
274 | }
275 |
276 | }
277 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/models/jpa/converter/AAGUIDConverterTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.models.jpa.converter;
18 |
19 | import org.junit.Test;
20 | import org.junit.Assert;
21 |
22 | import org.keycloak.models.utils.KeycloakModelUtils;
23 |
24 | import com.webauthn4j.data.attestation.authenticator.AAGUID;
25 |
26 | public class AAGUIDConverterTest {
27 |
28 | @Test
29 | public void test_convert() throws Exception {
30 | AAGUIDConverter converter = new AAGUIDConverter();
31 | byte[] aaguidBytes = converter.convertToDatabaseColumn(new AAGUID(KeycloakModelUtils.generateSecret(16)));
32 | AAGUID aaguidEntity = converter.convertToEntityAttribute(aaguidBytes);
33 | Assert.assertArrayEquals(aaguidBytes, converter.convertToDatabaseColumn(aaguidEntity));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/models/jpa/converter/AttestationStatementConverterTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.models.jpa.converter;
18 |
19 | import org.junit.Test;
20 | import org.junit.Assert;
21 |
22 | import com.webauthn4j.data.attestation.statement.AttestationStatement;
23 | import com.webauthn4j.data.attestation.statement.NoneAttestationStatement;
24 |
25 | public class AttestationStatementConverterTest {
26 |
27 | @Test
28 | public void test_converter() throws Exception {
29 | AttestationStatementConverter converter = new AttestationStatementConverter();
30 | String stringifiedStatement = converter.convertToDatabaseColumn(new NoneAttestationStatement());
31 | AttestationStatement statement = converter.convertToEntityAttribute(stringifiedStatement);
32 | Assert.assertEquals(stringifiedStatement, converter.convertToDatabaseColumn(statement));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/webauthn4j-ejb/src/test/java/org/keycloak/models/jpa/converter/CredentialPublicKeyConverterTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.keycloak.models.jpa.converter;
18 |
19 | import org.junit.Test;
20 | import org.junit.Assert;
21 |
22 | import com.webauthn4j.data.attestation.authenticator.CredentialPublicKey;
23 | import com.webauthn4j.data.attestation.authenticator.Curve;
24 | import com.webauthn4j.data.attestation.authenticator.EC2CredentialPublicKey;
25 | import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier;
26 |
27 | public class CredentialPublicKeyConverterTest {
28 |
29 | @Test
30 | public void test_converter() {
31 | EC2CredentialPublicKey credentialPublicKey = EC2CredentialPublicKey.createFromUncompressedECCKey(createECCredentialPublicKey().getBytes());
32 | CredentialPublicKeyConverter converter = new CredentialPublicKeyConverter();
33 | String stringifiedKey = converter.convertToDatabaseColumn(credentialPublicKey);
34 | CredentialPublicKey key = converter.convertToEntityAttribute(stringifiedKey);
35 | Assert.assertEquals(stringifiedKey, converter.convertToDatabaseColumn(key));
36 | }
37 |
38 | private static EC2CredentialPublicKey createECCredentialPublicKey() {
39 | return new EC2CredentialPublicKey(
40 | null,
41 | COSEAlgorithmIdentifier.ES256,
42 | null,
43 | null,
44 | Curve.SECP256R1,
45 | new byte[32],
46 | new byte[32]
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------