Module 4: Authentication
September 1, 2022 ยท View on GitHub
In this module, you will add a passwordless authentication to the application.
WebAuthn Authentication Overview
Expand for high level overview of WebAuthn authentication
Authentication
Initiate a authentication ceremony:
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder()
.username(Optional.of("alice"))
.build());
String json = jsonMapper.writeValueAsString(request);
return json;
Validate the response:
String responseJson = /* ... */;
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc =
jsonMapper.readValue(responseJson, new TypeReference<PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>>() {
});
try {
AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder()
.request(request)
.response(pkc)
.build());
if (result.isSuccess()) {
return result.getUsername();
}
} catch (AssertionFailedException e) { /* ... */ }
throw new RuntimeException("Authentication failed");
Authenticate the user:
// Manually authenticate user
String username = result.right().get().getRegistrations().iterator().next().getUserIdentity().getName();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDetails u = userDetailsService.loadUserByUsername(username);
Authentication newAuth = new UsernamePasswordAuthenticationToken(u, auth.getCredentials(),u.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(newAuth);
return ResponseEntity.status(HttpStatus.OK).body(result.right().get());
Implementation overview
This section will walk through how to expose the authentication REST endpoints, manually authenticate a user in Spring Security, and consume the authentication endpoints in the frontend.
Add authentication REST endpoints
Let's expose two REST endpoints to start and finish the WebAuthn authentication operation.
Step by step instructions
- Open the
./src/main/java/com/example/demo/WebAuthnController.javaclass in your editor - Add the following imports in the import section
import com.example.demo.data.AssertionRequestWrapper; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; - In the
WebAuthnControllerclass add a reference to theUserDetailsService. This is the service which loads user-specific data.@Autowired private UserDetailsService userDetailsService; - Add the
/authenticateand/authenticate/finishREST endpoints@PostMapping("/authenticate") public ResponseEntity<AssertionRequestWrapper> startAuthentication(@RequestParam("username") Optional<String> username) { Either<List<String>, AssertionRequestWrapper> result = webAuthnServer.startAuthentication(username); if (result.isRight()) { return ResponseEntity.status(HttpStatus.OK).body(result.right().get()); } else { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, result.left().get().toString()); } } @PostMapping("/authenticate/finish") public ResponseEntity<WebAuthnServer.SuccessfulAuthenticationResult> finishAuthentication( @RequestBody String responseJson) { Either<List<String>, WebAuthnServer.SuccessfulAuthenticationResult> result = webAuthnServer .finishAuthentication(responseJson); if (result.isRight()) { // Manually authenticate user String username = result.right().get().getRegistrations().iterator().next().getUserIdentity().getName(); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); UserDetails u = userDetailsService.loadUserByUsername(username); Authentication newAuth = new UsernamePasswordAuthenticationToken(u, auth.getCredentials(), u.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(newAuth); return ResponseEntity.status(HttpStatus.OK).body(result.right().get()); } else { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, result.left().get().toString()); } }
Make the authentication REST endpoints accessible
Step by step instructions
- Open the
./src/main/java/com/example/demo/WebSecurityConfig.javain your editor - Modify the
antMatchers()method to include the new REST endpoints and javascript libraries so that they are accessible to users before they sign in.
http
.authorizeRequests()
.antMatchers("/", "/home", "/css/**", "/images/**", "/js/**", "/lib/**", "/authenticate", "/authenticate/finish").permitAll()
.anyRequest().authenticated()
Integrate WebAuthn API into application
- Open the
./src/main/resources/templates/login.htmltemplate - Add the following code in the header section
<meta th:name="_csrf" th:content="${_csrf.token}" /> <meta th:name="_csrf_header" th:content="${_csrf.headerName}" /> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script> <script type="module" src="/lib/fetch/fetch-3.0.0.js"></script> <script src="/lib/base64js/base64js-1.3.0.min.js"></script> <script src="/js/base64url.js"></script> <script src="/js/webauthn.js"></script> <script> function setStatus(statusText, success) { $('#status').text(statusText); if (success) { $('#status').removeClass('error'); } else { $('#status').addClass('error'); } } function submitResponse(url, requestId, response) { console.log('submitResponse', url, requestId, response); var token = $("meta[name='_csrf']").attr("content"); const body = { requestId, credential: response, }; console.log('body', JSON.stringify(body)); return fetch(url, { method: 'POST', headers: { 'X-CSRF-TOKEN': token }, body: JSON.stringify(body), }).then(response => response.json()); ; } function authenticate() { const username = ""; const token = $("meta[name='_csrf']").attr("content"); return fetch('/authenticate', { method: 'POST', headers: { 'X-CSRF-TOKEN': token }, }) .then(response => response.json()) .then(function (request) { console.log('request succeeded with JSON response', request) return webauthn.getAssertion(request.publicKeyCredentialRequestOptions) .then(webauthn.responseToObject) .then(function (publicKeyCredential) { console.log("publicKeyCredential ", publicKeyCredential); url = '/authenticate/finish'; return submitResponse(url, request.requestId, publicKeyCredential); }) .catch(error => { throw error; }) ; }) .then(data => { console.log("Success!"); window.location.href = "/account" console.log(data); return data; }) .catch(error => { console.log('authenticate', error); setStatus(error.message, false); }) ; } </script> - Add the Passwordless sign in button to the body section.
<hr /> <h2 class="form-signin-heading">Passwordless sign in</h2> <p>Sign in with your previously registered security key</p> <p id="status"></p> <p><button onclick="authenticate()">Passwordless Sign in</button><br /> </p> <p><a href="/index" th:href="@{/home}">Back to home page</a></p>
Validate passwordless authentication functionality and integration into the application
-
Run the web app
Locally
mvn clean package spring-boot:runAzure Cloud Shell
mvn clean package azure-webapp:deploy -
Open the web app URL local https://localhost:8443/ or in the cloud.
-
Sign in to the web app
-
Register a security key
-
Sign out
-
Click the Passwordless sign in button to sign in without typing a username or password
-
You should signed in and redirected to the account page
Recap
- Expose authentication endpoints
- Make sure the authentication endpoints are accessible to anonymous users
- Consume the authentication endpoints in the frontend and call the WebAuthn API
- After the assertion is verified, authenticate the user's session
Next
Congratulations! You've completed the passwordless workshop. We hope that this time has been valuable for you. For further learning on this topic, please see Yubico's developer site.
Please remember to run through the Clean up steps to ensure you decommission all resources spun up during the workshop today.
Thank you for participating in this workshop!