mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-26 20:28:44 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			410 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			410 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| [[passkeys]]
 | |
| = Passkeys
 | |
| 
 | |
| Spring Security provides support for https://www.passkeys.com[passkeys].
 | |
| Passkeys are a more secure method of authenticating than passwords and are built using https://www.w3.org/TR/webauthn-3/[WebAuthn].
 | |
| 
 | |
| In order to use a passkey to authenticate, a user must first xref:servlet/authentication/passkeys.adoc#passkeys-register[Register a New Credential].
 | |
| After the credential is registered, it can be used to authenticate by xref:servlet/authentication/passkeys.adoc#passkeys-verify[verifying an authentication assertion].
 | |
| 
 | |
| [[passkeys-dependencies]]
 | |
| == Required Dependencies
 | |
| 
 | |
| To get started, add the `webauthn4j-core` dependency to your project.
 | |
| 
 | |
| [NOTE]
 | |
| ====
 | |
| 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[].
 | |
| ====
 | |
| 
 | |
| .Passkeys Dependencies
 | |
| [tabs]
 | |
| ======
 | |
| Maven::
 | |
| +
 | |
| [source,xml,role="primary",subs="verbatim,attributes"]
 | |
| ----
 | |
| <dependency>
 | |
|     <groupId>org.springframework.security</groupId>
 | |
|     <artifactId>spring-security-web</artifactId>
 | |
| </dependency>
 | |
| <dependency>
 | |
|     <groupId>com.webauthn4j</groupId>
 | |
|     <artifactId>webauthn4j-core</artifactId>
 | |
|     <version>{webauthn4j-core-version}</version>
 | |
| </dependency>
 | |
| ----
 | |
| 
 | |
| Gradle::
 | |
| +
 | |
| [source,groovy,role="secondary",subs="verbatim,attributes"]
 | |
| ----
 | |
| depenendencies {
 | |
|     implementation "org.springframework.security:spring-security-web"
 | |
|     implementation "com.webauthn4j:webauthn4j-core:{webauthn4j-core-version}"
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| [[passkeys-configuration]]
 | |
| == Configuration
 | |
| 
 | |
| The following configuration enables passkey authentication.
 | |
| 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].
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| SecurityFilterChain filterChain(HttpSecurity http) {
 | |
| 	// ...
 | |
| 	http
 | |
| 		// ...
 | |
| 		.formLogin(withDefaults())
 | |
| 		.webAuthn((webAuthn) -> webAuthn
 | |
| 			.rpName("Spring Security Relying Party")
 | |
| 			.rpId("example.com")
 | |
| 			.allowedOrigins("https://example.com")
 | |
| 			// optional properties
 | |
| 			.creationOptionsRepository(new CustomPublicKeyCredentialCreationOptionsRepository())
 | |
| 			.messageConverter(new CustomHttpMessageConverter())
 | |
| 		);
 | |
| 	return http.build();
 | |
| }
 | |
| 
 | |
| @Bean
 | |
| UserDetailsService userDetailsService() {
 | |
| 	UserDetails userDetails = User.withDefaultPasswordEncoder()
 | |
| 		.username("user")
 | |
| 		.password("password")
 | |
| 		.roles("USER")
 | |
| 		.build();
 | |
| 
 | |
| 	return new InMemoryUserDetailsManager(userDetails);
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Bean
 | |
| open fun filterChain(http: HttpSecurity): SecurityFilterChain {
 | |
| 	// ...
 | |
| 	http {
 | |
| 		webAuthn {
 | |
| 			rpName = "Spring Security Relying Party"
 | |
| 			rpId = "example.com"
 | |
| 			allowedOrigins = setOf("https://example.com")
 | |
| 			// optional properties
 | |
| 			creationOptionsRepository = CustomPublicKeyCredentialCreationOptionsRepository()
 | |
| 			messageConverter = CustomHttpMessageConverter()
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| @Bean
 | |
| open fun userDetailsService(): UserDetailsService {
 | |
| 	val userDetails = User.withDefaultPasswordEncoder()
 | |
| 		.username("user")
 | |
| 		.password("password")
 | |
| 		.roles("USER")
 | |
| 		.build()
 | |
| 	return InMemoryUserDetailsManager(userDetails)
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| 
 | |
| [[passkeys-configuration-persistence]]
 | |
| === JDBC & Custom Persistence
 | |
| 
 | |
| WebAuthn performs persistence with javadoc:org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository[] and javadoc:org.springframework.security.web.webauthn.management.UserCredentialRepository[].
 | |
| 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[].
 | |
| To configure JDBC based persistence, expose the repositories as a Bean:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| JdbcPublicKeyCredentialUserEntityRepository jdbcPublicKeyCredentialRepository(JdbcOperations jdbc) {
 | |
| 	return new JdbcPublicKeyCredentialUserEntityRepository(jdbc);
 | |
| }
 | |
| 
 | |
| @Bean
 | |
| JdbcUserCredentialRepository jdbcUserCredentialRepository(JdbcOperations jdbc) {
 | |
| 	return new JdbcUserCredentialRepository(jdbc);
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Bean
 | |
| fun jdbcPublicKeyCredentialRepository(jdbc: JdbcOperations): JdbcPublicKeyCredentialUserEntityRepository {
 | |
|     return JdbcPublicKeyCredentialUserEntityRepository(jdbc)
 | |
| }
 | |
| 
 | |
| @Bean
 | |
| fun jdbcUserCredentialRepository(jdbc: JdbcOperations): JdbcUserCredentialRepository {
 | |
|     return JdbcUserCredentialRepository(jdbc)
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| 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.
 | |
| 
 | |
| [[passkeys-configuration-pkccor]]
 | |
| === Custom PublicKeyCredentialCreationOptionsRepository
 | |
| 
 | |
| The `PublicKeyCredentialCreationOptionsRepository` is used to persist the `PublicKeyCredentialCreationOptions` between requests.
 | |
| The default is to persist it the `HttpSession`, but at times users may need to customize this behavior.
 | |
| This can be done by setting the optional property `creationOptionsRepository` demonstrated in xref:./passkeys.adoc#passkeys-configuration[Configuration] or by exposing a `PublicKeyCredentialCreationOptionsRepository` Bean:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| CustomPublicKeyCredentialCreationOptionsRepository creationOptionsRepository() {
 | |
| 	return new CustomPublicKeyCredentialCreationOptionsRepository();
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Bean
 | |
| open fun creationOptionsRepository(): CustomPublicKeyCredentialCreationOptionsRepository {
 | |
| 	return CustomPublicKeyCredentialCreationOptionsRepository()
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| [[passkeys-register]]
 | |
| == Register a New Credential
 | |
| 
 | |
| 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].
 | |
| 
 | |
| Registering a new credential is composed of two steps:
 | |
| 
 | |
| 1. Requesting the Registration Options
 | |
| 2. Registering the Credential
 | |
| 
 | |
| [[passkeys-register-options]]
 | |
| === Request the Registration Options
 | |
| 
 | |
| The first step in registration of a new credential is to request the registration options.
 | |
| In Spring Security, a request for the registration options is typically done using JavaScript and looks like:
 | |
| 
 | |
| [NOTE]
 | |
| ====
 | |
| Spring Security provides a default registration page that can be used as a reference on how to register credentials.
 | |
| ====
 | |
| 
 | |
| .Request for Registration Options
 | |
| [source,http]
 | |
| ----
 | |
| POST /webauthn/register/options
 | |
| X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
 | |
| ----
 | |
| 
 | |
| The request above will obtain the registration options for the currently authenticated user.
 | |
| 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.
 | |
| 
 | |
| .Response for Registration Options
 | |
| [source,json]
 | |
| ----
 | |
| {
 | |
|   "rp": {
 | |
|     "name": "SimpleWebAuthn Example",
 | |
|     "id": "example.localhost"
 | |
|   },
 | |
|   "user": {
 | |
|     "name": "user@example.localhost",
 | |
|     "id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w",
 | |
|     "displayName": "user@example.localhost"
 | |
|   },
 | |
|   "challenge": "q7lCdd3SVQxdC-v8pnRAGEn1B2M-t7ZECWPwCAmhWvc",
 | |
|   "pubKeyCredParams": [
 | |
|     {
 | |
|       "type": "public-key",
 | |
|       "alg": -8
 | |
|     },
 | |
|     {
 | |
|       "type": "public-key",
 | |
|       "alg": -7
 | |
|     },
 | |
|     {
 | |
|       "type": "public-key",
 | |
|       "alg": -257
 | |
|     }
 | |
|   ],
 | |
|   "timeout": 300000,
 | |
|   "excludeCredentials": [],
 | |
|   "authenticatorSelection": {
 | |
|     "residentKey": "required",
 | |
|     "userVerification": "preferred"
 | |
|   },
 | |
|   "attestation": "none",
 | |
|   "extensions": {
 | |
|     "credProps": true
 | |
|   }
 | |
| }
 | |
| ----
 | |
| 
 | |
| [[passkeys-register-create]]
 | |
| === Registering the Credential
 | |
| 
 | |
| After the registration options are obtained, they are used to create the credentials that are registered.
 | |
| 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`.
 | |
| 
 | |
| The returned value can then be sent to the server as a JSON request.
 | |
| An example registration request can be found below:
 | |
| 
 | |
| .Example Registration Request
 | |
| [source,http]
 | |
| ----
 | |
| POST /webauthn/register
 | |
| X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
 | |
| 
 | |
| {
 | |
|   "publicKey": { // <1>
 | |
|     "credential": {
 | |
|       "id": "dYF7EGnRFFIXkpXi9XU2wg",
 | |
|       "rawId": "dYF7EGnRFFIXkpXi9XU2wg",
 | |
|       "response": {
 | |
|         "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAALraVWanqkAfvZZFYZpVEg0AEHWBexBp0RRSF5KV4vV1NsKlAQIDJiABIVggQjmrekPGzyqtoKK9HPUH-8Z2FLpoqkklFpFPQVICQ3IiWCD6I9Jvmor685fOZOyGXqUd87tXfvJk8rxj9OhuZvUALA",
 | |
|         "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSl9RTi10SFJYRWVKYjlNcUNrWmFPLUdOVmlibXpGVGVWMk43Z0ptQUdrQSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
 | |
|         "transports": [
 | |
|           "internal",
 | |
|           "hybrid"
 | |
|         ]
 | |
|       },
 | |
|       "type": "public-key",
 | |
|       "clientExtensionResults": {},
 | |
|       "authenticatorAttachment": "platform"
 | |
|     },
 | |
|     "label": "1password" // <2>
 | |
|   }
 | |
| }
 | |
| ----
 | |
| <1> The result of calling `navigator.credentials.create` with binary values base64url encoded.
 | |
| <2> A label that the user selects to have associated with this credential to help the user distinguish the credential.
 | |
| 
 | |
| .Example Successful Registration Response
 | |
| [source,http]
 | |
| ----
 | |
| HTTP/1.1 200 OK
 | |
| 
 | |
| {
 | |
|   "success": true
 | |
| }
 | |
| ----
 | |
| 
 | |
| [[passkeys-verify]]
 | |
| == Verifying an Authentication Assertion
 | |
| 
 | |
| After xref:./passkeys.adoc#passkeys-register[] the passkey can be https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion[verified] (authenticated).
 | |
| 
 | |
| Verifying a credential is composed of two steps:
 | |
| 
 | |
| 1. Requesting the Verification Options
 | |
| 2. Verifying the Credential
 | |
| 
 | |
| [[passkeys-verify-options]]
 | |
| === Request the Verification Options
 | |
| 
 | |
| The first step in verification of a credential is to request the verification options.
 | |
| In Spring Security, a request for the verification options is typically done using JavaScript and looks like:
 | |
| 
 | |
| [NOTE]
 | |
| ====
 | |
| Spring Security provides a default log in page that can be used as a reference on how to verify credentials.
 | |
| ====
 | |
| 
 | |
| .Request for Verification Options
 | |
| [source,http]
 | |
| ----
 | |
| POST /webauthn/authenticate/options
 | |
| X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
 | |
| ----
 | |
| 
 | |
| The request above will obtain the verification options.
 | |
| 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.
 | |
| 
 | |
| The response will contain the options for obtaining a credential with binary values such as `challenge` base64url encoded.
 | |
| 
 | |
| .Example Response for Verification Options
 | |
| [source,json]
 | |
| ----
 | |
| {
 | |
|   "challenge": "cQfdGrj9zDg3zNBkOH3WPL954FTOShVy0-CoNgSewNM",
 | |
|   "timeout": 300000,
 | |
|   "rpId": "example.localhost",
 | |
|   "allowCredentials": [],
 | |
|   "userVerification": "preferred",
 | |
|   "extensions": {}
 | |
| }
 | |
| ----
 | |
| 
 | |
| [[passkeys-verify-get]]
 | |
| === Verifying the Credential
 | |
| 
 | |
| After the verification options are obtained, they are used to get a credential.
 | |
| 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`.
 | |
| 
 | |
| The returned value of `navigator.credentials.get` can then be sent to the server as a JSON request.
 | |
| Binary values such as `rawId` and `response.*` must be base64url encoded.
 | |
| An example authentication request can be found below:
 | |
| 
 | |
| .Example Authentication Request
 | |
| [source,http]
 | |
| ----
 | |
| POST /login/webauthn
 | |
| X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
 | |
| 
 | |
| {
 | |
|   "id": "dYF7EGnRFFIXkpXi9XU2wg",
 | |
|   "rawId": "dYF7EGnRFFIXkpXi9XU2wg",
 | |
|   "response": {
 | |
|     "authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
 | |
|     "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
 | |
|     "signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z",
 | |
|     "userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo"
 | |
|   },
 | |
|   "clientExtensionResults": {},
 | |
|   "authenticatorAttachment": "platform"
 | |
| }
 | |
| ----
 | |
| 
 | |
| .Example Successful Authentication Response
 | |
| [source,http]
 | |
| ----
 | |
| HTTP/1.1 200 OK
 | |
| 
 | |
| {
 | |
|   "redirectUrl": "/", // <1>
 | |
|   "authenticated": true // <2>
 | |
| }
 | |
| ----
 | |
| <1> The URL to redirect to
 | |
| <2> Indicates that the user is authenticated
 | |
| 
 | |
| .Example Authentication Failure Response
 | |
| [source,http]
 | |
| ----
 | |
| HTTP/1.1 401 OK
 | |
| 
 | |
| ----
 |