Dependencies
First, we need to create a Maven project with some dependencies: (→ Tutorial)
<dependencies>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin-bundle</artifactId>
<version>6.4.0</version>
</dependency>
</dependencies>
Creating controllers
We need something worth protecting. Let’s pretend we have a very important API for manipulating a user database. We make a controller-object with some dummy data and CRUD operations:
- Java
- Kotlin
import io.javalin.http.Context;
import java.util.*;
public class UserController {
public record User(String name, String email) {}
private static final Map<String, User> users;
static {
var tempMap = Map.of(
randomId(), new User("Alice", "[email protected]"),
randomId(), new User("Bob", "[email protected]"),
randomId(), new User("Carol", "[email protected]"),
randomId(), new User("Dave", "[email protected]")
);
users = new HashMap<>(tempMap);
}
public static void getAllUserIds(Context ctx) {
ctx.json(users.keySet());
}
public static void createUser(Context ctx) {
users.put(randomId(), ctx.bodyAsClass(User.class));
}
public static void getUser(Context ctx) {
ctx.json(users.get(ctx.pathParam("userId")));
}
public static void updateUser(Context ctx) {
users.put(ctx.pathParam("userId"), ctx.bodyAsClass(User.class));
}
public static void deleteUser(Context ctx) {
users.remove(ctx.pathParam("userId"));
}
private static String randomId() {
return UUID.randomUUID().toString();
}
}
import io.javalin.http.Context
import io.javalin.http.bodyAsClass
import java.util.*
object UserController {
private data class User(val name: String = "", val email: String = "")
private val users = hashMapOf(
randomId() to User(name = "Alice", email = "[email protected]"),
randomId() to User(name = "Bob", email = "[email protected]"),
randomId() to User(name = "Carol", email = "[email protected]"),
randomId() to User(name = "Dave", email = "[email protected]")
)
fun getAllUserIds(ctx: Context) {
ctx.json(users.keys)
}
fun createUser(ctx: Context) {
users[randomId()] = ctx.bodyAsClass()
}
fun getUser(ctx: Context) {
ctx.json(users[ctx.pathParam("userId")]!!)
}
fun updateUser(ctx: Context) {
users[ctx.pathParam("userId")] = ctx.bodyAsClass()
}
fun deleteUser(ctx: Context) {
users.remove(ctx.pathParam("userId"))
}
private fun randomId() = UUID.randomUUID().toString()
}
Creating roles
Now that we have our functionality, we need to define a set of roles for our system.
This is done by implementing the RouteRole
interface from io.javalin.security.RouteRole
.
We’ll define three roles, one for “anyone”, one for permission to read user-data,
and one for permission to write user-data.
- Java
- Kotlin
import io.javalin.security.RouteRole;
enum Role implements RouteRole { ANYONE, USER_READ, USER_WRITE }
import io.javalin.security.RouteRole
enum class Role : RouteRole { ANYONE, USER_READ, USER_WRITE }
Setting up the API
Now that we have roles, we can implement our endpoints:
- Java
- Kotlin
import io.javalin.Javalin;
import static io.javalin.apibuilder.ApiBuilder.*;
public class Main {
public static void main(String[] args) {
Javalin app = Javalin.create(config -> {
config.router.mount(router -> {
router.beforeMatched(Auth::handleAccess);
}).apiBuilder(() -> {
get("/", ctx -> ctx.redirect("/users"), Role.ANYONE);
path("users", () -> {
get(UserController::getAllUserIds, Role.ANYONE);
post(UserController::createUser, Role.USER_WRITE);
path("{userId}", () -> {
get(UserController::getUser, Role.USER_READ);
patch(UserController::updateUser, Role.USER_WRITE);
delete(UserController::deleteUser, Role.USER_WRITE);
});
});
});
}).start(7070);
}
}
import io.javalin.apibuilder.ApiBuilder.*
import io.javalin.Javalin
fun main() {
Javalin.create {
it.router.mount {
it.beforeMatched(Auth::handleAccess)
}.apiBuilder {
get("/", { ctx -> ctx.redirect("/users") }, Role.ANYONE)
path("users") {
get(UserController::getAllUserIds, Role.ANYONE)
post(UserController::createUser, Role.USER_WRITE)
path("{userId}") {
get(UserController::getUser, Role.USER_READ)
patch(UserController::updateUser, Role.USER_WRITE)
delete(UserController::deleteUser, Role.USER_WRITE)
}
}
}
}.start(7070)
}
A role has now been given to every endpoint:
ANYONE
cangetAllUserIds
USER_READ
cangetUser
USER_WRITE
cancreateUser
,updateUser
anddeleteUser
Now, all that remains is to implement the access-management (Auth::handleAccess
).
Implementing auth
The rules for our access manager are simple:
- When endpoint has
ApiRole.ANYONE
, all requests will be handled - When endpoint has another role set and the request has matching credentials, the request will be handled
- Otherwise, we stop the request and send
401 Unauthorized
back to the client
This translates nicely into code:
- Java
- Kotlin
public static void handleAccess(Context ctx) {
var permittedRoles = ctx.routeRoles();
if (permittedRoles.contains(Role.ANYONE)) {
return; // anyone can access
}
if (userRoles(ctx).stream().anyMatch(permittedRoles::contains)) {
return; // user has role required to access
}
ctx.header(Header.WWW_AUTHENTICATE, "Basic");
throw new UnauthorizedResponse();
}
fun handleAccess(ctx: Context) {
val permittedRoles = ctx.routeRoles()
when {
permittedRoles.contains(Role.ANYONE) -> return
ctx.userRoles.any { it in permittedRoles } -> return
else -> {
ctx.header(Header.WWW_AUTHENTICATE, "Basic")
throw UnauthorizedResponse();
}
}
}
Extracting user-roles from the context
There is no ctx.userRoles
or userRoles(ctx)
built into Javalin, so we need to implement something.
First we need a user-table. We’ll create a map(Pair<String, String>, Set<Role>)
where keys are
username+password in cleartext (please don’t do this for a real service), and values are user-roles:
- Java
- Kotlin
record Pair(String a, String b) {}
private static final Map<Pair, List<Role>> userRolesMap = Map.of(
new Pair("alice", "weak-1234"), List.of(Role.USER_READ),
new Pair("bob", "weak-123456"), List.of(Role.USER_READ, Role.USER_WRITE)
);
// pair is a native kotlin class
private val userRolesMap = mapOf(
Pair("alice", "weak-1234") to listOf(Role.USER_READ),
Pair("bob", "weak-123456") to listOf(Role.USER_READ, Role.USER_WRITE)
)
Now that we have a user-table, we need to authenticate the requests.
We do this by getting the username+password from the Basic-auth-header
and using them as keys for the userRoleMap
:
- Java
- Kotlin
public static List<Role> getUserRoles(Context ctx) {
return Optional.ofNullable(ctx.basicAuthCredentials())
.map(credentials -> userRolesMap.getOrDefault(new Pair(credentials.getUsername(), credentials.getPassword()), List.of()))
.orElse(List.of());
}
private val Context.userRoles: List<Role>
get() = this.basicAuthCredentials()?.let { (username, password) ->
userRolesMap[Pair(username, password)] ?: listOf()
} ?: listOf()
When using basic auth, credentials are transferred as plain text (although base64-encoded). Remember to enable SSL if you’re using basic-auth for a real service.
Conclusion
That’s it! You now have a secure REST API with three roles.