In my last post I did the needful and installed code coverage to tell me if my unit tests were testing enough. You might argue that they might not be very good tests, and I’ve never said that coverage reporting addresses quality, but there are now nearly enough tests.
What I should do now is actually provide some proper functionality! The good thing about this, is we’ll be able to test it all properly too!
Database Entities
Now that we’re getting serious, let’s have some more entities. We’ll need to add Client and Code entities in addition to Users.
@Entity class User( var username: String, var password: String, @Id @GeneratedValue var id: Long? = null) @Entity class Client( var name: String, var secret: String, var redirectUrl: String, @Id @GeneratedValue var id: Long? = null) @Entity class Code( var code: String, @ManyToOne var client: Client, @ManyToOne var user: User, @Id @GeneratedValue var id: Long? = null)
Clients represent the different user interfaces that may seek to authenticate users, and Codes represent the one time codes generated by the Auth service to enable fetching a JWT. Clients should have a fixed redirect URL so that we can be certain we know where we’re sending the one time codes to. The client itself should provide this value as a kind of proof that it is the real deal. I’ve left out obvious properties here like the user’s name and other properties you might add, because our code coverage will report any fields that we’re not actually using as uncovered lines. Neat!
Note the use of @ManyToOne to link to a Code’s Client and User. Amazingly simple.
We’ll also want a couple of smart repositories to hold our queries.
interface UserRepository : CrudRepository<User, Long> { fun findByUsername(username: String): User? } interface ClientRepository : CrudRepository<Client, Long> { } interface CodeRepository: CrudRepository<Code, Long> { fun findByCode(code: String): Code? }
This is really neat. We’re using the magic methods from spring boot’s database engine to do simple queries, but by defining these interfaces we can specify how the implementation should work. My IDE (Intellij) will tell me if the interface I’m writing doesn’t match the actual implementation. For Clients we’re going to use findById which is a concrete method of CrudRepository so there’s no need for a function definition here. Now that we have these repositories we can inject them as dependencies with zero hassle!
And lastly we’ll add some initial data to our app, since this is all development work. (the database is only in-memory while the app runs!) See how easily we inject the repositories!
@Configuration class AuthConfiguration { @Bean fun databaseInitializer( userRepository: UserRepository, clientRepository: ClientRepository, codeRepository: CodeRepository ) = ApplicationRunner { val user = userRepository.save(User("testuser", "testpassword")) val client = clientRepository.save(Client("Website", "secret", "https://localhost:3000/login")) codeRepository.save(Code("1234", client, user)) } }
A JWT Service
Next, Let’s create a service to help us generate JWTs
we can use the https://fusionauth.io package to construct a JWT, add claims to it (the user ID and the client ID just now), and sign it with our secret (a little bit of a hack, let’s remember to use a config option for that later!)
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("io.fusionauth:fusionauth-jwt:3.5.3") developmentOnly("org.springframework.boot:spring-boot-devtools")
package com.chrisyoung.auth import io.fusionauth.jwt.domain.JWT import io.fusionauth.jwt.hmac.HMACSigner import java.time.ZonedDateTime class JwtService { fun createAccessToken(client: Client, user: User): String { val secret = "secret" val signer = HMACSigner.newSHA256Signer(secret) val jwt = JWT() jwt.addClaim("clientId", client.id) jwt.addClaim("userId", user.id) jwt.setIssuedAt(ZonedDateTime.now()) jwt.setIssuer("auth-demo") return JWT.getEncoder().encode(jwt, signer) } }
Controllers
Now we want some functional controllers so we can go ahead and get that token from the front-end! We’re so close!
We’ll want to fill out the AuthController with the power to actually create those Codes
Our GET method should fetch the client from the database (and handle not-found scenario with an automatic response! Isn’t spring boot awesome!). We’ll add all the relevant information to the page for rendering the HTML (yes this route is traditional HTML rendered server-side)
@Controller class AuthController( private val entityManager: EntityManager, private val clientRepository: ClientRepository, private val codeRepository: CodeRepository ) { private val chars = ('0'..'z').toList().toTypedArray() @GetMapping("/authorize") fun authorizeForm( model: Model, request: HttpServletRequest, @RequestParam(name = "state") state: String, @RequestParam(name = "clientId") clientId: Long ): String { val client = clientRepository.findById(clientId).orElse(null) client ?: return ":notfound" request.session.getAttribute("user") as User? ?: return "redirect:/login" model["title"] = "Authorize" model["clientId"] = clientId model["clientName"] = client.name model["state"] = state return "authorizeForm" }
Our form should display all the useful information to the user, and keep the relevant data in the hidden fields for the POST. Clicking the submit button is going to POST the data to the same route and create our secret code!
{{> _header }} <div class="jumbotron"> <form method="post" action="/authorize"> <input type="hidden" name="clientId" value="{{clientId}}"/> <input type="hidden" name="state" value="{{state}}"> <h2>Authorise {{ clientName }}?</h2> <br/> <input class="btn btn-primary" type="submit" value="Authorise"> <button class="btn btn-danger" type="button" onclick="history.back()">Reject</button> </form> </div> {{> _footer }}
Our POST method should again fetch the client from the database, get the current user from the session, and create a new Code entity record before redirecting to the frontend with the string version of the Code.
@PostMapping("/authorize") fun authorize( model: Model, request: HttpServletRequest, @RequestParam(name = "state") state: String, @RequestParam(name = "clientId") clientId: Long ): String { val client: Client? = clientRepository.findById(clientId).orElse(null) client ?: return "notfound:" val user = request.session.getAttribute("user") as User? ?: return "redirect:/login" val secret = (1..32).map { chars.random() }.joinToString("") val code = codeRepository.save(Code(secret, client, user)) model["title"] = "Authorized" model["redirectUrl"] = format("%s?state=%s&code=%s", client.redirectUrl, state, code.code) return "authorize" }
The most significant parts here are the generation of the unique code (and saving to the database) and the creation of the redirect URL (if all goes well above) from the URL stored on the client record, the state value (a value provided by the client application at runtime representing the unique state at request time, which should be compared when receiving the code to prove the client application can trust the redirect request), and the one time code itself.
The rendered HTML just contains a javascript redirect to the url provided from the controller, and perhaps a nice UI to display while the user waits. You can tell I’m a back-end engineer as I’ve not bothered making it look nice.
{{> _header }} Redirecting <script type="text/javascript"> window.location.href=decodeURI("{{redirectUrl}}") </script> {{> _footer }}
And here’s some concise tests for the authorize form:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class AuthControllerTests(@Autowired val restTemplate: TestRestTemplate) { @Test fun `Assert that the form is displayed`() { val entity = restTemplate.getForEntity<String>("/authorize?state=1234&clientId=2") assertThat(entity.statusCode).isEqualTo(HttpStatus.OK) } @Test fun `Assert that the redirect happens if not logged in`() { val entity = restTemplate.postForEntity<String>("/authorize?state=1234&clientId=2") assertThat(entity.statusCode).isEqualTo(HttpStatus.FOUND) } @Test fun `Assert that wrong clientId fails`() { val entity = restTemplate.getForEntity<String>("/authorize?state=1234&clientId=1") assertThat(entity.statusCode).isEqualTo(HttpStatus.NOT_FOUND) } @Test fun `Assert that wrong clientId fails POST`() { val entity = restTemplate.postForEntity<String>("/authorize?state=1234&clientId=1") assertThat(entity.statusCode).isEqualTo(HttpStatus.NOT_FOUND) } @Test fun `Assert that the redirect happens`() { val loginResponse = restTemplate.postForEntity<String>("/login?username=testuser&password=testpassword") assertThat(loginResponse.statusCode).isEqualTo(HttpStatus.OK) val sessionCookie = loginResponse.headers.get("Set-Cookie")?.get(0)?.split(';')?.get(0); val headers = HttpHeaders(); headers.add("Cookie", sessionCookie) val entity = HttpEntity<Any>(headers) val result = restTemplate.exchange<String>("/authorize?state=5678&clientId=2", HttpMethod.POST, entity) assertThat(result.statusCode).isEqualTo(HttpStatus.OK) } }
Of course we should add some more assertions, but this just demonstrates how easy it is to perform the test.
Next, our TokenController will be a rest endpoint that will actually give us the JWT we’ve been waiting for! All the controller will do is fetch the code from the database (or show not found error) and use the JWT service to create a JWT and return a JSON response with the accessToken and (still dummy) refreshToken and the info about the user.
data class TokenRequest( val code: String ) data class TokenResponse( val accessToken: String, val refreshToken: String, val user: User ) @CrossOrigin(origins = ["http://localhost:3000"]) @RestController class TokenController(val codeRepository: CodeRepository) { @PostMapping("/token") fun createToken(@RequestBody(required = true) tokenRequest: TokenRequest): ResponseEntity<Any> { val code = codeRepository.findByCode(tokenRequest.code) ?: return ResponseEntity.notFound().build() val token = JwtService().createAccessToken(code.client, code.user) return ResponseEntity.ok().body(TokenResponse(token, "refresh-token", code.user)) } }
Note the use of the @CrossOrigin annotation to allow browser AJAX requests from different domains (in local development from a port 3000, React’s favourite), and the use of @RestController rather than just @Controller. This does some nice magic around providing a JSON response.
I quite enjoy the easy definition of what a request and a response look like, so that we can strictly type the controller method. A Token Request needs only the one-time code, and a Token Response will only have the JWT (access token), a refresh token, and the user object so we have some info about the user in the response body. We’ll use that later to display a welcome message.
No templates here, it’s a REST controller! Here’s a little test though:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class TokenControllerTests(@Autowired val restTemplate: TestRestTemplate) { @Test fun `Get a token successfully`() { val headers = HttpHeaders() headers.contentType = MediaType.APPLICATION_JSON val request = HttpEntity(TokenRequest("1234"), headers) val entity = restTemplate.postForEntity<String>("/token", request) assertThat(entity.statusCode).isEqualTo(HttpStatus.OK) } @Test fun `Bad code`() { val headers = HttpHeaders() headers.contentType = MediaType.APPLICATION_JSON val request = HttpEntity(TokenRequest("wrong"), headers) val entity = restTemplate.postForEntity<String>("/token", request) assertThat(entity.statusCode).isEqualTo(HttpStatus.NOT_FOUND) } }
Again only testing the response codes, but roughly I know I haven’t broken the controller entirely.
Let’s also add a Verify endpoint so the client application can check the JWT is (still) valid:
@CrossOrigin(origins = ["http://localhost:3000"]) @RestController class VerifyController { @GetMapping("/verify") fun verifiy( @RequestHeader(name = "Authorization") auth: String ): JWT? { val token = auth.replace("Bearer ", "") return JwtService().verify(token) } }
I’ll also throw in Twitter Bootstrap for some slightly Nicer styles:
<html> <head> <title>{{ title }}</title> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"> </head> <body>
Here’s the login screen:
And the Authorize screen:
My code coverage is doing alright too
In the next blog, I’ll show you the react app I’ve created to log in using these routes.