passkeys.adoc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. [[passkeys]]
  2. = Passkeys
  3. Spring Security provides support for https://www.passkeys.com[passkeys].
  4. Passkeys are a more secure method of authenticating than passwords and are built using https://www.w3.org/TR/webauthn-3/[WebAuthn].
  5. In order to use a passkey to authenticate, a user must first xref:servlet/authentication/passkeys.adoc#passkeys-register[Register a New Credential].
  6. After the credential is registered, it can be used to authenticate by xref:servlet/authentication/passkeys.adoc#passkeys-verify[verifying an authentication assertion].
  7. [[passkeys-dependencies]]
  8. == Required Dependencies
  9. To get started, add the `webauthn4j-core` dependency to your project.
  10. [NOTE]
  11. ====
  12. This assumes that you are managing Spring Security's versions with Spring Boot or Spring Security's BOM as described in xref:getting-spring-security.adoc[].
  13. ====
  14. .Passkeys Dependencies
  15. [tabs]
  16. ======
  17. Maven::
  18. +
  19. [source,xml,role="primary",subs="verbatim,attributes"]
  20. ----
  21. <dependency>
  22. <groupId>org.springframework.security</groupId>
  23. <artifactId>spring-security-web</artifactId>
  24. </dependency>
  25. <dependency>
  26. <groupId>com.webauthn4j</groupId>
  27. <artifactId>webauthn4j-core</artifactId>
  28. <version>{webauthn4j-core-version}</version>
  29. </dependency>
  30. ----
  31. Gradle::
  32. +
  33. [source,groovy,role="secondary",subs="verbatim,attributes"]
  34. ----
  35. depenendencies {
  36. implementation "org.springframework.security:spring-security-web"
  37. implementation "com.webauthn4j:webauthn4j-core:{webauthn4j-core-version}"
  38. }
  39. ----
  40. ======
  41. [[passkeys-configuration]]
  42. == Configuration
  43. The following configuration enables passkey authentication.
  44. It provides a way to xref:./passkeys.adoc#passkeys-register[] at `/webauthn/register` and a default log in page that allows xref:./passkeys.adoc#passkeys-verify[authenticating with passkeys].
  45. [tabs]
  46. ======
  47. Java::
  48. +
  49. [source,java,role="primary"]
  50. ----
  51. @Bean
  52. SecurityFilterChain filterChain(HttpSecurity http) {
  53. // ...
  54. http
  55. // ...
  56. .formLogin(withDefaults())
  57. .webAuthn((webAuthn) -> webAuthn
  58. .rpName("Spring Security Relying Party")
  59. .rpId("example.com")
  60. .allowedOrigins("https://example.com")
  61. // optional properties
  62. .creationOptionsRepository(new CustomPublicKeyCredentialCreationOptionsRepository())
  63. .messageConverter(new CustomHttpMessageConverter())
  64. );
  65. return http.build();
  66. }
  67. @Bean
  68. UserDetailsService userDetailsService() {
  69. UserDetails userDetails = User.withDefaultPasswordEncoder()
  70. .username("user")
  71. .password("password")
  72. .roles("USER")
  73. .build();
  74. return new InMemoryUserDetailsManager(userDetails);
  75. }
  76. ----
  77. Kotlin::
  78. +
  79. [source,kotlin,role="secondary"]
  80. ----
  81. @Bean
  82. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  83. // ...
  84. http {
  85. webAuthn {
  86. rpName = "Spring Security Relying Party"
  87. rpId = "example.com"
  88. allowedOrigins = setOf("https://example.com")
  89. // optional properties
  90. creationOptionsRepository = CustomPublicKeyCredentialCreationOptionsRepository()
  91. messageConverter = CustomHttpMessageConverter()
  92. }
  93. }
  94. }
  95. @Bean
  96. open fun userDetailsService(): UserDetailsService {
  97. val userDetails = User.withDefaultPasswordEncoder()
  98. .username("user")
  99. .password("password")
  100. .roles("USER")
  101. .build()
  102. return InMemoryUserDetailsManager(userDetails)
  103. }
  104. ----
  105. ======
  106. [[passkeys-configuration-persistence]]
  107. === JDBC & Custom Persistence
  108. WebAuthn performs persistence with javadoc:org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository[] and javadoc:org.springframework.security.web.webauthn.management.UserCredentialRepository[].
  109. The default is to use in memory persistence, but JDBC persistence is support with javadoc:org.springframework.security.web.webauthn.management.JdbcPublicKeyCredentialUserEntityRepository[] and javadoc:org.springframework.security.web.webauthn.management.JdbcUserCredentialRepository[].
  110. To configure JDBC based persistence, expose the repositories as a Bean:
  111. [tabs]
  112. ======
  113. Java::
  114. +
  115. [source,java,role="primary"]
  116. ----
  117. @Bean
  118. JdbcPublicKeyCredentialUserEntityRepository jdbcPublicKeyCredentialRepository(JdbcOperations jdbc) {
  119. return new JdbcPublicKeyCredentialUserEntityRepository(jdbc);
  120. }
  121. @Bean
  122. JdbcUserCredentialRepository jdbcUserCredentialRepository(JdbcOperations jdbc) {
  123. return new JdbcUserCredentialRepository(jdbc);
  124. }
  125. ----
  126. Kotlin::
  127. +
  128. [source,kotlin,role="secondary"]
  129. ----
  130. @Bean
  131. fun jdbcPublicKeyCredentialRepository(jdbc: JdbcOperations): JdbcPublicKeyCredentialUserEntityRepository {
  132. return JdbcPublicKeyCredentialUserEntityRepository(jdbc)
  133. }
  134. @Bean
  135. fun jdbcUserCredentialRepository(jdbc: JdbcOperations): JdbcUserCredentialRepository {
  136. return JdbcUserCredentialRepository(jdbc)
  137. }
  138. ----
  139. ======
  140. If JDBC does not meet your needs, you can create your own implementations of the interfaces and use them by exposing them as a Bean similar to the example above.
  141. [[passkeys-configuration-pkccor]]
  142. === Custom PublicKeyCredentialCreationOptionsRepository
  143. The `PublicKeyCredentialCreationOptionsRepository` is used to persist the `PublicKeyCredentialCreationOptions` between requests.
  144. The default is to persist it the `HttpSession`, but at times users may need to customize this behavior.
  145. This can be done by setting the optional property `creationOptionsRepository` demonstrated in xref:./passkeys.adoc#passkeys-configuration[Configuration] or by exposing a `PublicKeyCredentialCreationOptionsRepository` Bean:
  146. [tabs]
  147. ======
  148. Java::
  149. +
  150. [source,java,role="primary"]
  151. ----
  152. @Bean
  153. CustomPublicKeyCredentialCreationOptionsRepository creationOptionsRepository() {
  154. return new CustomPublicKeyCredentialCreationOptionsRepository();
  155. }
  156. ----
  157. Kotlin::
  158. +
  159. [source,kotlin,role="secondary"]
  160. ----
  161. @Bean
  162. open fun creationOptionsRepository(): CustomPublicKeyCredentialCreationOptionsRepository {
  163. return CustomPublicKeyCredentialCreationOptionsRepository()
  164. }
  165. ----
  166. ======
  167. [[passkeys-register]]
  168. == Register a New Credential
  169. In order to use a passkey, a user must first https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential[Register a New Credential].
  170. Registering a new credential is composed of two steps:
  171. 1. Requesting the Registration Options
  172. 2. Registering the Credential
  173. [[passkeys-register-options]]
  174. === Request the Registration Options
  175. The first step in registration of a new credential is to request the registration options.
  176. In Spring Security, a request for the registration options is typically done using JavaScript and looks like:
  177. [NOTE]
  178. ====
  179. Spring Security provides a default registration page that can be used as a reference on how to register credentials.
  180. ====
  181. .Request for Registration Options
  182. [source,http]
  183. ----
  184. POST /webauthn/register/options
  185. X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
  186. ----
  187. The request above will obtain the registration options for the currently authenticated user.
  188. Since the challenge is persisted (state is changed) to be compared at the time of registration, the request must be a POST and include a CSRF token.
  189. .Response for Registration Options
  190. [source,json]
  191. ----
  192. {
  193. "rp": {
  194. "name": "SimpleWebAuthn Example",
  195. "id": "example.localhost"
  196. },
  197. "user": {
  198. "name": "user@example.localhost",
  199. "id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w",
  200. "displayName": "user@example.localhost"
  201. },
  202. "challenge": "q7lCdd3SVQxdC-v8pnRAGEn1B2M-t7ZECWPwCAmhWvc",
  203. "pubKeyCredParams": [
  204. {
  205. "type": "public-key",
  206. "alg": -8
  207. },
  208. {
  209. "type": "public-key",
  210. "alg": -7
  211. },
  212. {
  213. "type": "public-key",
  214. "alg": -257
  215. }
  216. ],
  217. "timeout": 300000,
  218. "excludeCredentials": [],
  219. "authenticatorSelection": {
  220. "residentKey": "required",
  221. "userVerification": "preferred"
  222. },
  223. "attestation": "none",
  224. "extensions": {
  225. "credProps": true
  226. }
  227. }
  228. ----
  229. [[passkeys-register-create]]
  230. === Registering the Credential
  231. After the registration options are obtained, they are used to create the credentials that are registered.
  232. To register a new credential, the application should pass the options to https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create[`navigator.credentials.create`] after base64url decoding the binary values such as `user.id`, `challenge`, and `excludeCredentials[].id`.
  233. The returned value can then be sent to the server as a JSON request.
  234. An example registration request can be found below:
  235. .Example Registration Request
  236. [source,http]
  237. ----
  238. POST /webauthn/register
  239. X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
  240. {
  241. "publicKey": { // <1>
  242. "credential": {
  243. "id": "dYF7EGnRFFIXkpXi9XU2wg",
  244. "rawId": "dYF7EGnRFFIXkpXi9XU2wg",
  245. "response": {
  246. "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAALraVWanqkAfvZZFYZpVEg0AEHWBexBp0RRSF5KV4vV1NsKlAQIDJiABIVggQjmrekPGzyqtoKK9HPUH-8Z2FLpoqkklFpFPQVICQ3IiWCD6I9Jvmor685fOZOyGXqUd87tXfvJk8rxj9OhuZvUALA",
  247. "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSl9RTi10SFJYRWVKYjlNcUNrWmFPLUdOVmlibXpGVGVWMk43Z0ptQUdrQSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
  248. "transports": [
  249. "internal",
  250. "hybrid"
  251. ]
  252. },
  253. "type": "public-key",
  254. "clientExtensionResults": {},
  255. "authenticatorAttachment": "platform"
  256. },
  257. "label": "1password" // <2>
  258. }
  259. }
  260. ----
  261. <1> The result of calling `navigator.credentials.create` with binary values base64url encoded.
  262. <2> A label that the user selects to have associated with this credential to help the user distinguish the credential.
  263. .Example Successful Registration Response
  264. [source,http]
  265. ----
  266. HTTP/1.1 200 OK
  267. {
  268. "success": true
  269. }
  270. ----
  271. [[passkeys-verify]]
  272. == Verifying an Authentication Assertion
  273. After xref:./passkeys.adoc#passkeys-register[] the passkey can be https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion[verified] (authenticated).
  274. Verifying a credential is composed of two steps:
  275. 1. Requesting the Verification Options
  276. 2. Verifying the Credential
  277. [[passkeys-verify-options]]
  278. === Request the Verification Options
  279. The first step in verification of a credential is to request the verification options.
  280. In Spring Security, a request for the verification options is typically done using JavaScript and looks like:
  281. [NOTE]
  282. ====
  283. Spring Security provides a default log in page that can be used as a reference on how to verify credentials.
  284. ====
  285. .Request for Verification Options
  286. [source,http]
  287. ----
  288. POST /webauthn/authenticate/options
  289. X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
  290. ----
  291. The request above will obtain the verification options.
  292. Since the challenge is persisted (state is changed) to be compared at the time of authentication, the request must be a POST and include a CSRF token.
  293. The response will contain the options for obtaining a credential with binary values such as `challenge` base64url encoded.
  294. .Example Response for Verification Options
  295. [source,json]
  296. ----
  297. {
  298. "challenge": "cQfdGrj9zDg3zNBkOH3WPL954FTOShVy0-CoNgSewNM",
  299. "timeout": 300000,
  300. "rpId": "example.localhost",
  301. "allowCredentials": [],
  302. "userVerification": "preferred",
  303. "extensions": {}
  304. }
  305. ----
  306. [[passkeys-verify-get]]
  307. === Verifying the Credential
  308. After the verification options are obtained, they are used to get a credential.
  309. To get a credential, the application should pass the options to https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create[`navigator.credentials.get`] after base64url decoding the binary values such as `challenge`.
  310. The returned value of `navigator.credentials.get` can then be sent to the server as a JSON request.
  311. Binary values such as `rawId` and `response.*` must be base64url encoded.
  312. An example authentication request can be found below:
  313. .Example Authentication Request
  314. [source,http]
  315. ----
  316. POST /login/webauthn
  317. X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
  318. {
  319. "id": "dYF7EGnRFFIXkpXi9XU2wg",
  320. "rawId": "dYF7EGnRFFIXkpXi9XU2wg",
  321. "response": {
  322. "authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
  323. "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
  324. "signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z",
  325. "userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo"
  326. },
  327. "clientExtensionResults": {},
  328. "authenticatorAttachment": "platform"
  329. }
  330. ----
  331. .Example Successful Authentication Response
  332. [source,http]
  333. ----
  334. HTTP/1.1 200 OK
  335. {
  336. "redirectUrl": "/", // <1>
  337. "authenticated": true // <2>
  338. }
  339. ----
  340. <1> The URL to redirect to
  341. <2> Indicates that the user is authenticated
  342. .Example Authentication Failure Response
  343. [source,http]
  344. ----
  345. HTTP/1.1 401 OK
  346. ----