Imagine we have a resource server, exposing a REST API, written using Vertx. It is working well, but we want to go further: we would like to identify our users, both to let only authorized ones access our app and to log their activity.
The big world of authentication and authorization
We ask Google, and find tons of examples for tons of libraries and protocols: SAML, WS-Federation, OAuth, OpenId. Quite interesting, but very frightening, isn’t it?
The need
Let’s focus on our need, which is quite simple: users can use the API provided that they are authorized to do so.
We don’t want to manage our own users database. It’s not fun (At least for me). Moreover, for our users, it would be another account to create and password to remind. What about reusing Facebook or Twitter as the identity provider?
What do social providers have in common? OAuth2 (Well, they probably have many more in common). Simply put, we will get a token from the identity provider, and we will set the Authorization header of HTTP request with it.
Good, it seems that it fulfills our needs, as it is implemented by almost all major identity providers. But there is a problem: their implementations of OAuth2 are quite different, although there are SDK to help. We want to federate users from different providers, and assign them authorizations whatever the provider; how can we do that? The solution is to use a specialized service, such as Auth0.
The login workflow is out of scope of this post. You can either create a login UI or call directly the Auth0 authentication API. |
Auth0 to the rescue
Auth0 is an Identity Hub as a Service, which support a large number of identity providers and protocols. Among all these protocols (I never imagined that there were so many protocols!), our target: OAuth2.
We have two main choices for our use case when authenticating on the identity provider (via Auth0):
-
get an access token; the resource server must call the authorization server (auth0) to validate it and get further details (such as the user profile)
-
get an id token using OpenId Connect, this token being a (signed) Json Web Token, aka JWS; in this case, the resource server just have to know the secret key to validate the token, and is totally agnostic of the authorization server. The token can contain arbitrary data by claiming scopes (e.g. openid, email, name, roles, app_metadata …). For more, see the authentication API doc.
For our case, let’s use JWT.
Implementing the solution
Vertx provides a way to handle JWT, it is as easy as adding a handler:
AuthHandler authHandler = JWTAuthHandler.create(new Auth0TokenVerifier("myapp", applicationId, applicationSecret));
authHandler.addAuthority("create:user"); (2)
router.route("/api/*").handler(authHandler); (1)
1 | add the handler |
2 | add authority: only users with the permission "create:user" will be able to use the API |
The JWTAuthHandler
will parse the Authorization header, extract the token, and pass it to a AuthProvider
. So next, we have to provide that AuthProvider
, to parse the token and extract the authorizations.
For our purpose, the JWT will contain the authorizations in a claim object named app_metadata
:
{
...
"app_metadata": {
"myapp": {
"permissions": ["create:user", "update:user","read:user"]
}
}
...
}
public class Auth0TokenVerifier implements AuthProvider {
private final String audience;
private final byte[] secret;
private final String appName;
// ...
@Override
public void authenticate(JsonObject credentials, Handler<AsyncResult<io.vertx.ext.auth.User>> resultHandler) {
String token = credentials.getString("jwt");
try {
Map<String, Object> tokenInfo = verifyToken(token);
String name = (String) tokenInfo.getOrDefault("name", "unknown"); // TODO fail?
List<String> permissions = findPermissions(tokenInfo);
User user = new User(name, permissions);
resultHandler.handle(Future.succeededFuture(user));
} catch (Exception e) {
resultHandler.handle(Future.failedFuture(e));
}
}
private Map<String, Object> verifyToken(String token) throws NoSuchAlgorithmException, InvalidKeyException, IOException, SignatureException, JWTVerifyException {
JWTVerifier verifier = new JWTVerifier(secret, audience); (1)
return verifier.verify(token);
}
private List<String> findPermissions(Map<String, Object> tokenInfo) {(2)
List<String> permissions = new ArrayList<>();
Map<String, Object> appMetadata = (Map<String, Object>) tokenInfo.get("app_metadata");
if (appMetadata == null) {
return permissions;
}
Map<String, Object> translatorApp = (Map<String, Object>) appMetadata.get(appName);
if (translatorApp == null) {
return permissions;
}
List<String> appPermissions = (List<String>) translatorApp.get("permissions");
if (appPermissions == null) {
return permissions;
}
permissions.addAll(appPermissions);
return permissions;
}
}
1 | Token verification: the token is decoded and the signature is verified. You can debug a JWT here. |
2 | Permissions are extracted from the token. Auth0 provides a app_metadata object to store arbitrary data such as roles. |
Finally, we write a User
class extending AbstractUser
to handle the authorities:
public class User extends AbstractUser {
private final String name;
private final List<String> permissions;
public User(String name, List<String> permissions) {
this.name = name;
this.permissions = permissions;
}
@Override
protected void doIsPermitted(String permission, Handler<AsyncResult<Boolean>> resultHandler) {
resultHandler.handle(Future.succeededFuture(permissions.contains(permission)));
}
// ...
}
The final word
Well done. Now, we have an API which is totally agnostic of the identity provider, and protected.
Feel free to comment and give feedback!
At the time of writing, Auth0 has a free plan allowing up to 7000 users and 2 social identity providers. You should try it! |