Using keycloak public client token to communicate with confidential client

754 Views Asked by At

I have a quarkus backend app with a react frontend. I want to add a security layer where a user has to login in order to be able to access the UI, and any API calls made from the UI to the backend requires a token with the user is authenticated. Keycloak is the best and simplest(ish) solution for this.

I found this tutorial and it's exactly what I need but doesn't work :(

My keycloak setup is a client for the frontend and backend.

Frontend Setup

The frontend client is a public access type

frontend client

I then defined a few different JS files to setup the connection to keycloak and the header token...

UserService.ts

import Keycloak, {KeycloakInstance} from 'keycloak-js'

const keycloak: KeycloakInstance = Keycloak('/keycloak.json');

/**
 * Initializes Keycloak instance and calls the provided callback function if successfully authenticated.
 *
 * @param onAuthenticatedCallback
 */
const initKeycloak = (onAuthenticatedCallback) => {
    keycloak.init({
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri: window.location.origin + /silent-check-sso.html,
        checkLoginIframe: false,
    })
    .then((authenticated) => {
        if (authenticated) {
            onAuthenticatedCallback();
        } else {
            doLogin();
        }
    })
};

const doLogin = keycloak.login;

const doLogout = keycloak.logout;

const getToken = () => keycloak.token;

const getKeycloakId = () => keycloak.subject; // subject is the keycloak id

const isLoggedIn = () => !!keycloak.token;

const updateToken = (successCallback) =>
        keycloak.updateToken(5)
                .then(successCallback)
                .catch(doLogin);

const getUserInfo = async () => await keycloak.loadUserInfo();

const getUsername = () => keycloak.tokenParsed?.sub;

const hasRole = (roles) => roles.some((role) => keycloak.hasResourceRole(role, 'frontend'));

export const UserService = {
    initKeycloak,
    doLogin,
    doLogout,
    isLoggedIn,
    getToken,
    getKeycloakId,
    updateToken,
    getUsername,
    hasRole,
};

keycloak.json

{
  "realm": "buddydata",
  "auth-server-url": "http://127.0.0.1:8180/auth/",
  "ssl-required": "external",
  "resource": "app",
  "public-client": true,
  "verify-token-audience": true,
  "use-resource-role-mappings": true,
  "confidential-port": 0
}

HttpService.ts

import axios from "axios";
import {UserService} from "@/services/UserService";

const HttpMethods = {
  GET: 'GET',
  POST: 'POST',
  DELETE: 'DELETE',
  PUT: 'PUT',
};

const baseURL = 'http://localhost:9000/api/v1';


const _axios = axios.create({baseURL: baseURL});

const configure = () => {
  _axios.interceptors.request.use((config) => {
    if (UserService.isLoggedIn()) {
      const cb = () => {
        config.headers.Authorization = `Bearer ${UserService.getToken()}`;
        return Promise.resolve(config);
      };
      return UserService.updateToken(cb);
    }
  });
};

const getAxiosClient = () => _axios;

export const HttpService = {
  HttpMethods,
  configure,
  getAxiosClient
};

Backend Setup

backend client

backend client 2

There is no need to define any code updates, just config updates for quarkus...

application.properties

quarkus.resteasy-reactive.path=/api/v1
quarkus.http.port=9000
quarkus.http.cors=true


quarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/buddydata
quarkus.oidc.client-id=api
quarkus.oidc.credentials.secret=ff5b3f63-446f-4ca4-8623-1475cb59a343
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated

That is the keycloak config setup along with quarkus and react setup too. I have included all the information to show it all matches up. When the user logs in on the frontend a call is made to the backend to get the initial state, pre oidc setup, this worked but now it doesn't.

Root.tsx

const store = StoreService.setup();

export const Root = (): JSX.Element => {
    StoreService.getInitialData(store)
        .then(_ => console.log("Initial state loaded"));

    return (
        <RenderOnAuthenticated>
            <h1>Hello {UserService.getUsername()}</h1>
        </RenderOnAuthenticated>
    )
};

StoreService.ts

const setup = () => {
  const enhancers = [];
  const middleware = [
    thunk,
    axiosMiddleware(HttpService.getAxiosClient())
  ];

  if (process.env.NODE_ENV === 'development') {
    enhancers.push(applyMiddleware(logger));
  }

  // const composedEnhancers = compose(applyMiddleware(...middleware), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), ...enhancers);
  const composedEnhancers = compose(applyMiddleware(...middleware), ...enhancers);

  return createStore(rootReducer, composedEnhancers);
};

const getInitialData = async store => {
  // Get the logged in user first
  await store.dispatch(getLoggedInUser());
  const user = selectCurrentUser(store.getState());
}

const StoreService = {
  setup,
  getInitialData
};

export default StoreService;

currentUser.ts

const axios = HttpService.getAxiosClient();

// User state interface
export interface UserState {
    id: number
    keycloakId: string,
    address?: {},
    title?: string,
    firstName?: string,
    lastName?: string,
    jobTitle?: string,
    dateOfBirth?: string,
    mobile?: string,
    email: string,
    isAdmin?: boolean,
    creationDate?: Date,
    updatedDate?: Date
}

// Action Types
const GET_USER_SUCCESS = 'currentUser/GET_USER_SUCCESS';

// Reducer
const initialUser: UserState = {id: -1, keycloakId: "-1", email: "no@email"}
export const currentUserReducer = (currentUserState = initialUser, action) => {
    switch (action.type) {
        case GET_USER_SUCCESS:
            return {...currentUserState, ...action.payload.user};
        default:
            return currentUserState;
    }
};

// Synchronous action creator
export const getLoggedInUserSuccess = userResponse => ({
    type: GET_USER_SUCCESS,
    payload: { user: userResponse.data }
})

// Asynchronous thunk action creator
// calls api, then dispatches the synchronous action creator
export const getLoggedInUser = (keycloakId = UserService.getKeycloakId()) => {
    return async getLoggedInUser => {
        try {
            let axiosResponse = await axios.get(`/initial/users/${keycloakId}`)
            getLoggedInUser(getLoggedInUserSuccess(axiosResponse))
        } catch(e) {
            console.log(e);
        }
    }
}

// Selectors
export const selectCurrentUser = state => state.currentUser;

InitialDataController.java

@Path("/initial")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class InitialDataController {

    @Inject
    UserService userService;

    @GET
    @Path("/users/{keycloakId}")
    public Response getUserByKeycloakId(@PathParam("keycloakId") String keycloakId) {
        UserEntity user = userService.getUserWithKeycloakId(keycloakId);
        return Response
                .ok(user)
                .build();
    }
}

UserService.java

@ApplicationScoped
public class UserService {

    @Inject
    UserRepository repository;

    public UserEntity getUserWithKeycloakId(@NotNull String keycloakId) {
        return repository.findUserWithKeycloakId(keycloakId);
    }
}

UserRepository.java

@ApplicationScoped
public class UserRepository implements PanacheRepositoryBase<UserEntity, Long> {

    public UserEntity findUserWithKeycloakId(String keycloakId) {
        return find("#User.getUserByKeycloakId", keycloakId).firstResult();
    }
}

The named query is SELECT u FROM User u WHERE u.keycloakId = ?1

I have provided my config setup for keycloak, react and quarkus and a step through the code on how calls are made to the backend from the UI. How can I secure the backend client on keycloak and have the frontend public, and then be able to make secure requests from the frontend to the backend. At the moment any requests made is giving a 401 Unauthorized response. I don't know how to get around this and I feel it's a keycloak config issue but not sure which option to change/update specifically. Any new knowledge/information on how to get over this would be great.

0

There are 0 best solutions below