Using OICD Authorization Code Flow with a RESTful API and an SPA

458 Views Asked by At

I'm currently working on a personal project which involves a Quarkus REST API as a back-end, Keycloak as OpenId Connect Provider and a Vue app as front-end. I just can't wrap my head around how to make these three components play well together for user authentication while maintaining proper security.

According to draft v8 of OAuth 2.0 for Browser-Based Apps, the SPA shouldn't be the one to keep the access and (possibly) refresh tokens because they are hard to store securely in such a scenario. That means, the back-end must be acting as the Relying Party, initiating the OIDC Authorization Code Flow. I'd then either keep a session cookie with my SPA (which I'd prefer not to do, to keep the API stateless), or store the tokens inside a Secure, SameSite, HttpOnly cookie. The approach is what I’m trying to accomplish, so far with little success.

Prototype implementation

My Quarkus app uses the quarkus-oicd extension. The way I understand it, I have to add the following configuration to Quarkus' application.properties:

quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=myClientId
quarkus.oidc.credentials.secret=********
quarkus.oidc.auth-server-url=http://127.0.0.1:8082/auth/realms/myRealm

The application-type=web-app being what tells Quarkus that it is responsible for initiating the authorization code flow. The alternative would be service, in which case Quarkus only validates bearer tokens the client sends to the API.

The API is running on port 8081 and only exposes a single sample resource:

@Path("/hello")
@Authenticated
public class ReactiveGreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello RESTEasy Reactive";
    }
}

This simple Vue component is meant as a proof-of-concept:

// FetchComponent.vue

<template>
  <div>
    <button v-on:click="fetchFromBackend">Fetch</button>
    <p><b>Output:</b>{{ message }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  data() {
    return {
      message: "Click the button to fetch.",
    };
  },
  methods: {
    fetchFromBackend(): void {
      this.message = "Waiting...";
      fetch("http://localhost:8081/hello", {
        credentials: "include",
      })
        .then((resp) => {
          console.log(resp);
          if (resp.redirected) {
            window.location.assign(resp.url);
          } else {
            return resp.text().then((text) => (this.message = text));
          }
        })
        .catch((reason) => (this.message = "Caught error: " + reason));
    },
  },
});
</script>

<style></style>

Desired outcome

I'd have expected that the call to the back-end without a valid token gets redirected to Keycloak's authentication page. The user would then enter their credentials, be logged in and redirected back to the SPA with an auth code. It calls the API again. On the back-channel, Quarkus exchanges the auth code against a token and forwards it to the client in form of a cookie. This cookie would be used to authenticate the user for any further API calls.

Actual outcome

When the back-end is called without a valid token, it redirects to Keycloak's login page, as expected. Apparently, though, there is simply no way to navigate to the redirected URL from JS. The fetch specification states: "Redirects (a response whose status or internal response’s (if any) status is a redirect status) are not exposed to APIs. Exposing redirects might leak information not otherwise available through a cross-site scripting attack." The redirect: 'manual' option in a fetch request is somewhat of a red herring. It doesn't do what one would expect and it certainly doesn't allow me access to the redirect URL.

What happens instead is that the browser transparently follows the redirect and tries to fetch the login URL. That doesn't work at all. It results in a CORS error, because Keycloak doesn't set the relevant headers (and I suppose it shouldn't, because this isn't how it's supposed to work).

I have no clue how to proceed from here but I presume that the answer is extremely obvious to more experienced people.

As a closing remark I'd like to add that this architecture wasn't the result of a very well-informed decision making process. I chose it mostly because:

  • Quarkus: Java is currently my primary language at my job
  • Keycloak: I wanted to try my hand at proper externalized IAM and SSO for a while now. This seemed a good opportunity.
  • Vue: I wanted something to train my JS skills and which would look good on my resume. Any of the current batch of hot SPA frameworks would have fit the bill.

So, any answers along the lines of "that's a terrible setup, just don't do it, try X instead" are definitely also welcome, even though I'd still love to solve this puzzle as a matter of pride.

0

There are 0 best solutions below