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:
- An authentication middleware controls if users should be redirected to a
/login
route to login in the first place - The
/login
route has a simple Button, which issues a request to the SirixDB HTTP-server. Nuxt.js generates a unique, unguessablestate
and aredirect_uri
, which Nuxt.js sends to theGET /user/authorize
route as URL parameters. - The HTTP-Server redirects to a login-page of Keycloak and sends the two parameters as well
- 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)
- On the Node.js server, the Nuxt.js based front-end, a callback route is addressed by the redirect-URL from Keycloak
- Nuxt.js then extracts a URL parameter
code
and checks thestate
parameter for validity - Next, Nuxt.js sends a
POST
HTTP-request to the/token
endpoint on the SirixDB HTTP-Server with thecode
parameter, theredirect_uri
again, which is the same callback route. Additionally, it sends aresponse_type
which we set to code, such that Nuxt.js expects a JWT access token - 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'
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']
}
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>
To define the right TypeScript types to use this.$auth
we'll have to add
"typings": "types/index.d.ts",
"files": ["types/*.d.ts"]
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');
}
});
}
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))
}
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
)
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"
}
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)
}
}
}
}
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()
}
}
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)
}
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'
}
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 passwordadmin
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
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 π
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.
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β¦
sirixdb / sirix-web-frontend
A web front-end for SirixDB based on Nuxt.js/Vue.js, D3.js and Typescript
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)
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 π
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 :-)
Wow. Then I've to appreciate your efforts! You're doing great ππΎππ½
I'm really happy that I got it up and running :-)
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
Actually it's from your answer ;-)
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)
Ah, no, your answer is new, I think :-) thanks
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
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
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 ? ;)
I have never heard of SirixDB.
It's a temporal document store which never overrides data.
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.