diff --git a/src/frontend/app/desk/.env.development b/src/frontend/app/desk/.env.development
new file mode 100644
index 0000000..472706a
--- /dev/null
+++ b/src/frontend/app/desk/.env.development
@@ -0,0 +1,3 @@
+NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080/
+NEXT_PUBLIC_KEYCLOAK_REALM=people
+NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=people-front
diff --git a/src/frontend/app/desk/package.json b/src/frontend/app/desk/package.json
index 984d7ea..2aaaaf7 100644
--- a/src/frontend/app/desk/package.json
+++ b/src/frontend/app/desk/package.json
@@ -17,7 +17,8 @@
"@openfun/cunningham-react": "2.4.0",
"next": "14.0.4",
"react": "18.2.0",
- "react-dom": "18.2.0"
+ "react-dom": "18.2.0",
+ "zustand": "4.4.7"
},
"devDependencies": {
"@testing-library/jest-dom": "6.2.0",
@@ -35,6 +36,7 @@
"eslint-plugin-testing-library": "6.2.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
+ "keycloak-js": "23.0.3",
"prettier": "3.1.1",
"stylelint": "16.1.0",
"stylelint-config-standard": "36.0.0",
diff --git a/src/frontend/app/desk/src/app/page.tsx b/src/frontend/app/desk/src/app/page.tsx
index f274349..8905987 100644
--- a/src/frontend/app/desk/src/app/page.tsx
+++ b/src/frontend/app/desk/src/app/page.tsx
@@ -1,10 +1,23 @@
'use client';
-import { Button } from '@openfun/cunningham-react';
+import { Button, Loader } from '@openfun/cunningham-react';
+import { useEffect } from 'react';
import styles from './page.module.css';
+import useAuthStore from '@/auth/useAuthStore';
+
export default function Home() {
+ const { initAuth, authenticated } = useAuthStore();
+
+ useEffect(() => {
+ initAuth();
+ }, [initAuth]);
+
+ if (!authenticated) {
+ return ;
+ }
+
return (
Hello world!
diff --git a/src/frontend/app/desk/src/auth/keycloak.ts b/src/frontend/app/desk/src/auth/keycloak.ts
new file mode 100644
index 0000000..2e386fe
--- /dev/null
+++ b/src/frontend/app/desk/src/auth/keycloak.ts
@@ -0,0 +1,23 @@
+import Keycloak, { KeycloakConfig } from 'keycloak-js';
+
+const keycloakConfig: KeycloakConfig = {
+ url: process.env.NEXT_PUBLIC_KEYCLOAK_URL,
+ realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM || '',
+ clientId: process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || '',
+};
+
+export const initKeycloak = (onAuthSuccess: (_token?: string) => void) => {
+ const keycloak = new Keycloak(keycloakConfig);
+ keycloak
+ .init({
+ onLoad: 'login-required',
+ })
+ .then((authenticated) => {
+ if (authenticated) {
+ onAuthSuccess(keycloak.token);
+ }
+ })
+ .catch(() => {
+ throw new Error('Failed to initialize Keycloak.');
+ });
+};
diff --git a/src/frontend/app/desk/src/auth/useAuthStore.tsx b/src/frontend/app/desk/src/auth/useAuthStore.tsx
new file mode 100644
index 0000000..2e4c3c3
--- /dev/null
+++ b/src/frontend/app/desk/src/auth/useAuthStore.tsx
@@ -0,0 +1,32 @@
+import { create } from 'zustand';
+
+import { initKeycloak } from './keycloak';
+
+interface AuthStore {
+ initialized: boolean;
+ authenticated: boolean;
+ token: string | null;
+ initAuth: () => void;
+}
+
+const useAuthStore = create((set) => ({
+ initialized: false,
+ authenticated: false,
+ token: null,
+
+ initAuth: () =>
+ set((state) => {
+ if (process.env.NODE_ENV === 'development' && !state.initialized) {
+ initKeycloak((token) => set({ authenticated: true, token }));
+ return { initialized: true };
+ }
+
+ /**
+ * TODO: Implement OIDC production authentication
+ */
+
+ return {};
+ }),
+}));
+
+export default useAuthStore;
diff --git a/src/frontend/app/desk/yarn.lock b/src/frontend/app/desk/yarn.lock
index f1808f8..40e06eb 100644
--- a/src/frontend/app/desk/yarn.lock
+++ b/src/frontend/app/desk/yarn.lock
@@ -2657,6 +2657,11 @@ balanced-match@^2.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
+base64-js@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -4752,6 +4757,11 @@ jest@29.7.0:
import-local "^3.0.2"
jest-cli "^29.7.0"
+js-sha256@^0.10.1:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.10.1.tgz#b40104ba1368e823fdd5f41b66b104b15a0da60d"
+ integrity sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -4856,6 +4866,20 @@ json5@^2.2.3:
object.assign "^4.1.4"
object.values "^1.1.6"
+jwt-decode@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
+ integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
+
+keycloak-js@23.0.3:
+ version "23.0.3"
+ resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-23.0.3.tgz#91c71ac5c8e3bbafe9c7e5b8ef81756ffdd023ac"
+ integrity sha512-T28rdeRgGdOvIUMl6Wo9IiHEGcDxZhUZxXBwk3EwDFWrn8uhWBngDFxdsdsv7qGGKAMKrkZVL2JN/h81sllN7A==
+ dependencies:
+ base64-js "^1.5.1"
+ js-sha256 "^0.10.1"
+ jwt-decode "^4.0.0"
+
keyv@^4.5.3, keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -6282,6 +6306,11 @@ url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
+use-sync-external-store@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
+ integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
+
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -6513,3 +6542,10 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zustand@4.4.7:
+ version "4.4.7"
+ resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c"
+ integrity sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==
+ dependencies:
+ use-sync-external-store "1.2.0"