DEV Community

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

Posted on

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

Top comments (13)

Collapse
 
liyasthomas profile image
Liyas Thomas

This is great! πŸ™ Thanks for the article.

Can we have a Nuxt Vue Firebase auth tutorial πŸ™ƒ perhaps with CRUD operations on Firestore πŸ˜ŠπŸ‘‹ (if you'd seen any such tutorial, please DM/reply. It would be great πŸ™

Collapse
 
johanneslichtenberger profile image
Johannes Lichtenberger

Hey, sadly not and I have to say it has driven me crazy, to figure out what's going on and what exactly Nuxt.js is doing as I'm super new to the front-end stuff and never worked with Vue.js and Nuxt.js or JavaScript and TypeScript. Even CORS has driven me crazy ;) Currently I'm reading a book about TypeScript and one about D3.js, hope I'm then able to finally start some "real" work on the visualizations and with the document store itself :-)

Collapse
 
liyasthomas profile image
Liyas Thomas

Wow. Then I've to appreciate your efforts! You're doing great πŸ‘ŒπŸΎπŸ‘‹πŸ½

Thread Thread
 
johanneslichtenberger profile image
Johannes Lichtenberger

I'm really happy that I got it up and running :-)

Collapse
 
nickbolles profile image
Nick Bolles

For the typings for nuxt-auth see my stack overflow answer here for a more standard way of doing the typings
stackoverflow.com/a/59011507

Collapse
 
johanneslichtenberger profile image
Johannes Lichtenberger

Actually it's from your answer ;-)

Collapse
 
nickbolles profile image
Nick Bolles • Edited

But your copying and pasting the typings. From my SO answer, you can add @types/nuxtjs__auth as a npm dev dependency and then add the package name to your tsconfig -> compilerOptions.types array.

Also, the "typings" key in your package.json is for including typings in published npm packages. And ussually that's not how nuxt is used (nuxt apps just consume typings, nothing usually consumes the nuxt app itself besides the nuxt-cli, so there's no need to define typings for the nuxt app)

Thread Thread
 
johanneslichtenberger profile image
Johannes Lichtenberger

Ah, no, your answer is new, I think :-) thanks

Collapse
 
johanneslichtenberger profile image
Johannes Lichtenberger

BTW: If anyone figures out, how to extend the basic oauth2 Nuxt.js config, that it would also work in universal mode with Node.js (standard) and without an API-server proxy (the SirixDB HTTP-Server in this case) let me know, how to send the client_secret in the code for token exchange request. It is simply not sent usually :(

cmty.app/nuxt/auth-module/issues/c445

Collapse
 
ptrckdev profile image
Patrick Strobel

why would you put a client_secret into the users browser? ^^ You have to create a new client in keycloak with a public access type, because that's what your "secret" would be if you put it in a browser: public :D

Collapse
 
mpgalaxy profile image
mpgalaxy

Hi, thank you so much for this, it is really great! ;)
I'm developing a nuxt.js universal app which is being secured by keycloak (openidconnect) via the nuxt auth module. Everything works fine until I'm stuck behind a proxy server. The openidconnect flow (standard flow) posts server side to get the access and refresh token. This times out behind a corporate proxy. I would need to intercept the openidconnect flow to tell axios to use the company proxy just for the call to fetch the token. I already build an interceptor for axios, but I can't access the point, where the token is being fetched, the interceptor always runs after the login and token fetch process is finished (e.g. logout). I don't want global proxy configuration, because 99% of my axios calls on server side are internal, thus not needing the proxy. How can I intercept THE axios call for fetching the token and provide it with a custom axios interceptor, which inclues the proxy configuration ? ;)

Collapse
 
mkhgfdrtt profile image
JT

I have never heard of SirixDB.

Collapse
 
johanneslichtenberger profile image
Johannes Lichtenberger • Edited

It's a temporal document store which never overrides data.

  • if you ever encountered human or application errors when storing data in a database it might be for you (simply revert to a known good snapshot and store a new revision with fixed data)
  • if you ever had to audit what was in the database at a specific time it might be for you (every revision is indexed, unchanged database pages are shared between revisions...)
  • if you ever had to analyse how something changed over time you can do time-travel queries. For instance selecting a price of a product (might be a JSON record value and you can simply fetch all past versions). Or you can do really sophisticated time travel queries...

Basically It's all about versioning, even the database pages are stored in fragments to write changes on a fine granular level.

The revision timestamp, the time a transaction commits is only stored once for all the changed data in a RevisionRootPage.

All records might be hashed in a merkle tree with a rolling hash function.