Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add java version to auth tutorial #233

Merged
merged 1 commit into from
May 17, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 119 additions & 14 deletions _posts/tutorials/2017-08-24-javalin-auth-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ permalink: /tutorials/auth-example
github: https://github.com/javalin/javalin-samples/tree/main/javalin5/javalin-auth-example
summarytitle: Secure your endpoints!
summary: Learn how to secure your endpoints using Javalin's AccessManager interface
language: kotlin
language: ["java", "kotlin"]
---

## Dependencies
Expand All @@ -30,7 +30,51 @@ 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:

```kotlin
{% capture java %}
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", "alice@alice.kt"),
randomId(), new User("Bob", "bob@bob.kt"),
randomId(), new User("Carol", "carol@carol.kt"),
randomId(), new User("Dave", "dave@dave.kt")
);
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();
}
}
{% endcapture %}
{% capture kotlin %}
import io.javalin.http.Context
import io.javalin.http.bodyAsClass
import java.util.*
Expand Down Expand Up @@ -69,7 +113,8 @@ object UserController {
private fun randomId() = UUID.randomUUID().toString()

}
```
{% endcapture %}
{% include macros/docsSnippet.html java=java kotlin=kotlin %}

<small><em>We're using `!!` to convert nullables to non-nullables.
If `{user-id}` is missing or `users[id]` returns null, we'll get a NullPointerException
Expand All @@ -81,14 +126,44 @@ This is done by implementing the `Role` interface from `io.javalin.security.Role
We'll define three roles, one for "anyone", one for permission to read user-data,
and one for permission to write user-data.

```kotlin

{% capture java %}
enum Role implements RouteRole { ANYONE, USER_READ, USER_WRITE }
{% endcapture %}
{% capture kotlin %}
enum class Role : RouteRole { ANYONE, USER_READ, USER_WRITE }
```
{% endcapture %}
{% include macros/docsSnippet.html java=java kotlin=kotlin %}

## Setting up the API
Now that we have roles, we can implement our endpoints:

```kotlin
{% capture java %}
import io.javalin.Javalin;
import io.javalin.apibuilder.ApiBuilder.*;

public class Main {

public static void main(String[] args) {

Javalin app = Javalin.create(config -> {
config.accessManager(Auth::accessManager);
}).routes(() -> {
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);
}
}
{% endcapture %}
{% capture kotlin %}
import io.javalin.apibuilder.ApiBuilder.*
import io.javalin.Javalin

Expand All @@ -110,7 +185,8 @@ fun main() {
}.start(7070)

}
```
{% endcapture %}
{% include macros/docsSnippet.html java=java kotlin=kotlin %}

A role has now been given to every endpoint:
* `ANYONE` can `getAllUserIds`
Expand All @@ -131,8 +207,20 @@ The rules for our access manager are also simple:
* When endpoint has another role set and the request has matching credentials, the request will be handled
* Else we ignore the request and send `401 Unauthorized` back to the client

This translates nicely into Kotlin:
```kotlin
This translates nicely into code:
{% capture java %}
public static void accessManager(Handler handler, Context ctx, Set<RouteRole> permittedRoles) {
if (permittedRoles.contains(Role.ANYONE)) {
return handler.handle(ctx);
}
if (ctx.userRoles().stream().anyMatch(permittedRoles::contains)) {
return handler.handle(ctx);
}
ctx.header(Header.WWW_AUTHENTICATE, "Basic");
throw new UnauthorizedResponse();
}
{% endcapture %}
{% capture kotlin %}
fun accessManager(handler: Handler, ctx: Context, permittedRoles: Set<RouteRole>) {
when {
permittedRoles.contains(Role.ANYONE) -> handler.handle(ctx)
Expand All @@ -143,30 +231,47 @@ fun accessManager(handler: Handler, ctx: Context, permittedRoles: Set<RouteRole>
}
}
}
```
{% endcapture %}
{% include macros/docsSnippet.html java=java kotlin=kotlin %}

### Extracting user-roles from the context
There is no `ctx.userRoles` concept in Javalin, so we need to implement it.
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:

```kotlin
{% capture java %}
public record Pair(String user, String password) {}
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)
);
{% endcapture %}
{% capture kotlin %}
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)
)
```
{% endcapture %}
{% include macros/docsSnippet.html java=java kotlin=kotlin %}

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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Basic_authentication_scheme)
and using them as keys for the `userRoleMap`:

```kotlin
{% capture java %}
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());
}
{% endcapture %}
{% capture kotlin %}
private val Context.userRoles: List<Role>
get() = this.basicAuthCredentials()?.let { (username, password) ->
userRolesMap[Pair(username, password)] ?: listOf()
} ?: listOf()
```
{% endcapture %}
{% include macros/docsSnippet.html java=java kotlin=kotlin %}

<small><em>
When using basic auth, credentials are transferred as plain text (although base64-encoded).
Expand Down