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
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
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.
Related
export class HttpClientService {
private baseURL: string;
tokenRepository: LocalTokenRepository;
instance: AxiosInstance;
constructor(baseURL: string, tokenRepository: LocalTokenRepository) {
this.tokenRepository = tokenRepository;
this.baseURL = baseURL;
this.instance = axios.create({
baseURL: this.baseURL,
headers: { Authorization: this.tokenRepository.get() }
});
this.handleInterceptor();
}
private handleInterceptor = () => {
this.instance.interceptors.response.use(
this.handleResponse,
this.handleError
);
};
private handleResponse = ({ data }: AxiosResponse) => data;
protected handleError = (error: any) => Promise.reject(error);
}
export class AuthService {
private httpClient: HttpClientService;
private tokenRepository: LocalTokenRepository;
constructor(
httpClient: HttpClientService
// tokenRepository: LocalTokenRepository
) {
this.httpClient = httpClient;
this.tokenRepository = this.httpClient.tokenRepository;
}
async signup(email: string, password: string) {
const accessTOKEN = (await this.httpClient.instance.post(
'data',
{ email, password },
{ params: '' }
)) as string;
this.tokenRepository.save(accessTOKEN);
}
async singin(email: string, password: string): Promise<any> {
console.log(email, password, 'signIn?');
// return await this.httpClient.instance.get('products', { params: '' });
}
signout() {
this.tokenRepository.remove();
}
}
export const apiContext = createContext<APIType | null>(null);
const tokenRepository = new LocalTokenRepository();
const httpClient = new HttpClientService('baseurl', tokenRepository);
const productService = new ProductService(httpClient);
const authService = new AuthService(httpClient);
export const ApiProvider = ({ children }: { children: React.ReactNode }) => {
return (
<apiContext.Provider value={{ productService, authService }}>
{children}
</apiContext.Provider>
);
};
I made network logic like this. Make the network logic by class and inject dependency by context API. Is there any advantages of making network logic by class?
I don't recognize between network class logic and use axios right away.
What is the best way to handle api logic in react? and What kind of method people use the most?
I'm building NextJS app with SSR. I've written the getServerSideProps function that makes a call to supabase. Before making the call I'm trying to get user session by calling getSession function from #auth0/nextjs-auth0 package.
I'm trying to mock it in the handlers.ts file:
import { rest } from 'msw';
export const handlers = [
// this is the endpoint called by getSession
rest.get('/api/auth/session', (_req, res, ctx) => {
return res(ctx.json(USER_DATA));
}),
rest.get('https://<supabase-id>.supabase.co/rest/v1/something', (_req, res, ctx) => {
return res(ctx.json(SOMETHING));
}),
];
My mocks file: requestMocks/index.ts:
export const initMockServer = async () => {
const { server } = await import('./server');
server.listen();
return server;
};
export const initMockBrowser = async () => {
const { worker } = await import('./browser');
worker.start();
return worker;
};
export const initMocks = async () => {
if (typeof window === 'undefined') {
console.log('<<<< setup server');
return initMockServer();
}
console.log('<<<< setup browser');
return initMockBrowser();
};
initMocks();
Finally, I'm calling it in the _app.tsx file:
if (process.env.NEXT_PUBLIC_API_MOCKING === 'true') {
require('../requestMocks');
}
Unfortunately, it does work for me. I'm getting no user session data in the getServerSideProps function in my page component:
import { getSession } from '#auth0/nextjs-auth0';
export const getServerSideProps = async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) => {
const session = getSession(req, res);
if (!session?.user.accessToken) {
// I'm constantly falling here
console.log('no.session');
return { props: { something: [] } };
}
// DO something else
};
Any suggestions on how to make it working in Cypress tests would be great.
I'm expecting that I will be able to mock requests made in getServerSideProps function with MSW.js library.
I made it finally. Looks like I don't have to mock any calls. I need to copy my user appSession cookie and save it in cypress/fixtures/appSessionCookie.json file:
{
"appSession": "<cookie-value>"
}
Then use it in tests as follows:
before(() => {
cy.fixture('appSessionCookie').then((cookie) => {
cy.setCookie('appSession', cookie.appSession);
});
});
This makes a user automatically logged in with Auth0.
I am converting my CRA app to Nextjs and running into some issues with my Axios interceptor pattern.
It works, but I am forced to create and pass an Axios instance to every api call.
Is there a better way to do this?
Here is what I have now:
Profile.js:
import { useSession } from 'next-auth/react'
function Profile(props) {
const { data: session } = useSession()
const [user, setUser] = useState()
useEffect(()=> {
const proc= async ()=> {
const user = await getUser(session?.user?.userId)
setUser(user)
}
proc()
},[])
return <div> Hello {user.userName}<div>
}
getUser.js:
export default async function getUser(userId) {
const axiosInstance = useAxios()
const url = apiBase + `/user/${userId}`
const { data } = await axiosInstance.get(url)
return data
}
useAxios.js:
import axios from 'axios'
import { useSession } from 'next-auth/react'
const getInstance = (token) => {
const axiosApiInstance = axios.create()
axiosApiInstance.interceptors.request.use(
(config) => {
if (token && !config.url.includes('authenticate')) {
config.headers.common = {
Authorization: `${token}`
}
}
return config
},
(error) => {
Promise.reject(error)
}
)
return axiosApiInstance
}
export default function useAxios() {
const session = useSession()
const token = session?.data?.token?.accessToken
return getInstance(token)
}
In case anyone else has this problem, this was how i solved it (using getSession):
credit to:
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
import axios from 'axios'
import { getSession } from 'next-auth/react'
const ApiClient = () => {
const instance = axios.create()
instance.interceptors.request.use(async (request) => {
const session = await getSession()
if (session) {
request.headers.common = {
Authorization: `${session.token.accessToken}`
}
}
return request
})
instance.interceptors.response.use(
(response) => {
return response
},
(error) => {
console.log(`error`, error)
}
)
return instance
}
export default ApiClient()
There is actually a neat way on including user extended details to session object
// /api/[...nextauth].ts
...
callbacks: {
session({ session, user, token }) {
// fetch user profile here. you could utilize contents of token and user
const profile = getUser(user.userId)
// once done above, you can now attach profile to session object
session.profile = profile;
return session;
}
},
The you could utilize it as:
const { data: session } = useSession()
// Should display profile details not included in session.user
console.log(session.profile)
I know one way to do this is to use
const session = await getSession()
Is there any other way to go about it without using await getSession() because what this does is that it makes a network request to get your session every time your Axios request runs?
I'm currently working on a backend API using Ruby on Rails, Devise, and Devise-JWT, with NextJS on the frontend client, using axios for requests. I've been trying to access the Authorization token from the header (once the user logs in), to then store the token into localhost but it doesn't come within the header on the request. Although, the post login request does supply an Auth on the header when testing on Postman. What am I missing? Thanks in advance!
expose Authorzation header using this: expose : ['Authorization']
in the rack corse config do this:
Rails.application.config.middleware.insert_before 0, "Rack::Cors" do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :options, :patch, :delete], expose: ['Authorization']
end
end
https://glaucocustodio.github.io/2016/01/20/dont-forget-to-expose-headers-when-using-rack-cors/
It would help to see the code you implemented the Axios user auth for the frontend.
But maybe this can help.
//---------- Reducer -----------
// In your reducer
const authReducer = (state, { type, payload }) => {
switch (type) {
case 'LOGIN': {
localStorage.setItem('authToken', JSON.stringify(payload.auth_token));
localStorage.setItem('authEmail', JSON.stringify(payload.email));
return {
authToken: payload.auth_token,
authEmail: payload.email,
};
}
case 'YOUROTHER': {
return { };
}
default: {
}
}
};
export default authReducer;
//-------------------------------
//---------- Contexts -----------
// In your Contexts auth.js file
import authReducer from '../reducers/auth';
const AuthStateContext = React.createContext();
const AuthDispatchContext = React.createContext();
const token = JSON.parse(localStorage.getItem('authToken'));
const email = JSON.parse(localStorage.getItem('authEmail'));
const initialState = {
isLoggedIn: !!token,
authToken: token ? token : null,
authEmail: email ? email : null,
};
const AuthProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(authReducer, initialState);
return (
<AuthStateContext.Provider value={state}>
<AuthDispatchContext.Provider value={dispatch}>
{children}
</AuthDispatchContext.Provider>
</AuthStateContext.Provider>
);
};
//-------------------------------
//--------- App.js file ---------
// Then wrap the App.js like:
import { AuthProvider } from './contexts/auth';
function App(props) {
return (
<AuthProvider>
<Main {...props} />
</AuthProvider>
);
}
//-------------------------------
//------- Your Login Component -------
// Imports
import { useAuthDispatch } from '../../contexts/auth';
// Axios handler
const baseUrl = 'http://localhost:3001/';
const login = payload => axios.post(`${baseUrl}api/v1/login`, payload);
const authApi = {
login,
};
// Auth Header Function
const setAuthHeaders = () => {
// axios request try this
axios.defaults.headers = {
Accept: 'applicaion/json',
'Content-Type': 'application/json',
};
const token = JSON.parse(localStorage.getItem('authToken'));
const email = JSON.parse(localStorage.getItem('authEmail'));
//Add token & email to axios header
if (token && email) {
axios.defaults.headers['X-Auth-Email'] = email;
axios.defaults.headers['X-Auth-Token'] = token;
}
};
// Login
const Login = () => {
const [initialValues, setInitialValues] = useState({
email: '',
password: '',
});
const authDispatch = useAuthDispatch();
//const userDispatch = useUserDispatch();
const handleLogin = async values => {
const { /*eg. email, password*/ } = values;
try {
const {
data: { user, /* should include:*/ auth_token },
} = await authApi.login({ user: { email, password } });
// your dispatch with payload: auth_token
authDispatch({ type: 'LOGIN', payload: { auth_token, email } }); // passing the token & email via Dispatch
// The user: { email, password } comes from the userDispatch
setAuthHeaders(); // Auth Header Func
} catch (error) {
};
return (
<>
{/* your jsx code */}
</>
);
}
};
export default Login;
//--- # Your Login Component ---
So I'm creating authentication logic in my Next.js app. I created /api/auth/login page where I handle request and if user's data is good, I'm creating a httpOnly cookie with JWT token and returning some data to frontend. That part works fine but I need some way to protect some pages so only the logged users can access them and I have problem with creating a HOC for that.
The best way I saw is to use getInitialProps but on Next.js site it says that I shouldn't use it anymore, so I thought about using getServerSideProps but that doesn't work either or I'm probably doing something wrong.
This is my HOC code:
(cookie are stored under userToken name)
import React from 'react';
const jwt = require('jsonwebtoken');
const RequireAuthentication = (WrappedComponent) => {
return WrappedComponent;
};
export async function getServerSideProps({req,res}) {
const token = req.cookies.userToken || null;
// no token so i take user to login page
if (!token) {
res.statusCode = 302;
res.setHeader('Location', '/admin/login')
return {props: {}}
} else {
// we have token so i return nothing without changing location
return;
}
}
export default RequireAuthentication;
If you have any other ideas how to handle auth in Next.js with cookies I would be grateful for help because I'm new to the server side rendering react/auth.
You should separate and extract your authentication logic from getServerSideProps into a re-usable higher-order function.
For instance, you could have the following function that would accept another function (your getServerSideProps), and would redirect to your login page if the userToken isn't set.
export function requireAuthentication(gssp) {
return async (context) => {
const { req, res } = context;
const token = req.cookies.userToken;
if (!token) {
// Redirect to login page
return {
redirect: {
destination: '/admin/login',
statusCode: 302
}
};
}
return await gssp(context); // Continue on to call `getServerSideProps` logic
}
}
You would then use it in your page by wrapping the getServerSideProps function.
// pages/index.js (or some other page)
export const getServerSideProps = requireAuthentication(context => {
// Your normal `getServerSideProps` code here
})
Based on Julio's answer, I made it work for iron-session:
import { GetServerSidePropsContext } from 'next'
import { withSessionSsr } from '#/utils/index'
export const withAuth = (gssp: any) => {
return async (context: GetServerSidePropsContext) => {
const { req } = context
const user = req.session.user
if (!user) {
return {
redirect: {
destination: '/',
statusCode: 302,
},
}
}
return await gssp(context)
}
}
export const withAuthSsr = (handler: any) => withSessionSsr(withAuth(handler))
And then I use it like:
export const getServerSideProps = withAuthSsr((context: GetServerSidePropsContext) => {
return {
props: {},
}
})
My withSessionSsr function looks like:
import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'
import { IronSessionOptions } from 'iron-session'
const IRON_OPTIONS: IronSessionOptions = {
cookieName: process.env.IRON_COOKIE_NAME,
password: process.env.IRON_PASSWORD,
ttl: 60 * 2,
}
function withSessionRoute(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, IRON_OPTIONS)
}
// Theses types are compatible with InferGetStaticPropsType https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getstaticprops
function withSessionSsr<P extends { [key: string]: unknown } = { [key: string]: unknown }>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) {
return withIronSessionSsr(handler, IRON_OPTIONS)
}
export { withSessionRoute, withSessionSsr }