This post explains how you can secure your Micronaut app with FreshBooks. Using FreshBooks’ OAuth 2.0 implementation, I’ll show you how to use the Authorization Code grant to authenticate with FreshBooks.
FreshBooks is an invoice and accounting Software-as-a-Service (SaaS) for small business.
Using Micronaut OAuth 2.0
We’ll be using Micronaut and it’s OAuth 2.0 libraries. Micronaut is a dependency injection framework for Java. If that sounds a lot like Spring Framework, well, you’re close. Both play in the same space. The difference is Micronaut resolves its bean injections at compile time, instead of at runtime as Spring Framework does. The result is a much faster startup time and a smaller memory footprint, mainly because there is no runtime reflection or proxying.
This can be a big deal in cloud environments where fast start up times are important for rapid scaling and elasticity. To put some numbers on this, a Hello World web app written with Spring Framework took 10 seconds to start on my mid-2012 MackBook Pro. The same app rewritten using Micronaut took less than two seconds to start. Mind-blowing, eh?
FreshBooks OAuth 2.0
In contrast to the examples in the Micronaut Security Guides, FreshBooks does not use OpenID, nor does it return a JWT (as of Jan 2021). Rather, it returns the basic OAuth 2.0 access and refresh tokens in the standard JSON format:
{
"access_token": "lots_of_letters_and_numbers",
"token_type": "bearer",
"expires_in": 43200,
"refresh_token": "more_letters_and_numbers",
"created_at": 1586211696
}
Part of our solution here will be to retrieve the user’s name and email address, much like what OpenID does. We’ll cobble this together in a JWT and give it to the browser as a cookie.
The Solution
If you want to skip to the end result, you can see the working code in my GitHub repo.
Getting Started
Begin by installing Micronaut. I’ll assume you are using macOS, you use Homebrew as your package manager, and you have version 11 of the Java JDK installed with JAVA_HOME configured correctly:
$ brew install micronaut
If you prefer SDKMAN! over Homebrew, just go:
$ sdk install micronaut
I also assume you know your way around Gradle. Micronaut installs the Gradle wrapper when you perform the next step.
Create the Application
Now the fun part – writing the application. With Micronaut you can create the application with either the Command Line Interface or Micronaut Launch.
Command Line Interface
$ mn create-app ca.airspeed.freshbooks-auth --features lombok,views-thymeleaf,security-oauth2,security-jwt
That command created the folder freshbooks-auth
with the standard Java application folder structure , and a default package name of ca.airspeed
. It also added some needed dependencies that I’ll mention shortly.
Micronaut Launch
Browse to https://micronaut.io/launch/, and fill out the form as shown below. Make sure you select the four Features shown:

Views
For this demo we’ll use ThymeLeaf as the template engine to render views. Creating the app with the views-thymeleaf
feature added it as a dependency in build.gradle
:
dependencies {
...
implementation("io.micronaut.views:micronaut-views-thymeleaf")
...
}
Micronaut Security and Lombok Dependencies
I’m a Lombok fan boy, and I love the simplicity it brings to your code. We specified it as a Feature along with a couple Micronaut Security Features. The final dependencies block in build.gradle should look like this:
dependencies {
annotationProcessor("org.projectlombok:lombok")
annotationProcessor("io.micronaut.security:micronaut-security-annotations")
implementation("io.micronaut:micronaut-validation")
implementation("io.micronaut:micronaut-runtime")
implementation("javax.annotation:javax.annotation-api")
implementation("io.micronaut:micronaut-http-client")
compileOnly("org.projectlombok:lombok")
implementation("io.micronaut.security:micronaut-security-jwt")
implementation("io.micronaut.security:micronaut-security-oauth2")
implementation("io.micronaut.views:micronaut-views-thymeleaf")
runtimeOnly("ch.qos.logback:logback-classic")
}
Since both Lombok and Micronaut use annotation processors, it is important we have Lombok as the first annotation processor in the dependencies block. The Command Line Interface and Micronaut Launch take care of this for us.
Your gradle.properties
already specifies the Micronaut version to use:
micronautVersion=2.2.3
Setup A Client Application in FreshBooks
If you don’t have a FreshBooks account, set one up on their sign up page. Then, head over to their Developer Portal and click on “Create an App”.

Give it a name and description. Most importantly, specify a Redirect URI of https://localhost/oauth/callback/freshbooks
. You just need the one Redirect URI:

Click the green arrow button, and click the green Save button at the top of the screen.
Back at the Developer Portal, click the down arrow next to the app you just added. FreshBooks has assigned the Client ID and Client Secret:

Create a setenv
file in the project root, copy the Client ID and Client Secret from the Developer Portal and use them as values for OAUTH_CLIENT_ID
and OAUTH_CLIENT_SECRET
respectively.
Make up a 256-bit JWT key (I used RandomKeygen), and use this for the value of JWT_SECRET
. Your setenv file should look like this:
export OAUTH_CLIENT_ID=1e70blahblahabunchoflettersandnumbersblahblah2b
export OAUTH_CLIENT_SECRET=3e25ablahblahmoreoflettersandnumbersbf45e7931
export JWT_SECRET=Piyadayadaevenmorelettersandnumberskwk
Configure Our Application
Next up, edit src/main/resources/application.yml
so it looks like this:
micronaut:
application:
name: freshbooksAuth
security:
authentication: cookie
oauth2:
clients:
freshbooks:
client-id: '${OAUTH_CLIENT_ID}'
client-secret: '${OAUTH_CLIENT_SECRET}'
authorization:
url: https://auth.freshbooks.com/service/auth/oauth/authorize
scopes: admin:all:legacy
token:
url: https://api.freshbooks.com/auth/oauth/token
auth-method: client-secret-post
revocation:
url: https://api.freshbooks.com/auth/oauth/revoke
auth-method: client-secret-post
endpoints:
logout:
get-allowed: true
reject-not-found: false
token:
jwt:
signatures:
secret:
generator:
secret: '${JWT_SECRET}'
jws-algorithm: HS256
generator:
access-token:
expiration: 43200
ssl:
enabled: true
build-self-signed: true
port: 443
Here is what we just did:
- We’ve defined an oauth2 client arbitrarily named
freshbooks
. That client name must be the same as the last part of the path you specified in the Redirect URI in FreshBooks. - We supplied the Client ID and Client Secret we set up in FreshBooks.
- We defined the URL our app will use to request an authorization code, and the scopes we are asking for. Those scopes appear to be pre-defined by FreshBooks, and are the only ones I’ve got to work.
- We defined the URL our browser will use so it can obtain an access token.
- We’re allowing a GET on our app’s
/logout
endpoint. - We configured the signature our app will use to sign the JWT token it creates, and defined its expiration to match the FreshBooks access token.
- We enabled self-signed TLS (SSL) certificate. FreshBooks requires us to use https, and rightly so.
Create the file src/main/resources/views/home.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<title>Home</title>
</head>
<body>
<h1>Micronaut - FreshBooks example</h1>
<h2 th:if="${security}">Welcome <span th:text="${security.attributes.get('firstName')}"></span>!</h2>
<h2 th:unless="${security}">username: Anonymous</h2>
<nav>
<ul>
<li th:unless="${security}"><a href="/oauth/login/freshbooks">Enter</a></li>
<li th:if="${security}"><a href="/oauth/logout/freshbooks">Logout</a></li>
</ul>
</nav>
</body>
</html>
The URI /oauth/login/freshbooks
will resolve to the micronaut.security.oauth2.clients.freshbooks.authorization.url
setting we made earlier in application.yml.
Home
Create the file src/main/java/ca/airspeed/HomeController.java:
package ca.airspeed;
import static io.micronaut.security.rules.SecurityRule.IS_ANONYMOUS;
import java.util.HashMap;
import java.util.Map;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.views.View;
@Controller
public class HomeController {
@Secured(IS_ANONYMOUS)
@View("home")
@Get
public Map<String, Object> index() {
return new HashMap<>();
}
}
The @View
annotation resolves to the views/home.html
file we created.
User Details
Write a client class to let us get user details and to logout using FreshBooks’s API. We’ll use Micronaut’s declarative HTTP client because it is so simple:
package ca.airspeed;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Flowable;
import io.reactivex.Single;
@Header(name="User-Agent", value="Micronaut")
@Client("https://api.freshbooks.com")
public interface FreshBooksApiClient {
@Get("/auth/api/v1/users/me")
Flowable<FreshBooksUser> getUser(@Header("Authorization") String authorization);
@Post("/auth/oauth/revoke")
Single<HttpResponse<String>> revokeToken(@Body RevokePayload body);
}
Code a simple src/main/java/ca/airspeed/FreshBooksUser.java
class to hold the response from that getUser()
call:
package ca.airspeed;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import io.micronaut.core.annotation.Introspected;
import lombok.Getter;
import lombok.Setter;
@Introspected
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
@Getter
@Setter
public class FreshBooksUser {
@JsonProperty("response")
private JsonNode response;
}
The response body in an https://api.freshbooks.com/auth/api/v1/users/me
call is huge. For simplicity we’ll use a JsonNode object to let us fish out the few items we want.
Now, the real meat and potatoes of User Details. Code this src/main/java/ca/airspeed/FreshBooksUserDetailsMapper.java
class:
package ca.airspeed;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Named;
import javax.inject.Singleton;
import org.reactivestreams.Publisher;
import com.fasterxml.jackson.databind.JsonNode;
import edu.umd.cs.findbugs.annotations.Nullable;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper;
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Named("freshbooks")
@Singleton
@Slf4j
@RequiredArgsConstructor
public class FreshBooksUserDetailsMapper implements OauthUserDetailsMapper {
private final FreshBooksApiClient apiClient;
@Override
public Publisher<UserDetails> createUserDetails(TokenResponse tokenResponse) {
return Publishers.just(new UnsupportedOperationException());
}
@Override
public Publisher<AuthenticationResponse> createAuthenticationResponse(TokenResponse tokenResponse,
@Nullable State state) {
return apiClient.getUser(format("Bearer %s", tokenResponse.getAccessToken()))
.map(user -> {
Map<String, Object> attributes = new HashMap<>();
attributes.put(ACCESS_TOKEN_KEY, tokenResponse.getAccessToken());
attributes.put(REFRESH_TOKEN_KEY, tokenResponse.getRefreshToken());
JsonNode responseNode = user.getResponse();
String email = responseNode.path("email").asText();
attributes.put("email", email);
attributes.put("firstName", responseNode.path("first_name").asText());
attributes.put("lastName", responseNode.path("last_name").asText());
return new UserDetails(email, asList(tokenResponse.getScope().split(":")), attributes);
});
}
}
The @Named annotation links up with the micronaut.security.oauth2.clients.freshbooks
block in application.yml
. Since it implements OAuthUserDetailsMapper, it gets called when we have retrieved an access token (the TokenResponse
) from FreshBooks.
Notice we’re calling the getUser()
method in the FreshBooksApiClient
class we coded earlier. We’re passing the access token as an argument so that our HTTP client will stick this in the Authorization header when it calls FreshBooks’ /users/me
endpoint.
We’re essentially building an AuthenticationResponse
that will be available in the ThymeLeaf template as the security
object.
Logging Out
To logout we’ll use our FreshBooksApiClient
class to make a call to FreshBooks’ /auth/oauth/revoke
endpoint. This will revoke both our access token and refresh token.
First, code this src/main/java/ca/airspeed/RevokePayload.java class:
package ca.airspeed;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.core.annotation.Introspected;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
@Introspected
public class RevokePayload {
@JsonProperty("client_id")
private String clientId;
@JsonProperty("client_secret")
private String clientSecret;
private String token;
}
Second, code this src/main/java/ca/airspeed/FreshBooksLogoutController.java class:
package ca.airspeed;
import static io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper.ACCESS_TOKEN_KEY;
import static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED;
import java.net.URISyntaxException;
import java.util.Map;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.handlers.LogoutHandler;
import io.micronaut.security.oauth2.configuration.OauthClientConfiguration;
import io.reactivex.Single;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Controller
@RequiredArgsConstructor
@Slf4j
public class FreshBooksLogoutController {
private final OauthClientConfiguration clientConfiguration;
private final FreshBooksApiClient freshBooksApiClient;
private final LogoutHandler logoutHandler;
@Get("/oauth/logout/freshbooks")
@Secured(IS_AUTHENTICATED)
public Single<HttpResponse<?>> logout(HttpRequest<?> request,
Authentication auth) throws URISyntaxException {
Map<String, Object> attributes = auth.getAttributes();
log.debug("Revoking the FreshBooks access token.");
RevokePayload body = RevokePayload.builder()
.clientId(clientConfiguration.getClientId())
.clientSecret(clientConfiguration.getClientSecret())
.token((String) attributes.get(ACCESS_TOKEN_KEY))
.build();
return freshBooksApiClient.revokeToken(body)
.map(httpResponse -> {
log.debug("Response {} - {}.", httpResponse.getStatus().getCode(), httpResponse.getStatus().getReason());
return logoutHandler.logout(request);
});
}
}
The OauthClientConfiguration
and LogoutHandler
are beans that Micronaut made and are available for injection here.
The @Get("/oauth/logout/freshbooks")
annotation matches up with the logout link in views/home.html
.
With the @Secured(IS_AUTHENTICATED)
, we’re restricting this to authenticated users, obviously.
Though we re not using OpenID, we rely on the default redirect-uri
of “/” from openid.end-session
.
Run the Application
Let’s kick the tires and light the fires. First, set those shell environment variables we defined earlier. Make sure setenv
is executable (chmod 755 setenv
), and go:
$ . ./setenv
Second, build and run the application:
$ ./gradlew build run
Finally, browse to https://localhost, accept the warnings about insecurity and a self-signed cert, and follow along.
If you used the code from my GitHub repository, you’ll notice a lot of DEBUG-level messages on the console. Obviously you would not use these in a production environment because of the sensitive information they have such as access tokens.
Turn on the developer tools in your browser, and you’ll see a “JWT” cookie. Copy its value and use https://jwt.io to see its constituent parts. Notice it disappears when you click the logout link.
Wrapping Up
There you have it. This is how you secure your app with FreshBooks using the OAuth 2.0 authorization code grant.
We recommend the installation of Micronaut via [SDKMan](https://sdkman.io) instead of homebrew for MacOS