diff --git a/src/assets/images/orcid.png b/src/assets/images/orcid.png
new file mode 100644
index 0000000000000000000000000000000000000000..2db382dfc069b8a098c876a8d38add9977bc406a
Binary files /dev/null and b/src/assets/images/orcid.png differ
diff --git a/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx b/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2cd0b46b08fd232978af3fe8db18240334b1ec0e
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { loginUserSelector } from '@/redux/user/user.selectors';
+import { openLoginModal } from '@/redux/modal/modal.slice';
+import { MINUS_ONE, ZERO } from '@/constants/common';
+import { Button } from '@/shared/Button';
+import { adminEmailValSelector } from '@/redux/configuration/configuration.selectors';
+import { projectsSelector } from '@/redux/projects/projects.selectors';
+import { getOAuth } from '@/redux/oauth/oauth.thunks';
+
+export const AccessDeniedModal: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const login = useAppSelector(loginUserSelector);
+  const projects = useAppSelector(projectsSelector);
+
+  const adminEmail = useAppSelector(adminEmailValSelector);
+
+  const isAnonymousLogin = !login;
+
+  const isProjectsAvailable = projects.length > ZERO;
+
+  const isAdminEmail = adminEmail !== '' && adminEmail !== undefined;
+
+  const handleGoBack = async (e: React.FormEvent<HTMLButtonElement>): Promise<void> => {
+    e.preventDefault();
+    window.history.go(MINUS_ONE);
+  };
+
+  const handleLogin = async (e: React.FormEvent<HTMLButtonElement>): Promise<void> => {
+    e.preventDefault();
+    dispatch(getOAuth());
+    dispatch(openLoginModal());
+  };
+
+  const handleContactAmin = async (e: React.FormEvent<HTMLButtonElement>): Promise<void> => {
+    e.preventDefault();
+    window.location.href = `mailto:${adminEmail}`;
+  };
+
+  const openProject = (e: React.FormEvent<HTMLButtonElement>): void => {
+    window.location.href = `?id=${e.currentTarget.value}`;
+  };
+
+  return (
+    <div className="w-[400px] border border-t-[#E1E0E6] bg-white p-[24px]">
+      {isAnonymousLogin && (
+        <div className="grid grid-cols-2 gap-2">
+          <div>
+            <Button
+              className="ring-transparent hover:ring-transparent"
+              variantStyles="secondary"
+              onClick={handleGoBack}
+            >
+              Go back to previous page
+            </Button>
+          </div>
+          <div className="text-center">
+            <Button className="block w-full" onClick={handleLogin}>
+              Login to your account
+            </Button>
+          </div>
+        </div>
+      )}
+      {isProjectsAvailable && (
+        <div>
+          <div className="mb-1 mt-5 text-sm">Switch to another map</div>
+          <div className="grid grid-cols-3 gap-2">
+            {projects.map(project => (
+              <Button
+                key={project.projectId}
+                value={project.projectId}
+                variantStyles="ghost"
+                className="text-center text-gray-500"
+                onClick={openProject}
+              >
+                {project.name} ({project.projectId})
+              </Button>
+            ))}
+          </div>
+        </div>
+      )}
+      {isAdminEmail && (
+        <div className="mt-1 text-center">
+          <Button
+            className="block w-full ring-transparent hover:ring-transparent"
+            variantStyles="secondary"
+            onClick={handleContactAmin}
+          >
+            Contact admin
+          </Button>
+        </div>
+      )}
+    </div>
+  );
+};
diff --git a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx
index 3fa89a01282b34bd7a1dec3b1b3698f260ea16c8..58e6e2f672b5120cb8a691f118352b34c12e6307 100644
--- a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx
@@ -7,6 +7,10 @@ import { Button } from '@/shared/Button';
 import { Input } from '@/shared/Input';
 import Link from 'next/link';
 import React from 'react';
+import { BASE_API_URL } from '@/constants';
+import { orcidEndpointSelector } from '@/redux/oauth/oauth.selectors';
+import Image from 'next/image';
+import orcidLogoImg from '@/assets/images/orcid.png';
 
 export const LoginModal: React.FC = () => {
   const dispatch = useAppDispatch();
@@ -14,11 +18,19 @@ export const LoginModal: React.FC = () => {
   const isPending = loadingUser === 'pending';
   const [credentials, setCredentials] = React.useState({ login: '', password: '' });
 
+  const orcidEndpoint = useAppSelector(orcidEndpointSelector);
+
+  const isOrcidAvailable = orcidEndpoint !== undefined;
+
   const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
     const { name, value } = e.target;
     setCredentials(prevCredentials => ({ ...prevCredentials, [name]: value }));
   };
 
+  const handleLoginViaOrcid = (): void => {
+    window.location.href = `${BASE_API_URL}/..${orcidEndpoint}`;
+  };
+
   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
     e.preventDefault();
     await dispatch(login(credentials));
@@ -57,6 +69,16 @@ export const LoginModal: React.FC = () => {
             Forgot password?
           </Link>
         </div>
+        {isOrcidAvailable && (
+          <Button
+            variantStyles="quiet"
+            className="mb-1 w-full justify-center text-base font-medium"
+            onClick={handleLoginViaOrcid}
+          >
+            <Image src={orcidLogoImg} alt="orcid logo" height={32} width={32} className="mr-1.5" />
+            Sign in with Orcid
+          </Button>
+        )}
         <Button
           type="submit"
           className="w-full justify-center text-base font-medium"
diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx
index 1fa31af6a7f052d1005bf4d6155f06352292d16c..8e2a02b528cf172eebb868fd183008da565dde12 100644
--- a/src/components/FunctionalArea/Modal/Modal.component.tsx
+++ b/src/components/FunctionalArea/Modal/Modal.component.tsx
@@ -1,6 +1,7 @@
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { modalSelector } from '@/redux/modal/modal.selector';
 import dynamic from 'next/dynamic';
+import { AccessDeniedModal } from '@/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component';
 import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component';
 import { EditOverlayModal } from './EditOverlayModal';
 import { LoginModal } from './LoginModal';
@@ -51,6 +52,11 @@ export const Modal = (): React.ReactNode => {
           <LoggedInMenuModal />
         </ModalLayout>
       )}
+      {isOpen && modalName === 'access-denied' && (
+        <ModalLayout>
+          <AccessDeniedModal />
+        </ModalLayout>
+      )}
       {isOpen && modalName === 'add-comment' && (
         <ModalLayout>
           <AddCommentModal />
diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
index 392a673a0a05cef759abe4e2918d7246b8a00cf0..3afcf1f8cfc442b64f8ced8934545a33337b4d2c 100644
--- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
+++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
@@ -28,19 +28,22 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
           className={twMerge(
             'flex h-5/6 w-10/12	flex-col	overflow-hidden rounded-lg',
             modalName === 'login' && 'h-auto w-[400px]',
+            modalName === 'access-denied' && 'h-auto w-[400px]',
             modalName === 'add-comment' && 'h-auto w-[400px]',
             modalName === 'error-report' && 'h-auto w-[800px]',
             ['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]',
           )}
         >
           <div className="flex items-center justify-between bg-white p-[24px] text-xl">
-            {modalName === 'error-report' && (
+            {(modalName === 'error-report' || modalName === 'access-denied') && (
               <div className="font-bold text-red-500">
                 <Icon name="info" className={twMerge('mr-4 fill-red-500')} />
                 {modalTitle}
               </div>
             )}
-            {modalName !== 'error-report' && <div> {modalTitle} </div>}
+            {modalName !== 'error-report' && modalName !== 'access-denied' && (
+              <div> {modalTitle} </div>
+            )}
 
             {modalName !== 'logged-in-menu' && (
               <button type="button" onClick={handleCloseModal} aria-label="close button">
diff --git a/src/components/FunctionalArea/TopBar/User/User.component.test.tsx b/src/components/FunctionalArea/TopBar/User/User.component.test.tsx
index 6198869f43d302eedd7269fe183e1622eed445a3..707e0f5dac4dac4709f7d47e1eef9e5ee3e226ba 100644
--- a/src/components/FunctionalArea/TopBar/User/User.component.test.tsx
+++ b/src/components/FunctionalArea/TopBar/User/User.component.test.tsx
@@ -8,6 +8,7 @@ import { apiPath } from '@/redux/apiPath';
 import { HttpStatusCode } from 'axios';
 import mockRouter from 'next-router-mock';
 import { projectFixture } from '@/models/fixtures/projectFixture';
+import { oauthFixture } from '@/models/fixtures/oauthFixture';
 import { User } from './User.component';
 
 const mockedAxiosClient = mockNetworkResponse();
@@ -131,6 +132,8 @@ describe('AuthenticatedUser component', () => {
   });
 
   it('should display login modal if switch account is pressed', async () => {
+    mockedAxiosClient.onGet(apiPath.getOauthProviders()).reply(HttpStatusCode.Ok, oauthFixture);
+
     const { store } = renderComponent({
       user: {
         ...USER_INITIAL_STATE_MOCK,
diff --git a/src/components/FunctionalArea/TopBar/User/hooks/useUserActions.ts b/src/components/FunctionalArea/TopBar/User/hooks/useUserActions.ts
index dd5b22725de0bb9da3a296a43d2597c62e7e4c4a..f1c0f845b995fe380cd750a38acbf3d7b874631b 100644
--- a/src/components/FunctionalArea/TopBar/User/hooks/useUserActions.ts
+++ b/src/components/FunctionalArea/TopBar/User/hooks/useUserActions.ts
@@ -8,6 +8,7 @@ import { useRouter } from 'next/router';
 import { USER_ROLE } from '@/constants/user';
 import { useMemo } from 'react';
 import { CURRENT_PROJECT_ADMIN_PANEL_URL } from '@/constants';
+import { getOAuth } from '@/redux/oauth/oauth.thunks';
 import { ADMIN_CURATOR_ACTIONS, BASE_ACTIONS } from '../User.constants';
 
 type UseUserActionsReturnType = {
@@ -33,6 +34,7 @@ export const useUserActions = (): UseUserActionsReturnType => {
   }, [userRole]);
 
   const openModalLogin = (): void => {
+    dispatch(getOAuth());
     dispatch(openLoginModal());
   };
 
diff --git a/src/constants/common.ts b/src/constants/common.ts
index c3a1b1d09a4c675a5cca281cd1a02f33515bb2bd..9bdcd6487cb3cd51ff146c5a2d60796ddccb4c99 100644
--- a/src/constants/common.ts
+++ b/src/constants/common.ts
@@ -2,6 +2,8 @@ export const SIZE_OF_EMPTY_ARRAY = 0;
 export const SIZE_OF_ARRAY_WITH_FOUR_ELEMENTS = 4;
 export const SIZE_OF_ARRAY_WITH_ONE_ELEMENT = 1;
 
+export const MINUS_ONE = -1;
+
 export const ZERO = 0;
 export const FIRST_ARRAY_ELEMENT = 0;
 
diff --git a/src/models/fixtures/oauthFixture.ts b/src/models/fixtures/oauthFixture.ts
new file mode 100644
index 0000000000000000000000000000000000000000..753b8da35f060089663aadb518d34b7bee7b1b4e
--- /dev/null
+++ b/src/models/fixtures/oauthFixture.ts
@@ -0,0 +1,8 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { oauthSchema } from '@/models/oauthSchema';
+
+export const oauthFixture = createFixture(oauthSchema, {
+  seed: ZOD_SEED,
+});
diff --git a/src/models/fixtures/projectsFixture.ts b/src/models/fixtures/projectsFixture.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c625481f22a4209cead2372ea09a0d98aa94c85a
--- /dev/null
+++ b/src/models/fixtures/projectsFixture.ts
@@ -0,0 +1,10 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { pageableSchema } from '@/models/pageableSchema';
+import { projectSchema } from '../projectSchema';
+
+export const projectPageFixture = createFixture(pageableSchema(projectSchema), {
+  seed: ZOD_SEED,
+  array: { min: 1, max: 1 },
+});
diff --git a/src/models/oauthSchema.ts b/src/models/oauthSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a9370c9d2876ad0aa7fff55142461c9b9b7dcea7
--- /dev/null
+++ b/src/models/oauthSchema.ts
@@ -0,0 +1,5 @@
+import { z } from 'zod';
+
+export const oauthSchema = z.object({
+  Orcid: z.string().optional(),
+});
diff --git a/src/models/pageableSchema.ts b/src/models/pageableSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..94c544ee3909ae23cd73b48f3ddb4e9da1383820
--- /dev/null
+++ b/src/models/pageableSchema.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+import { ZodTypeAny } from 'zod/lib/types';
+
+export const pageableSchema = <T extends ZodTypeAny>(type: T): ZodTypeAny =>
+  z.object({
+    totalPages: z.number().nonnegative(),
+    totalElements: z.number().nonnegative(),
+    numberOfElements: z.number().nonnegative(),
+    size: z.number().positive(),
+    number: z.number().nonnegative(),
+    content: z.array(type),
+  });
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index ae21303c3c760f0da9efe1878d2c01b7a0f6615f..8227bfec287407fd9cc09415b7176beb6ed914bb 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -54,6 +54,7 @@ export const apiPath = {
   getAllBackgroundsByProjectIdQuery: (projectId: string): string =>
     `projects/${projectId}/backgrounds/`,
   getProjectById: (projectId: string): string => `projects/${projectId}`,
+  getProjects: (): string => `projects/`,
   getSessionValid: (): string => `users/isSessionValid`,
   postLogin: (): string => `doLogin`,
   getConfigurationOptions: (): string => 'configuration/options/',
@@ -100,6 +101,7 @@ export const apiPath = {
   user: (login: string): string => `users/${login}`,
   getStacktrace: (code: string): string => `stacktrace/${code}`,
   submitError: (): string => `minervanet/submitError`,
+  getOauthProviders: (): string => `oauth/providers/`,
   userPrivileges: (login: string): string => `users/${login}?columns=privileges`,
   getComments: (): string => `projects/${PROJECT_ID}/comments/models/*/`,
   addComment: (modelId: number, x: number, y: number): string =>
diff --git a/src/redux/configuration/configuration.constants.ts b/src/redux/configuration/configuration.constants.ts
index a03edc92e3b60554196a00115bc246453a7eef83..59ef10da2e7defa1753613d5b185ad0d6d454439 100644
--- a/src/redux/configuration/configuration.constants.ts
+++ b/src/redux/configuration/configuration.constants.ts
@@ -4,6 +4,7 @@ export const SIMPLE_COLOR_VAL_NAME_ID = 'SIMPLE_COLOR_VAL';
 export const NEUTRAL_COLOR_VAL_NAME_ID = 'NEUTRAL_COLOR_VAL';
 export const OVERLAY_OPACITY_NAME_ID = 'OVERLAY_OPACITY';
 export const SEARCH_DISTANCE_NAME_ID = 'SEARCH_DISTANCE';
+export const REQUEST_ACCOUNT_EMAIL = 'REQUEST_ACCOUNT_EMAIL';
 
 export const LEGEND_FILE_NAMES_IDS = [
   'LEGEND_FILE_1',
diff --git a/src/redux/configuration/configuration.selectors.ts b/src/redux/configuration/configuration.selectors.ts
index 00683ccb082b98e28df71b0fc95a3e4f88ef5518..ae5b97b7bc077a3f8316f26faf57e32ce2f8a5bf 100644
--- a/src/redux/configuration/configuration.selectors.ts
+++ b/src/redux/configuration/configuration.selectors.ts
@@ -17,7 +17,9 @@ import {
   SIMPLE_COLOR_VAL_NAME_ID,
   SVG_IMAGE_HANDLER_NAME_ID,
   SEARCH_DISTANCE_NAME_ID,
+  REQUEST_ACCOUNT_EMAIL,
 } from './configuration.constants';
+
 import { ConfigurationHandlersIds, ConfigurationImageHandlersIds } from './configuration.types';
 
 const configurationSelector = createSelector(rootSelector, state => state.configuration);
@@ -56,6 +58,11 @@ export const searchDistanceValSelector = createSelector(
   state => configurationAdapterSelectors.selectById(state, SEARCH_DISTANCE_NAME_ID)?.value,
 );
 
+export const adminEmailValSelector = createSelector(
+  configurationOptionsSelector,
+  state => configurationAdapterSelectors.selectById(state, REQUEST_ACCOUNT_EMAIL)?.value,
+);
+
 export const defaultLegendImagesSelector = createSelector(configurationOptionsSelector, state =>
   LEGEND_FILE_NAMES_IDS.map(
     legendNameId => configurationAdapterSelectors.selectById(state, legendNameId)?.value,
diff --git a/src/redux/middlewares/error.middleware.test.ts b/src/redux/middlewares/error.middleware.test.ts
index 6c34711b000f25d6642c79fdf7068a4b16f47913..f8f7de9c01c87bcb615f05192f22ee758845377e 100644
--- a/src/redux/middlewares/error.middleware.test.ts
+++ b/src/redux/middlewares/error.middleware.test.ts
@@ -1,5 +1,5 @@
 import { store } from '@/redux/store';
-import { showToast } from '@/utils/showToast';
+
 import { errorMiddlewareListener } from './error.middleware';
 
 jest.mock('../../utils/showToast');
@@ -132,7 +132,7 @@ describe('errorMiddlewareListener', () => {
     );
   });
 
-  it('should toast on access denied', async () => {
+  it('should show modal on access denied', async () => {
     const action = {
       type: 'action/rejected',
       payload: null,
@@ -149,9 +149,10 @@ describe('errorMiddlewareListener', () => {
     // eslint-disable-next-line @typescript-eslint/ban-ts-comment
     // @ts-expect-error
     await errorMiddlewareListener(action, { getState, dispatch });
-    expect(showToast).toHaveBeenCalledWith({
-      message: 'Access denied.',
-      type: 'error',
-    });
+    expect(dispatchSpy).toHaveBeenCalledWith(
+      expect.objectContaining({
+        type: 'modal/openAccessDeniedModal',
+      }),
+    );
   });
 });
diff --git a/src/redux/middlewares/error.middleware.ts b/src/redux/middlewares/error.middleware.ts
index 8c6bd8160be1d71fd632d1ec542b66c48c9017fc..56ba18c0f5cfcef3435ac3d6b86aa2e8db452f45 100644
--- a/src/redux/middlewares/error.middleware.ts
+++ b/src/redux/middlewares/error.middleware.ts
@@ -1,8 +1,8 @@
 import type { AppListenerEffectAPI, AppStartListening } from '@/redux/store';
 import { Action, createListenerMiddleware, isRejected } from '@reduxjs/toolkit';
 import { createErrorData } from '@/utils/error-report/errorReporting';
-import { openErrorReportModal } from '@/redux/modal/modal.slice';
-import { showToast } from '@/utils/showToast';
+import { openAccessDeniedModal, openErrorReportModal } from '@/redux/modal/modal.slice';
+import { getProjects } from '@/redux/projects/projects.thunks';
 
 export const errorListenerMiddleware = createListenerMiddleware();
 
@@ -14,10 +14,8 @@ export const errorMiddlewareListener = async (
 ): Promise<void> => {
   if (isRejected(action) && action.type !== 'user/getSessionValid/rejected') {
     if (action.error.code === '403') {
-      showToast({
-        type: 'error',
-        message: 'Access denied.',
-      });
+      dispatch(getProjects());
+      dispatch(openAccessDeniedModal());
     } else {
       const errorData = await createErrorData(action.error, getState());
       dispatch(openErrorReportModal(errorData));
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
index c52c9cebc2596b2d31228519c88b5583d41311f9..f9d88800610efc49275e3cdad86a19a2396a4d97 100644
--- a/src/redux/modal/modal.reducers.ts
+++ b/src/redux/modal/modal.reducers.ts
@@ -67,6 +67,12 @@ export const openErrorReportModalReducer = (
   };
 };
 
+export const openAccessDeniedModalReducer = (state: ModalState): void => {
+  state.isOpen = true;
+  state.modalName = 'access-denied';
+  state.modalTitle = 'Access denied!';
+};
+
 export const setOverviewImageIdReducer = (
   state: ModalState,
   action: PayloadAction<number>,
diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts
index 4b7fc45a1227026975d411deb49700324ceb9455..57d852cd19596f39ccb2476e0aae599d4927be4f 100644
--- a/src/redux/modal/modal.slice.ts
+++ b/src/redux/modal/modal.slice.ts
@@ -12,6 +12,7 @@ import {
   openLoggedInMenuModalReducer,
   openAddCommentModalReducer,
   openErrorReportModalReducer,
+  openAccessDeniedModalReducer,
 } from './modal.reducers';
 
 const modalSlice = createSlice({
@@ -29,6 +30,7 @@ const modalSlice = createSlice({
     openEditOverlayModal: openEditOverlayModalReducer,
     openLoggedInMenuModal: openLoggedInMenuModalReducer,
     openErrorReportModal: openErrorReportModalReducer,
+    openAccessDeniedModal: openAccessDeniedModalReducer,
   },
 });
 
@@ -44,6 +46,7 @@ export const {
   openEditOverlayModal,
   openLoggedInMenuModal,
   openErrorReportModal,
+  openAccessDeniedModal,
 } = modalSlice.actions;
 
 export default modalSlice.reducer;
diff --git a/src/redux/oauth/oauth.constants.ts b/src/redux/oauth/oauth.constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc92f1da5ab570919841515581dd6be8a4988ea2
--- /dev/null
+++ b/src/redux/oauth/oauth.constants.ts
@@ -0,0 +1 @@
+export const OAUTH_FETCHING_ERROR_PREFIX = 'Failed to fetch oauth providers';
diff --git a/src/redux/oauth/oauth.fixtures.ts b/src/redux/oauth/oauth.fixtures.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5b8212184250c69c348ce5fae7c8ac8392a914b8
--- /dev/null
+++ b/src/redux/oauth/oauth.fixtures.ts
@@ -0,0 +1,5 @@
+import { OAuth } from '@/types/models';
+
+export const initialOAuthFixture: OAuth = {
+  Orcid: undefined,
+};
diff --git a/src/redux/oauth/oauth.mock.ts b/src/redux/oauth/oauth.mock.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b1b18f8f02b1e79c503f6729793a82721cbf956e
--- /dev/null
+++ b/src/redux/oauth/oauth.mock.ts
@@ -0,0 +1,10 @@
+import { DEFAULT_ERROR } from '@/constants/errors';
+import { OauthState } from '@/redux/oauth/oauth.types';
+import { initialOAuthFixture } from '@/redux/oauth/oauth.fixtures';
+
+export const OAUTH_INITIAL_STATE_MOCK: OauthState = {
+  data: initialOAuthFixture,
+  loading: 'idle',
+  error: DEFAULT_ERROR,
+  orcidEndpoint: undefined,
+};
diff --git a/src/redux/oauth/oauth.reducers.ts b/src/redux/oauth/oauth.reducers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e5cce6516930ce2c66cfc37c86b864a6ff8f740
--- /dev/null
+++ b/src/redux/oauth/oauth.reducers.ts
@@ -0,0 +1,20 @@
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import { OauthState } from '@/redux/oauth/oauth.types';
+import { getOAuth } from '@/redux/oauth/oauth.thunks';
+
+export const getOauthReducer = (builder: ActionReducerMapBuilder<OauthState>): void => {
+  builder.addCase(getOAuth.pending, state => {
+    state.loading = 'pending';
+  });
+  builder.addCase(getOAuth.fulfilled, (state, action) => {
+    state.data = action.payload || undefined;
+    if (action.payload && action.payload.Orcid) {
+      state.orcidEndpoint = action.payload.Orcid;
+    }
+    state.loading = 'succeeded';
+  });
+  builder.addCase(getOAuth.rejected, state => {
+    state.loading = 'failed';
+    // TODO to discuss manage state of failure
+  });
+};
diff --git a/src/redux/oauth/oauth.selectors.ts b/src/redux/oauth/oauth.selectors.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8bda1780c56eb0804cfbc0e53935a6d6e492fc0d
--- /dev/null
+++ b/src/redux/oauth/oauth.selectors.ts
@@ -0,0 +1,6 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { rootSelector } from '../root/root.selectors';
+
+export const oauthSelector = createSelector(rootSelector, state => state.oauth);
+
+export const orcidEndpointSelector = createSelector(oauthSelector, oauth => oauth?.orcidEndpoint);
diff --git a/src/redux/oauth/oauth.slice.ts b/src/redux/oauth/oauth.slice.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8abb3ba8feb2a9b9414d529710cdbfc367f402ca
--- /dev/null
+++ b/src/redux/oauth/oauth.slice.ts
@@ -0,0 +1,21 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { OauthState } from '@/redux/oauth/oauth.types';
+import { getOauthReducer } from '@/redux/oauth/oauth.reducers';
+
+const initialState: OauthState = {
+  data: undefined,
+  loading: 'idle',
+  error: { name: '', message: '' },
+  orcidEndpoint: undefined,
+};
+
+const oauthSlice = createSlice({
+  name: 'oauth',
+  initialState,
+  reducers: {},
+  extraReducers: builder => {
+    getOauthReducer(builder);
+  },
+});
+
+export default oauthSlice.reducer;
diff --git a/src/redux/oauth/oauth.thunks.ts b/src/redux/oauth/oauth.thunks.ts
new file mode 100644
index 0000000000000000000000000000000000000000..42907946419bc86ca8befe0794a16661aec26da7
--- /dev/null
+++ b/src/redux/oauth/oauth.thunks.ts
@@ -0,0 +1,24 @@
+import { axiosInstance } from '@/services/api/utils/axiosInstance';
+import { OAuth } from '@/types/models';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { ThunkConfig } from '@/types/store';
+import { getError } from '@/utils/error-report/getError';
+import { oauthSchema } from '@/models/oauthSchema';
+import { OAUTH_FETCHING_ERROR_PREFIX } from '@/redux/oauth/oauth.constants';
+import { apiPath } from '../apiPath';
+
+export const getOAuth = createAsyncThunk<OAuth | undefined, void, ThunkConfig>(
+  'oauth/getProviders',
+  async () => {
+    try {
+      const response = await axiosInstance.get<OAuth>(apiPath.getOauthProviders());
+
+      const isDataValid = validateDataUsingZodSchema(response.data, oauthSchema);
+
+      return isDataValid ? response.data : undefined;
+    } catch (error) {
+      return Promise.reject(getError({ error, prefix: OAUTH_FETCHING_ERROR_PREFIX }));
+    }
+  },
+);
diff --git a/src/redux/oauth/oauth.types.ts b/src/redux/oauth/oauth.types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2992b701903eca04a3792cfed87a85b5388ca170
--- /dev/null
+++ b/src/redux/oauth/oauth.types.ts
@@ -0,0 +1,9 @@
+import { OAuth } from '@/types/models';
+import { Loading } from '@/types/loadingState';
+
+export type OauthState = {
+  orcidEndpoint: string | undefined;
+  data: OAuth | undefined;
+  loading: Loading;
+  error: Error;
+};
diff --git a/src/redux/projects/projects.constants.ts b/src/redux/projects/projects.constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a7fb0f8bc731b88c03942145baf6c4f4413b8df4
--- /dev/null
+++ b/src/redux/projects/projects.constants.ts
@@ -0,0 +1 @@
+export const PROJECTS_FETCHING_ERROR_PREFIX = 'Failed to fetch projects';
diff --git a/src/redux/projects/projects.mock.ts b/src/redux/projects/projects.mock.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e5b9c93da402d13feee78fcce2e02792fc80216c
--- /dev/null
+++ b/src/redux/projects/projects.mock.ts
@@ -0,0 +1,8 @@
+import { DEFAULT_ERROR } from '@/constants/errors';
+import { ProjectsState } from '@/redux/projects/projects.types';
+
+export const PROJECTS_STATE_INITIAL_MOCK: ProjectsState = {
+  data: [],
+  loading: 'idle',
+  error: DEFAULT_ERROR,
+};
diff --git a/src/redux/projects/projects.reducers.test.ts b/src/redux/projects/projects.reducers.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2912689e2e33d2fd4b8c30e2ad2b858e2def6a28
--- /dev/null
+++ b/src/redux/projects/projects.reducers.test.ts
@@ -0,0 +1,76 @@
+import { projectPageFixture } from '@/models/fixtures/projectsFixture';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import { unwrapResult } from '@reduxjs/toolkit';
+import { ProjectsState } from '@/redux/projects/projects.types';
+import { getProjects } from '@/redux/projects/projects.thunks';
+import { apiPath } from '../apiPath';
+import projectsReducer from './projects.slice';
+
+const mockedAxiosClient = mockNetworkNewAPIResponse();
+
+const INITIAL_STATE: ProjectsState = {
+  data: [],
+  loading: 'idle',
+  error: { name: '', message: '' },
+};
+
+describe('projects reducer', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<ProjectsState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('projects', projectsReducer);
+  });
+
+  it('should match initial state', () => {
+    const action = { type: 'unknown' };
+
+    expect(projectsReducer(undefined, action)).toEqual(INITIAL_STATE);
+  });
+  it('should update store after successfull getProjects query', async () => {
+    mockedAxiosClient.onGet(apiPath.getProjects()).reply(HttpStatusCode.Ok, projectPageFixture);
+
+    const { type } = await store.dispatch(getProjects());
+    const { data, loading, error } = store.getState().projects;
+
+    expect(type).toBe('project/getProjects/fulfilled');
+    expect(loading).toEqual('succeeded');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual(projectPageFixture.content);
+  });
+
+  it('should update store after failed getProjects query', async () => {
+    mockedAxiosClient.onGet(apiPath.getProjects()).reply(HttpStatusCode.NotFound, []);
+
+    const action = await store.dispatch(getProjects());
+    const { data, loading, error } = store.getState().projects;
+
+    expect(action.type).toBe('project/getProjects/rejected');
+    expect(() => unwrapResult(action)).toThrow(
+      "Failed to fetch projects: The page you're looking for doesn't exist. Please verify the URL and try again.",
+    );
+    expect(loading).toEqual('failed');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual([]);
+  });
+
+  it('should update store on loading getProjects query', async () => {
+    mockedAxiosClient.onGet(apiPath.getProjects()).reply(HttpStatusCode.Ok, projectPageFixture);
+
+    const actionPromise = store.dispatch(getProjects());
+
+    const { data, loading } = store.getState().projects;
+    expect(data).toEqual([]);
+    expect(loading).toEqual('pending');
+
+    actionPromise.then(() => {
+      const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().projects;
+
+      expect(dataPromiseFulfilled).toEqual(projectPageFixture.content);
+      expect(promiseFulfilled).toEqual('succeeded');
+    });
+  });
+});
diff --git a/src/redux/projects/projects.reducers.ts b/src/redux/projects/projects.reducers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..49c8a58b0fec82c7fb7b7db5d201ecfc2e3a75bb
--- /dev/null
+++ b/src/redux/projects/projects.reducers.ts
@@ -0,0 +1,17 @@
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import { ProjectsState } from '@/redux/projects/projects.types';
+import { getProjects } from '@/redux/projects/projects.thunks';
+
+export const getProjectsReducer = (builder: ActionReducerMapBuilder<ProjectsState>): void => {
+  builder.addCase(getProjects.pending, state => {
+    state.loading = 'pending';
+  });
+  builder.addCase(getProjects.fulfilled, (state, action) => {
+    state.data = action.payload || undefined;
+    state.loading = 'succeeded';
+  });
+  builder.addCase(getProjects.rejected, state => {
+    state.loading = 'failed';
+    // TODO to discuss manage state of failure
+  });
+};
diff --git a/src/redux/projects/projects.selectors.ts b/src/redux/projects/projects.selectors.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fb4ed5c4ec056e07bf3d242f96da94549def1f44
--- /dev/null
+++ b/src/redux/projects/projects.selectors.ts
@@ -0,0 +1,5 @@
+import { rootSelector } from '@/redux/root/root.selectors';
+import { createSelector } from '@reduxjs/toolkit';
+
+export const projectsRootSelector = createSelector(rootSelector, state => state.projects);
+export const projectsSelector = createSelector(projectsRootSelector, state => state.data);
diff --git a/src/redux/projects/projects.slice.ts b/src/redux/projects/projects.slice.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3da9cb6525f5dcc9e89d532a441438a4b2adbfde
--- /dev/null
+++ b/src/redux/projects/projects.slice.ts
@@ -0,0 +1,21 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { ProjectsState } from '@/redux/projects/projects.types';
+
+import { getProjectsReducer } from '@/redux/projects/projects.reducers';
+
+const initialState: ProjectsState = {
+  data: [],
+  loading: 'idle',
+  error: { name: '', message: '' },
+};
+
+const projectsSlice = createSlice({
+  name: 'project',
+  initialState,
+  reducers: {},
+  extraReducers: builder => {
+    getProjectsReducer(builder);
+  },
+});
+
+export default projectsSlice.reducer;
diff --git a/src/redux/projects/projects.thunks.ts b/src/redux/projects/projects.thunks.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b3715ea773daa96201d55d43f6c4be53fd9da86c
--- /dev/null
+++ b/src/redux/projects/projects.thunks.ts
@@ -0,0 +1,25 @@
+import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
+import { projectSchema } from '@/models/projectSchema';
+import { PageOf, Project } from '@/types/models';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { ThunkConfig } from '@/types/store';
+import { getError } from '@/utils/error-report/getError';
+import { PROJECTS_FETCHING_ERROR_PREFIX } from '@/redux/projects/projects.constants';
+import { pageableSchema } from '@/models/pageableSchema';
+import { apiPath } from '../apiPath';
+
+export const getProjects = createAsyncThunk<Project[], void, ThunkConfig>(
+  'project/getProjects',
+  async () => {
+    try {
+      const response = await axiosInstanceNewAPI.get<PageOf<Project>>(apiPath.getProjects());
+
+      const isDataValid = validateDataUsingZodSchema(response.data, pageableSchema(projectSchema));
+
+      return isDataValid ? response.data.content : [];
+    } catch (error) {
+      return Promise.reject(getError({ error, prefix: PROJECTS_FETCHING_ERROR_PREFIX }));
+    }
+  },
+);
diff --git a/src/redux/projects/projects.types.ts b/src/redux/projects/projects.types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7448a70129808f96ab110e4ee3d68875b55c7939
--- /dev/null
+++ b/src/redux/projects/projects.types.ts
@@ -0,0 +1,4 @@
+import { Project } from '@/types/models';
+import { FetchDataState } from '@/types/fetchDataState';
+
+export type ProjectsState = FetchDataState<Project[], []>;
diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts
index 9c5e6df4433c43af3513df7ed427b22681695bc5..421151bd9d870052cc5137a3f66f7c3451b494e8 100644
--- a/src/redux/root/root.fixtures.ts
+++ b/src/redux/root/root.fixtures.ts
@@ -1,4 +1,6 @@
 import { CONSTANT_INITIAL_STATE } from '@/redux/constant/constant.adapter';
+import { PROJECTS_STATE_INITIAL_MOCK } from '@/redux/projects/projects.mock';
+import { OAUTH_INITIAL_STATE_MOCK } from '@/redux/oauth/oauth.mock';
 import { COMMENT_INITIAL_STATE_MOCK } from '@/redux/comment/comment.mock';
 import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock';
 import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock';
@@ -30,6 +32,7 @@ import { USER_INITIAL_STATE_MOCK } from '../user/user.mock';
 export const INITIAL_STORE_STATE_MOCK: RootState = {
   search: SEARCH_STATE_INITIAL_MOCK,
   project: PROJECT_STATE_INITIAL_MOCK,
+  projects: PROJECTS_STATE_INITIAL_MOCK,
   drugs: DRUGS_INITIAL_STATE_MOCK,
   chemicals: CHEMICALS_INITIAL_STATE_MOCK,
   models: MODELS_INITIAL_STATE_MOCK,
@@ -37,6 +40,7 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
   backgrounds: BACKGROUND_INITIAL_STATE_MOCK,
   drawer: drawerInitialStateMock,
   map: initialMapStateFixture,
+  oauth: OAUTH_INITIAL_STATE_MOCK,
   overlays: OVERLAYS_INITIAL_STATE_MOCK,
   reactions: REACTIONS_STATE_INITIAL_MOCK,
   configuration: CONFIGURATION_INITIAL_STATE,
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 3e957717d60ad91e097b46beca519bbc8f3dab33..ebc65642cab5db9e0a830989779bdaaceec80450 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -10,9 +10,11 @@ import drugsReducer from '@/redux/drugs/drugs.slice';
 import mapReducer from '@/redux/map/map.slice';
 import modalReducer from '@/redux/modal/modal.slice';
 import modelsReducer from '@/redux/models/models.slice';
+import oauthReducer from '@/redux/oauth/oauth.slice';
 import overlayBioEntityReducer from '@/redux/overlayBioEntity/overlayBioEntity.slice';
 import overlaysReducer from '@/redux/overlays/overlays.slice';
 import projectReducer from '@/redux/project/project.slice';
+import projectsReducer from '@/redux/projects/projects.slice';
 import reactionsReducer from '@/redux/reactions/reactions.slice';
 import searchReducer from '@/redux/search/search.slice';
 import userReducer from '@/redux/user/user.slice';
@@ -38,6 +40,7 @@ import statisticsReducer from './statistics/statistics.slice';
 export const reducers = {
   search: searchReducer,
   project: projectReducer,
+  projects: projectsReducer,
   drugs: drugsReducer,
   chemicals: chemicalsReducer,
   bioEntity: bioEntityReducer,
@@ -63,6 +66,7 @@ export const reducers = {
   plugins: pluginsReducer,
   markers: markersReducer,
   entityNumber: entityNumberReducer,
+  oauth: oauthReducer,
 };
 
 export const middlewares = [mapListenerMiddleware.middleware, errorListenerMiddleware.middleware];
diff --git a/src/types/modal.ts b/src/types/modal.ts
index 768f836f968f325dacf5ed073d2d0dc5d88008fb..90e7c20d501aa6c7cb50642df1eea295c6406816 100644
--- a/src/types/modal.ts
+++ b/src/types/modal.ts
@@ -7,4 +7,5 @@ export type ModalName =
   | 'publications'
   | 'edit-overlay'
   | 'error-report'
+  | 'access-denied'
   | 'logged-in-menu';
diff --git a/src/types/models.ts b/src/types/models.ts
index 53d477a7f88a2dcb40b451db206236aa3e9fd86d..c55955ac291dc9571cd2879f730777ebb663682e 100644
--- a/src/types/models.ts
+++ b/src/types/models.ts
@@ -64,6 +64,7 @@ import { z } from 'zod';
 import { commentSchema } from '@/models/commentSchema';
 import { userSchema } from '@/models/userSchema';
 import { javaStacktraceSchema } from '@/models/javaStacktraceSchema';
+import { oauthSchema } from '@/models/oauthSchema';
 
 export type Project = z.infer<typeof projectSchema>;
 export type OverviewImageView = z.infer<typeof overviewImageView>;
@@ -124,3 +125,14 @@ export type MarkerWithPosition = z.infer<typeof markerWithPositionSchema>;
 export type Marker = z.infer<typeof markerSchema>;
 export type JavaStacktrace = z.infer<typeof javaStacktraceSchema>;
 export type Comment = z.infer<typeof commentSchema>;
+
+export type PageOf<T> = {
+  totalPages: number;
+  totalElements: number;
+  numberOfElements: number;
+  size: number;
+  number: number;
+  content: T[];
+};
+
+export type OAuth = z.infer<typeof oauthSchema>;
diff --git a/src/utils/validateDataUsingZodSchema.ts b/src/utils/validateDataUsingZodSchema.ts
index 673e253dcbbd2f25ca32612d2e67393adf729e26..5171a271449cfe39e46b6a2449d56b7cde4a1e95 100644
--- a/src/utils/validateDataUsingZodSchema.ts
+++ b/src/utils/validateDataUsingZodSchema.ts
@@ -6,9 +6,11 @@ export const validateDataUsingZodSchema: IsApiResponseValid = (data, schema: Zod
   const validationResults = schema.safeParse(data);
 
   if (validationResults.success === false) {
-    // TODO - probably need to rething way of handling parsing errors, for now let's leave it to console.log
+    // TODO - probably need to rethink way of handling parsing errors, for now let's leave it to console.log
     // eslint-disable-next-line no-console
-    console.error('Error on parsing data', validationResults.error.message);
+    console.error('Error on parsing data', validationResults.error);
+    // eslint-disable-next-line no-console
+    console.error(validationResults.error.message);
   }
 
   return validationResults.success;