Easy SSO for Vert.x with Keycloak

TL;DR:

In this blog post you’ll learn:

  • How to im­ple­ment Sin­gle Sign-​on with OpenID Con­nect
  • How to use Key­cloak’s OpenID Dis­cov­ery to infer OpenID provider con­fig­u­ra­tion
  • How to ob­tain user in­for­ma­tion
  • How to check for au­tho­riza­tion
  • How to call a Bearer pro­tected ser­vice with an Ac­cess Token
  • How to im­ple­ment a form based lo­gout

Hello Blog

This is my first post in the Vert.x Blog and I must admit that up until now I have never used Vert.x in a real project. “Why are you here?”, you might ask… Well I cur­rently have two main hob­bies, learn­ing new things and se­cur­ing apps with Key­cloak. So a few days ago, I stum­bled upon the In­tro­duc­tion to Vert.x video se­ries on youtube by Deven Phillips and I was im­me­di­ately hooked. Vert.x was a new thing for me, so the next log­i­cal step was to fig­ure out how to se­cure a Vert.x app with Key­cloak.

For this ex­am­ple I build a small web app with Vert.x that shows how to im­ple­ment Sin­gle Sign-​on (SSO) with Key­cloak and OpenID Con­nect, ob­tain in­for­ma­tion about the cur­rent user, check for roles, call bearer pro­tected ser­vices and prop­erly han­dling lo­gout.

Keycloak

Key­cloak is a Open Source Iden­tity and Ac­cess Man­age­ment so­lu­tion which pro­vides sup­port for OpenID Con­nect based Singe-​Sign on, among many other things. I briefly looked for ways to se­cur­ing a Vert.x app with Key­cloak and quickly found an older Vert.x Key­cloak in­te­gra­tion ex­am­ple in this very blog. Whilst this is a good start for be­gin­ners, the ex­am­ple con­tains a few is­sues, e.g.:

  • It uses hard­coded OpenID provider con­fig­u­ra­tion
  • Fea­tures a very sim­plis­tic in­te­gra­tion (for the sake of sim­plic­ity)
  • No user in­for­ma­tion used
  • No lo­gout func­tion­al­ity is shown

That some­how nerd­sniped me a bit and so it came that, after a long day of con­sult­ing work, I sat down to cre­ate an ex­am­ple for a com­plete Key­cloak in­te­gra­tion based on Vert.x OpenID Con­nect / OAuth2 Sup­port.

So let’s get started!

Keycloak Setup

To se­cure a Vert.x app with Key­cloak we of course need a Key­cloak in­stance. Al­though Key­cloak has a great get­ting started guide I wanted to make it a bit eas­ier to put every­thing to­gether, there­fore I pre­pared a local Key­cloak docker con­tainer as de­scribed here that you can start eas­ily, which comes with all the re­quired con­fig­u­ra­tion in place.

The pre­con­fig­ured Key­cloak realm named vertx con­tains a demo-client for our Vert.x web app and a set of users for test­ing.

docker run \
  -it \
  --name vertx-keycloak \
  --rm \
  -e KEYCLOAK_USER=admin \
  -e KEYCLOAK_PASSWORD=admin \
  -e KEYCLOAK_IMPORT=/tmp/vertx-realm.json \
  -v $PWD/vertx-realm.json:/tmp/vertx-realm.json \
  -p 8080:8080 \
  quay.io/keycloak/keycloak:9.0.0

Vert.x Web App

The sim­ple web app con­sists of a sin­gle Verticle, runs on http://localhost:8090 and pro­vides a few routes with pro­tected re­sources. You can find the com­plete ex­am­ple here.

The web app con­tains the fol­low­ing routes with han­dlers:

  • / - The un­pro­tected index page
  • /protected - The pro­tected page, which shows a greet­ing mes­sage, users need to login to ac­cess pages be­neath this path.
  • /protected/user - The pro­tected user page, which shows some in­for­ma­tion about the user.
  • /protected/admin - The pro­tected admin page, which shows some in­for­ma­tion about the admin, only users with role admin can ac­cess this page.
  • /protected/userinfo - The pro­tected user­info page, ob­tains user in­for­ma­tion from the bearer token pro­tected user­info end­point in Key­cloak.
  • /logout - The pro­tected lo­gout re­source, which trig­gers the user lo­gout.

Running the app

To run the app, we need to build our app via:

cd keycloak-vertx
mvn clean package

This cre­ates a runnable jar, which we can run via:

java -jar target/*.jar

Note, that you need to start Key­cloak, since our app will try to fetch con­fig­u­ra­tion from Key­cloak.

If the ap­pli­ca­tion is run­ning, just browse to: http://localhost:8090/.

An ex­am­ple in­ter­ac­tion with the app can be seen in the fol­low­ing gif:

Vert.x Keycloak Integration Demo

Router, SessionStore and CSRF Protection

We start the con­fig­u­ra­tion of our web app by cre­at­ing a Router where we can add cus­tom han­dler func­tions for our routes. To prop­erly han­dle the au­then­ti­ca­tion state we need to cre­ate a SessionStore and at­tach it to the Router. The SessionStore is used by our OAuth2/OpenID Con­nect in­fra­struc­ture to as­so­ciate au­then­ti­ca­tion in­for­ma­tion with a ses­sion. By the way, the SessionStore can also be clus­tered if you need to dis­trib­ute the server-​side state.

Note that if you want to keep your server state­less but still want to sup­port clus­ter­ing, then you could pro­vide your own im­ple­men­ta­tion of a SessionStore which stores the ses­sion in­for­ma­tion as an en­crypted cookie on the Client.

Router router = Router.router(vertx);

// Store session information on the server side
SessionStore sessionStore = LocalSessionStore.create(vertx);
SessionHandler sessionHandler = SessionHandler.create(sessionStore);
router.route().handler(sessionHandler);

In order to pro­tected against CSRF at­tacks it is good prac­tice to pro­tect HTML forms with a CSRF token. We need this for our lo­gout form that we’ll see later.

To do this we con­fig­ure a CSRFHandler and add it to our Router:

// CSRF handler setup required for logout form
String csrfSecret = "zwiebelfische";
CSRFHandler csrfHandler = CSRFHandler.create(csrfSecret);
router.route().handler(ctx -> {
            // Ensures that the csrf token request parameter is available for the CsrfHandler
            // after the logout form was submitted.
            // See "Handling HTML forms" https://vertx.io/docs/vertx-core/java/#_handling_requests
            ctx.request().setExpectMultipart(true);
            ctx.request().endHandler(v -> csrfHandler.handle(ctx));
        }
);

Keycloak Setup via OpenID Connect Discovery

Our app is reg­is­tered as a con­fi­den­tial OpenID Con­nect client with Au­tho­riza­tion Code Flow in Key­cloak, thus we need to con­fig­ure client_id and client_secret. Con­fi­den­tial clients are typ­i­cally used for server-​side web ap­pli­ca­tions, where one can se­curely store the client_secret. You can find out more aboutThe dif­fer­ent Client Ac­cess Types in the Key­cloak doc­u­men­ta­tion.

Since we don’t want to con­fig­ure things like OAuth2 / OpenID Con­nect End­points our­selves, we use Key­cloak’s OpenID Con­nect dis­cov­ery end­point to infer the nec­es­sary Oauth2 / OpenID Con­nect end­point URLs.

String hostname = System.getProperty("http.host", "localhost");
int port = Integer.getInteger("http.port", 8090);
String baseUrl = String.format("http://%s:%d", hostname, port);
String oauthCallbackPath = "/callback";

OAuth2ClientOptions clientOptions = new OAuth2ClientOptions()
    .setFlow(OAuth2FlowType.AUTH_CODE)
    .setSite(System.getProperty("oauth2.issuer", "http://localhost:8080/auth/realms/vertx"))
    .setClientID(System.getProperty("oauth2.client_id", "demo-client"))
    .setClientSecret(System.getProperty("oauth2.client_secret", "1f88bd14-7e7f-45e7-be27-d680da6e48d8"));

KeycloakAuth.discover(vertx, clientOptions, asyncResult -> {

    OAuth2Auth oauth2Auth = asyncResult.result();

    if (oauth2Auth == null) {
        throw new RuntimeException("Could not configure Keycloak integration via OpenID Connect Discovery Endpoint. Is Keycloak running?");
    }

    AuthHandler oauth2 = OAuth2AuthHandler.create(oauth2Auth, baseUrl + oauthCallbackPath)
        .setupCallback(router.get(oauthCallbackPath))
        // Additional scopes: openid for OpenID Connect
        .addAuthority("openid");

    // session handler needs access to the authenticated user, otherwise we get an infinite redirect loop
    sessionHandler.setAuthProvider(oauth2Auth);

    // protect resources beneath /protected/* with oauth2 handler
    router.route("/protected/*").handler(oauth2);

    // configure route handlers
    configureRoutes(router, webClient, oauth2Auth);
});

getVertx().createHttpServer().requestHandler(router).listen(port);

Route handlers

We con­fig­ure our route han­dlers via configureRoutes:

private void configureRoutes(Router router, WebClient webClient, OAuth2Auth oauth2Auth) {

    router.get("/").handler(this::handleIndex);

    router.get("/protected").handler(this::handleGreet);
    router.get("/protected/user").handler(this::handleUserPage);
    router.get("/protected/admin").handler(this::handleAdminPage);

    // extract discovered userinfo endpoint url
    String userInfoUrl =  ((OAuth2AuthProviderImpl)oauth2Auth).getConfig().getUserInfoPath();
    router.get("/protected/userinfo").handler(createUserInfoHandler(webClient, userInfoUrl));

    router.post("/logout").handler(this::handleLogout);
}

The index han­dler ex­poses an un­pro­tected re­source:

private void handleIndex(RoutingContext ctx) {
    respondWithOk(ctx, "text/html", "<h1>Welcome to Vert.x Keycloak Example</h1><br><a href=\"/protected\">Protected</a>");
}

Extract User Information from the OpenID Connect ID Token

Our app ex­poses a sim­ple greet­ing page which shows some in­for­ma­tion about the user and pro­vides links to other pages.

The user greet­ing han­dler is pro­tected by the Key­cloak OAuth2 / OpenID Con­nect in­te­gra­tion. To show in­for­ma­tion about the cur­rent user, we first need to call the ctx.user() method to get an user ob­ject we can work with. To ac­cess the OAuth2 token in­for­ma­tion, we need to cast it to OAuth2TokenImpl.

We can ex­tract the user in­for­ma­tion like the user­name from the IDToken ex­posed by the user ob­ject via user.idToken().getString("preferred_username"). Note, there are many more claims like (name, email, give­nanme, fam­i­ly­name etc.) avail­able. The OpenID Con­nect Core Spec­i­fi­ca­tion con­tains a list of avail­able claims.

We also gen­er­ate a list with links to the other pages which are sup­ported:

private void handleGreet(RoutingContext ctx) {

    OAuth2TokenImpl oAuth2Token = (OAuth2TokenImpl) ctx.user();

    String username = oAuth2Token.idToken().getString("preferred_username");

    String greeting = String.format("<h1>Hi %s @%s</h1><ul>" +
            "<li><a href=\"/protected/user\">User Area</a></li>" +
            "<li><a href=\"/protected/admin\">Admin Area</a></li>" +
            "<li><a href=\"/protected/userinfo\">User Info (Remote Call)</a></li>" +
            "</ul>", username, Instant.now());

    String logoutForm = createLogoutForm(ctx);

    respondWithOk(ctx, "text/html", greeting + logoutForm);
}

The user page han­dler shows in­for­ma­tion about the cur­rent user:

private void handleUserPage(RoutingContext ctx) {

    OAuth2TokenImpl user = (OAuth2TokenImpl) ctx.user();

    String username = user.idToken().getString("preferred_username");
    String displayName = oAuth2Token.idToken().getString("name");

    String content = String.format("<h1>User Page: %s (%s) @%s</h1><a href=\"/protected\">Protected Area</a>",
                                   username, displayName, Instant.now());
    respondWithOk(ctx, "text/html", content);
}

Authorization: Checking for Required Roles

Our app ex­poses a sim­ple admin page which shows some in­for­ma­tion for ad­mins, which should only be vis­i­ble for ad­mins. Thus we re­quire that users must have the admin realm role in Key­cloak to be able to ac­cess the admin page.

This is done via a call to user.isAuthorized("realm:admin", cb). The han­dler func­tion cb ex­poses the re­sult of the au­tho­riza­tion check via the AsyncResult<Boolean> res. If the cur­rent user has the admin role then the re­sult is true oth­er­wise false:

private void handleAdminPage(RoutingContext ctx) {

    OAuth2TokenImpl user = (OAuth2TokenImpl) ctx.user();

    // check for realm-role "admin"
    user.isAuthorized("realm:admin", res -> {

        if (!res.succeeded() || !res.result()) {
            respondWith(ctx, 403, "text/html", "<h1>Forbidden</h1>");
            return;
        }

        String username = user.idToken().getString("preferred_username");

        String content = String.format("<h1>Admin Page: %s @%s</h1><a href=\"/protected\">Protected Area</a>",
                                        username, Instant.now());
        respondWithOk(ctx, "text/html", content);
    });
}

Call Services protected with Bearer Token

Often we need to call other ser­vices from our web app that are pro­tected via Bearer Au­then­ti­ca­tion. This means that we need a valid access token to ac­cess a re­source pro­vided on an­other server.

To demon­strate this we use Key­cloak’s /userinfo end­point as a straw man to demon­strate back­end calls with a bearer token.

We can ob­tain the cur­rent valid access token via user.opaqueAccessToken(). Since we use a WebClient to call the pro­tected end­point, we need to pass the access token via the Authorization header by call­ing bearerTokenAuthentication(user.opaqueAccessToken()) in the cur­rent HttpRequest ob­ject:

private Handler<RoutingContext> createUserInfoHandler(WebClient webClient, String userInfoUrl) {

    return (RoutingContext ctx) -> {

        OAuth2TokenImpl user = (OAuth2TokenImpl) ctx.user();

        URI userInfoEndpointUri = URI.create(userInfoUrl);
        webClient
            .get(userInfoEndpointUri.getPort(), userInfoEndpointUri.getHost(), userInfoEndpointUri.getPath())
            // use the access token for calls to other services protected via JWT Bearer authentication
            .bearerTokenAuthentication(user.opaqueAccessToken())
            .as(BodyCodec.jsonObject())
            .send(ar -> {

                if (!ar.succeeded()) {
                    respondWith(ctx, 500, "application/json", "{}");
                    return;
                }

                JsonObject body = ar.result().body();
                respondWithOk(ctx, "application/json", body.encode());
            });
    };
}

Handle logout

Now that we got a work­ing SSO login with au­tho­riza­tion, it would be great if we would allow users to lo­gout again. To do this we can lever­age the built-​in OpenID Con­nect lo­gout func­tion­al­ity which can be called via oAuth2Token.logout(cb).

The han­dler func­tion cb ex­poses the re­sult of the lo­gout ac­tion via the AsyncResult<Void> res. If the lo­gout was suc­cess­full we destory our ses­sion via ctx.session().destroy() and redi­rect the user to the index page.

The lo­gout form is gen­er­ated via the createLogoutForm method.

As men­tioned ear­lier, we need to pro­tect our lo­gout form with a CSRF token to pre­vent CSRF at­tacks.

Note: If we had end­points that would ac­cept data sent to the server, then we’d need to guard those end­points with an CSRF token as well.

We need to ob­tain the gen­er­ated CSRFToken and ren­der it into a hid­den form input field that’s trans­fered via HTTP POST when the lo­gout form is sub­mit­ted:

private void handleLogout(RoutingContext ctx) {

    OAuth2TokenImpl oAuth2Token = (OAuth2TokenImpl) ctx.user();
    oAuth2Token.logout(res -> {

        if (!res.succeeded()) {
            // the user might not have been logged out, to know why:
            respondWith(ctx, 500, "text/html", String.format("<h1>Logout failed %s</h1>", res.cause()));
            return;
        }

        ctx.session().destroy();
        ctx.response().putHeader("location", "/?logout=true").setStatusCode(302).end();
    });
}

private String createLogoutForm(RoutingContext ctx) {

    String csrfToken = ctx.get(CSRFHandler.DEFAULT_HEADER_NAME);

    return "<form action=\"/logout\" method=\"post\">"
            + String.format("<input type=\"hidden\" name=\"%s\" value=\"%s\">", CSRFHandler.DEFAULT_HEADER_NAME, csrfToken)
            + "<button>Logout</button></form>";
}

Some ad­di­tional plumb­ing:

private void respondWithOk(RoutingContext ctx, String contentType, String content) {
    respondWith(ctx, 200, contentType, content);
}

private void respondWith(RoutingContext ctx, int statusCode, String contentType, String content) {
    ctx.request().response() //
            .putHeader("content-type", contentType) //
            .setStatusCode(statusCode)
            .end(content);
}

More examples

This con­cludes the Key­cloak in­te­gra­tion ex­am­ple.

Check out the com­plete ex­am­ple in keycloak-​vertx Ex­am­ples Repo.

Thank you for your time, stay tuned for more up­dates! If you want to learn more about Key­cloak, feel free to reach out to me. You can find me via thomas­da­ri­mont on twit­ter.

Happy Hack­ing!

Next post

Eclipse Vert.x 3.9.0 released!

New features include fluent queries in SQL clients, a new Redis client, an updated Kafka client, an improved Future API, and many more things.

Read more
Previous post

Eclipse Vert.x 3.8.5

This version is a minor bug fix release addressing issues found in Vert.x 3.8.4. We would like to thank you all for reporting these issues.

Read more
Related posts

JWT Authorization for Vert.x with Keycloak

In this blog post, you'll learn about JWT foundations, protect routes with JWT Authorization, JWT encoded tokens, and RBAC with Keycloak

Read more

Some Rest with Vert.x

This post is part of the Introduction to Vert.x series. Let’s go a bit further this time and develop a CRUD-ish application

Read more

Real-time bidding with Websockets and Vert.x

The expectations of users for interactivity with web applications have changed over the past few years. Users during bidding in auction no longer want to press the refresh button.

Read more