How to Implement Nuxt.js/Vue.js OAuth2 Authentication With an External REST-API Server (based on Vert.x/Kotlin) and Keycloak 🐬

johanneslichtenberger

Johannes Lichtenberger

Posted on November 17, 2019

How to Implement Nuxt.js/Vue.js OAuth2 Authentication With an External REST-API Server (based on Vert.x/Kotlin) and Keycloak 🐬

Introduction

Authentication is hard. Therefore it's best to delegate authentication to a dedicated Software. In our case, we decided to use Keycloak.

We want to build a Nuxt.js based front-end for SirixDB, a temporal document store, which can efficiently retain and query snapshots of your data. A non-blocking, asynchronous REST-API is provided by an HTTP-Server. We decided to use Kotlin (heavy use of Coroutines) and Vert.x to implement the API-Server.

Authentication via OAuth2

OAuth2 specifies several so-called flows. For browser-based applications, the Authorization Code Flow is the best and most secure flow, which we'll use.

💚 OAuth2 Authorization Code Flow with Nuxt.js

We have a Workflow, where only ever the SirixDB HTTP-Server is interacting with Keycloak directly (besides redirects to the Node.js server). Thus, our front-end just has to know two routes of the SirixDB HTTP-Server: GET /user/authorize and POST /token.

In general, our workflow is as follows:

  1. An authentication middleware controls if users should be redirected to a /login route to login in the first place
  2. The /login route has a simple Button, which issues a request to the SirixDB HTTP-server. Nuxt.js generates a unique, unguessable state and a redirect_uri, which Nuxt.js sends to the GET /user/authorize route as URL parameters.
  3. The HTTP-Server redirects to a login-page of Keycloak and sends the two parameters as well
  4. Once a user correctly fills in his credentials, Keycloak redirects the browser to the given redirect_url, which Nuxt.js sends in the first place (and the SirixDB HTTP-Server)
  5. On the Node.js server, the Nuxt.js based front-end, a callback route is addressed by the redirect-URL from Keycloak
  6. Nuxt.js then extracts a URL parameter code and checks the state parameter for validity
  7. Next, Nuxt.js sends a POST HTTP-request to the /token endpoint on the SirixDB HTTP-Server with the code parameter, the redirect_uri again, which is the same callback route. Additionally, it sends a response_type which we set to code, such that Nuxt.js expects a JWT access token
  8. The SirixDB HTTP-Server then exchanges the given code with a JWT access token from Keycloak and sends it in the HTTP response to the Nuxt.js based front-end

Note that we can simplify this workflow if we are in the universal mode (not SPA). The Node.js server from Nuxt.js could also directly communicate with Keycloak, as we'll see later on. In this setup, the SirixDB HTTP-Server will only check authorization on its routes based on the issued JWT-tokens. However, this way, the front-end doesn't need to know that it's Keycloak and the host/ports and endpoint details. Furthermore, we'll see that Nuxt.js doesn't work with Keycloak out of the box.

👾 Nuxt.js Setup

In the Nuxt.js configuration file nuxt.config.js we have to add the following modules:

['@nuxtjs/axios',  { baseURL: 'https://localhost:9443' }], '@nuxtjs/auth', '@nuxtjs/proxy'
Enter fullscreen mode Exit fullscreen mode

Then we'll add:

  axios: {
    baseURL: 'https://localhost:9443',
    browserBaseURL: 'https://localhost:9443',
    proxyHeaders: true,
    proxy: true,
  },
  auth: {
    strategies: {
      keycloak: {
        _scheme: 'oauth2',
        authorization_endpoint: 'https://localhost:9443/user/authorize',
        userinfo_endpoint: false,
        access_type: 'offline',
        access_token_endpoint: 'https://localhost:9443/token',
        response_type: 'code',
        token_type: 'Bearer',
        token_key: 'access_token',
      },
    },
    redirect: {
      login: '/login',
      callback: '/callback',
      home: '/'
    },
  },
  router: {
    middleware: ['auth']
  }
Enter fullscreen mode Exit fullscreen mode

https://localhost:9443 is the host/port where the SirixDB HTTP-Server is listening.

Per default, our Nuxt.js configuration activates the authentication middleware on all routes. If the user is not authenticated, the first step is initiated, and the auth module from Nuxt.js redirects the user to the GET /login route.

We'll define a straightforward login page:

<template>
  <div>
    <h3>Login</h3>
    <el-button type="primary" @click="login()">Login via Keycloak</el-button>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component
export default class Login extends Vue {
  private login(): void {
    this.$auth.loginWith('keycloak')
  }
}
</script>

<style lang="scss">
</style>
Enter fullscreen mode Exit fullscreen mode

To define the right TypeScript types to use this.$auth we'll have to add

"typings": "types/index.d.ts",
"files": ["types/*.d.ts"]
Enter fullscreen mode Exit fullscreen mode

to the package.json file. Furthermore, we'll create the types directory and add the index.d.ts file.

In the Nuxt.js application in the plugin folder we'll add a file to extend the axios client:

export default function ({ $axios, redirect }) {
  $axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false })

  $axios.onRequest(config => {
    config.headers.common['Origin'] = 'http://localhost:3005';
    config.headers.common['Content-Type'] = 'application/json';
    config.headers.common['Accept'] = 'application/json';

    config.headers.put['Origin'] = 'http://localhost:3005';
    config.headers.put['Content-Type'] = 'application/json';
    config.headers.put['Accept'] = 'application/json';

    config.headers.post['Origin'] = 'http://localhost:3005';
    config.headers.post['Content-Type'] = 'application/json';
    config.headers.post['Accept'] = 'application/json';

    config.headers.del['Origin'] = 'http://localhost:3005';
    config.headers.del['Content-Type'] = 'application/json';
    config.headers.del['Accept'] = 'application/json';
  });

  $axios.onError(error => {
    const code = parseInt(error.response && error.response.status);
    if (code === 401) {
      redirect('https://localhost:9443/user/authorize');
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Now we've finished the Nuxt.js part of the equation. Next, we'll look into the SirixDB HTTP-Server.

🚀 SirixDB HTTP-Server: Vert.x based REST API

We'll have to set up the OAuth2 login routes as well as all other OAuth2 configuration related stuff.

But first we'll add a CORS handler for the OAuth2 Authentication Code Flow:

if (oauth2Config.flow == OAuth2FlowType.AUTH_CODE) {
    val allowedHeaders = HashSet<String>()
    allowedHeaders.add("x-requested-with")
    allowedHeaders.add("Access-Control-Allow-Origin")
    allowedHeaders.add("origin")
    allowedHeaders.add("Content-Type")
    allowedHeaders.add("accept")
    allowedHeaders.add("X-PINGARUNER")
    allowedHeaders.add("Authorization")

    val allowedMethods = HashSet<HttpMethod>()
    allowedMethods.add(HttpMethod.GET)
    allowedMethods.add(HttpMethod.POST)
    allowedMethods.add(HttpMethod.OPTIONS)

    allowedMethods.add(HttpMethod.DELETE)
    allowedMethods.add(HttpMethod.PATCH)
    allowedMethods.add(HttpMethod.PUT)

    this.route().handler(CorsHandler.create("*")
                .allowedHeaders(allowedHeaders)
                .allowedMethods(allowedMethods))
}

Enter fullscreen mode Exit fullscreen mode

OAuth2 configuration is read via:

val oauth2Config = oAuth2ClientOptionsOf()
    .setFlow(OAuth2FlowType.valueOf(config.getString("oAuthFlowType", "PASSWORD")))
    .setSite(config.getString("keycloak.url"))
    .setClientID("sirix")
    .setClientSecret(config.getString("client.secret"))
    .setTokenPath(config.getString("token.path", "/token"))
    .setAuthorizationPath(config.getString("auth.path", "/user/authorize"))

val keycloak = KeycloakAuth.discoverAwait(
    vertx, oauth2Config
)
Enter fullscreen mode Exit fullscreen mode

The configuration file looks like this:

{
  "https.port": 9443,
  "keycloak.url": "http://localhost:8080/auth/realms/sirixdb",
  "auth.path": "http://localhost:8080/auth/realms/sirixdb/protocol/openid-connect/auth",
  "token.path": "/token",
  "client.secret": "2e54cfdf-909b-47ca-b385-4c44886f04f0",
  "oAuthFlowType" : "AUTH_CODE",
  "redirect.uri" : "http://localhost:3005/callback"
}
Enter fullscreen mode Exit fullscreen mode

Note that usually, Nuxt.js specifies the redirect-URI, in which case the SirixDB HTTP-server reads it from the URL query parameters.

The HTTP-Server uses the following extension function, to provide coroutine handlers, whereas the suspending functions run on the Vert.x event loop:


/**
 * An extension method for simplifying coroutines usage with Vert.x Web routers.
 */
private fun Route.coroutineHandler(fn: suspend (RoutingContext) -> Unit): Route {
    return handler { ctx ->
        launch(ctx.vertx().dispatcher()) {
            try {
                fn(ctx)
            } catch (e: Exception) {
                ctx.fail(e)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The GET /user/authorize route (step 2). The browser will be redirected to the Keycloak login page.

get("/user/authorize").coroutineHandler { rc ->
    if (oauth2Config.flow != OAuth2FlowType.AUTH_CODE) {
        rc.response().statusCode = HttpStatus.SC_BAD_REQUEST
    } else {
        val redirectUri =
            rc.queryParam("redirect_uri").getOrElse(0) { config.getString("redirect.uri") }
        val state = rc.queryParam("state").getOrElse(0) { java.util.UUID.randomUUID().toString() }

        val authorizationUri = keycloak.authorizeURL(
            JsonObject()
                .put("redirect_uri", redirectUri)
                .put("state", state)
        )
        rc.response().putHeader("Location", authorizationUri)
            .setStatusCode(HttpStatus.SC_MOVED_TEMPORARILY)
            .end()
    }
}

Enter fullscreen mode Exit fullscreen mode

After providing the credentials, the Browser is sent back to the redirect_uri, (the /callback route), with the given state (generated by Nuxt.js in the first place). Then the auth module of Nuxt.js extracts the state and code from the URL query parameter. If the state is the same as it generated, it proceeds to POST the code and stores, the redirect_uri again, and the response_type as form parameters.

The POST /token route (step 7):

post("/token").handler(BodyHandler.create()).coroutineHandler { rc ->
    try {
        val dataToAuthenticate: JsonObject =
            when (rc.request().getHeader(HttpHeaders.CONTENT_TYPE)) {
                "application/json" -> rc.bodyAsJson
                "application/x-www-form-urlencoded" -> formToJson(rc)
                else -> rc.bodyAsJson
            }

        val user = keycloak.authenticateAwait(dataToAuthenticate)
        rc.response().end(user.principal().toString())
    } catch (e: DecodeException) {
        rc.fail(
            HttpStatusException(
                HttpResponseStatus.INTERNAL_SERVER_ERROR.code(),
                "\"application/json\" and \"application/x-www-form-urlencoded\" are supported Content-Types." +
                        "If none is specified it's tried to parse as JSON"
            )
        )
    }
}

private fun formToJson(rc: RoutingContext): JsonObject {
    val formAttributes = rc.request().formAttributes()
    val code =
        formAttributes.get("code")
    val redirectUri =
        formAttributes.get("redirect_uri")
    val responseType =
        formAttributes.get("response_type")

    return JsonObject()
        .put("code", code)
        .put("redirect_uri", redirectUri)
        .put("response_type", responseType)
}
Enter fullscreen mode Exit fullscreen mode

The SirixDB HTTP-Server retrieves a JWT token from Keycloak and sends it back to the front-end.

Afterward, Nuxt.js stores the token in its session, the store, and so on.

Finally, Axios has to send the token for each API request it does in the Authorization-Header as a Bearer token. We can retrieve the token via this.$auth.getToken('keycloak').

Note that instead of the indirection using the SirixDB HTTP-Server, Nuxt.js/Node.js could interact with Keycloak directly and the SirixDB HTTP-Server then only validates the JWT-tokens.

In that case the nuxt.config.js keycloak auth object looks as follows:

keycloak: {
    _scheme: 'oauth2',
    authorization_endpoint: 'http://localhost:8080/auth/realms/sirixdb/protocol/openid-connect/auth',
    userinfo_endpoint: false,
    access_type: 'offline',
    access_token_endpoint: 'http://localhost:8080/auth/realms/sirixdb/protocol/openid-connect/token',
    response_type: 'code',
    token_type: 'Bearer',
    token_key: 'access_token',
    client_secret: '2e54cfdf-909b-47ca-b385-4c44886f04f0',
    client_id: 'sirix'
}
Enter fullscreen mode Exit fullscreen mode

In this case we need to add http://localhost:3005 to the allowed Web Origins in Keycloak as we'll see in the next section.

However, I couldn't get this to work, as the auth module from Nuxt.js somehow doesn't send the client_secret to the Keycloak token-endpoint:

error: "unauthorized_client"
error_description: "Client secret not provided in request"

💚 Setting up Keycloak

Setting up Keycloak can be done as described in this excellent Tutorial. The following description is a short SirixDB summary (you can skip some parts by using SirixDBs docker-compose file). However, it should be almost identical to the Keycloak setuo of other projects.

In short :

  • Open your browser. URL: http://localhost:8080 Login with username admin and password admin to access Keycloaks web configuration interface
  • Create a new realm with the name sirixdb
  • Go to Clients => account
  • Change client-id to sirix
  • Make sure access-type is set to confidential
  • Go to Credentials tab
  • Put the client secret into the SirixDB HTTP-Server configuration file (posted above). Change the value of client.secret to whatever Keycloak set up.
  • The standard flow on the settings tab must be enabled.
  • Set the valid redirect URIs to http://localhost:3005/* or port 3000 or wherever your Nuxt.js application runs
  • Make sure to set the right values for Web Origins to allow CORS from these domains

Keycloak Configuration Image

Conclusion

Setting up everything to work together brought about some headaches. One simplification would be to let Nuxt.js do all the authentication in the first place, and let the external API server check the tokens.

Let me know if this article helps or if I made the whole authorization process too complicated.

Regarding SirixDB and the front-end I'd love to get some input or even contributions, that would be the most remarkable thing :-) I'm a backend engineer and I'm currently learning Nuxt.js/Vue.js and TypeScript as well as D3 in my spare time for this project. It's a green field project, so we can use the Vue.js Composition API for instance. 🐣

And if you like the project, you might share it on twitter and so and spread the word!? 🙈

Contribute on GitHub SirixDB and GitHub SirixDB Web Frontend 💚

GitHub logo sirixdb / sirix

SirixDB is an an embeddable, bitemporal, append-only database system and event store, storing immutable lightweight snapshots. It keeps the full history of each resource. Every commit stores a space-efficient snapshot through structural sharing. It is log-structured and never overwrites data. SirixDB uses a novel page-level versioning approach.

An Embeddable, Bitemporal, Append-Only Database System and Event Store

Stores small-sized, immutable snapshots of your data in an append-only manner. It facilitates querying and reconstructing the entire history as well as easy audits.

Tweet

Follow

Download ZIP | Join us on Discord | Community Forum | Documentation | Architecture & Concepts

Working on your first Pull Request? You can learn how from this free series How to Contribute to an Open Source Project on GitHub and another tutorial: How YOU can contribute to OSS, a beginners guide

"Remember that you're lucky, even if you don't think you are because there's always something that you can be thankful for." - Esther Grace Earl (http://tswgo.org)

We want to build the database system together with you. Help us and become a maintainer yourself. Why? You may like the software and want to help us. Furthermore, you'll learn a lot. You may want to




GitHub logo sirixdb / sirix-web-frontend

A web front-end for SirixDB based on Nuxt.js/Vue.js, D3.js and Typescript

PRs Welcome All Contributors

Tweet

Follow

Join us on Slack | Community Forum

Working on your first Pull Request? You can learn how from this free series How to Contribute to an Open Source Project on GitHub and another tutorial: How YOU can contribute to OSS, a beginners guide

SirixDB Web frontend - An Evolutionary, Versioned, Temporal NoSQL Document Store

Store and query revisions of your data efficiently

"Remember that you're lucky, even if you don't think you are, because there's always something that you can be thankful for." - Esther Grace Earl (http://tswgo.org)

Introduction

Discuss it in the Community Forum

This is the repository for a web frontend based on Vue.js, D3.js and TypeScript.

It'll provide several interaction possibilities to store, update and query databases in SirixDB. Furthermore the front-end will provide interactive visualizations for exploring and comparing revisions of resources stored in SirixDB based on different views.

Some ideas for

Kind regards
Johannes

💖 💪 🙅 🚩
johanneslichtenberger
Johannes Lichtenberger

Posted on November 17, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related