From ef1b036dcc0e872d59927236fb2b4f2cf9e38d48 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Mon, 17 Feb 2025 09:26:12 +0100 Subject: [PATCH] =?UTF-8?q?chore:=20WIP=20uilisant=20d'un=20CSRF=20global?= =?UTF-8?q?=20=C3=A0=20l'appli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Auth/urls.py | 5 +- Back-End/Auth/views.py | 36 +-- Back-End/N3wtSchool/settings.py | 35 ++- Back-End/requirements.txt | Bin 2414 -> 2490 bytes Front-End/package-lock.json | 252 +++++++++++++++++- Front-End/package.json | 2 + .../src/app/[locale]/users/login/page.js | 102 +++---- Front-End/src/app/layout.js | 11 +- Front-End/src/app/lib/authAction.js | 2 +- Front-End/src/context/CsrfContext.js | 39 +++ Front-End/src/csrfMiddleware.js | 12 + Front-End/src/hooks/useCsrfToken.js | 50 ++-- Front-End/src/pages/api/auth/[...nextauth].js | 79 ++++++ Front-End/src/pages/api/auth/signin.js | 22 ++ Front-End/src/pages/api/auth/signout.js | 9 + Front-End/src/pages/protected-page.js | 42 +++ Front-End/src/utils/Url.js | 1 + Front-End/src/utils/getCsrfToken.js | 9 + 18 files changed, 563 insertions(+), 145 deletions(-) create mode 100644 Front-End/src/context/CsrfContext.js create mode 100644 Front-End/src/csrfMiddleware.js create mode 100644 Front-End/src/pages/api/auth/[...nextauth].js create mode 100644 Front-End/src/pages/api/auth/signin.js create mode 100644 Front-End/src/pages/api/auth/signout.js create mode 100644 Front-End/src/pages/protected-page.js create mode 100644 Front-End/src/utils/getCsrfToken.js diff --git a/Back-End/Auth/urls.py b/Back-End/Auth/urls.py index 32b1331..d16a5f9 100644 --- a/Back-End/Auth/urls.py +++ b/Back-End/Auth/urls.py @@ -11,11 +11,8 @@ urlpatterns = [ re_path(r'^subscribe$', SubscribeView.as_view(), name='subscribe'), re_path(r'^newPassword$', NewPasswordView.as_view(), name='newPassword'), re_path(r'^resetPassword/(?P[a-zA-Z]+)$', ResetPasswordView.as_view(), name='resetPassword'), - re_path(r'^infoSession$', Auth.views.infoSession, name='infoSession'), + re_path(r'^infoSession$', SessionView.as_view(), name='infoSession'), re_path(r'^profiles$', ProfileView.as_view(), name="profile"), re_path(r'^profiles/(?P[0-9]+)$', ProfileSimpleView.as_view(), name="profile"), - - # Test SESSION VIEW - re_path(r'^session$', SessionView.as_view(), name="session"), ] \ No newline at end of file diff --git a/Back-End/Auth/views.py b/Back-End/Auth/views.py index 2838fc5..16c71dc 100644 --- a/Back-End/Auth/views.py +++ b/Back-End/Auth/views.py @@ -29,6 +29,8 @@ import Subscriptions.util as util from N3wtSchool import bdd, error +from rest_framework_simplejwt.authentication import JWTAuthentication + @swagger_auto_schema( method='get', @@ -57,7 +59,7 @@ class SessionView(APIView): 401: openapi.Response('Session invalide') } ) - def post(self, request): + def get(self, request): token = request.META.get('HTTP_AUTHORIZATION', '').split('Bearer ')[-1] try: @@ -146,27 +148,6 @@ class ProfileSimpleView(APIView): def delete(self, request, id): return bdd.delete_object(Profile, id) - -@swagger_auto_schema( - method='get', - operation_description="Obtenir les informations de session", - responses={200: openapi.Response('Informations de session', schema=openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - 'cacheSession': openapi.Schema(type=openapi.TYPE_BOOLEAN), - 'typeProfil': openapi.Schema(type=openapi.TYPE_STRING), - 'username': openapi.Schema(type=openapi.TYPE_STRING) - } - ))} -) -@api_view(['GET']) -def infoSession(request): - profilCache = cache.get('session_cache') - if profilCache: - return JsonResponse({"cacheSession":True,"typeProfil":profilCache.droit, "username":profilCache.email}, safe=False) - else: - return JsonResponse({"cacheSession":False,"typeProfil":Profile.Droits.PROFIL_UNDEFINED, "username":""}, safe=False) - @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class LoginView(APIView): @@ -195,7 +176,7 @@ class LoginView(APIView): def post(self, request): data=JSONParser().parse(request) validatorAuthentication = validator.ValidatorAuthentication(data=data) - retour = error.returnMessage[error.WRONGid] + retour = error.returnMessage[error.WRONG_ID] validationOk, errorFields = validatorAuthentication.validate() user = None if validationOk: @@ -212,15 +193,8 @@ class LoginView(APIView): retour = '' else: retour = error.returnMessage[error.PROFIL_INACTIVE] - - # Génération du token JWT - # jwt_token = jwt.encode({ - # 'id': user.id, - # 'email': user.email, - # 'role': "admin" - # }, settings.SECRET_KEY, algorithm='HS256') else: - retour = error.returnMessage[error.WRONGid] + retour = error.returnMessage[error.WRONG_ID] return JsonResponse({ diff --git a/Back-End/N3wtSchool/settings.py b/Back-End/N3wtSchool/settings.py index 101eb13..1ce52b6 100644 --- a/Back-End/N3wtSchool/settings.py +++ b/Back-End/N3wtSchool/settings.py @@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/5.0/ref/settings/ from pathlib import Path import json import os +from datetime import timedelta # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -53,6 +54,7 @@ INSTALLED_APPS = [ 'django_celery_beat', 'N3wtSchool', 'drf_yasg', + 'rest_framework_simplejwt' ] MIDDLEWARE = [ @@ -250,10 +252,19 @@ CORS_ALLOW_ALL_HEADERS = True CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [ - os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000') + 'http://localhost:3000' ] -CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://localhost:8080').split(',') +CSRF_TRUSTED_ORIGINS = [ + 'http://localhost:3000', + 'http://localhost:8080' +] + +# CORS_ALLOWED_ORIGINS = [ +# os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000') +# ] + +# CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://localhost:8080').split(',') CSRF_COOKIE_HTTPONLY = False CSRF_COOKIE_SECURE = False @@ -289,7 +300,10 @@ NB_MAX_PAGE = 100 REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomPagination', - 'PAGE_SIZE': NB_RESULT_PER_PAGE + 'PAGE_SIZE': NB_RESULT_PER_PAGE, + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), } CELERY_BROKER_URL = 'redis://redis:6379/0' @@ -308,3 +322,18 @@ REDIS_DB = 0 REDIS_PASSWORD = None SECRET_KEY = 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3' +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', +} + diff --git a/Back-End/requirements.txt b/Back-End/requirements.txt index b12640ee3c819d960d758d0b9b938562459f13c7..11ec216ce424226521876445e69c486c7470a461 100644 GIT binary patch delta 50 zcmaDSv`cscAIsz`%qoiU48;tY47m&i3^@#`3|S223?&S=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "license": "MIT", @@ -1651,6 +1662,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/busboy": { "version": "1.6.0", "dependencies": { @@ -2092,6 +2109,15 @@ "version": "0.2.0", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.47", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz", @@ -3715,6 +3741,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -3768,6 +3803,28 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "dev": true, @@ -3782,6 +3839,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -3863,11 +3941,53 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -3963,7 +4083,6 @@ }, "node_modules/ms": { "version": "2.1.3", - "devOptional": true, "license": "MIT" }, "node_modules/mz": { @@ -4045,6 +4164,38 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-intl": { "version": "3.24.0", "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.24.0.tgz", @@ -4157,6 +4308,12 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -4282,6 +4439,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -4308,6 +4474,42 @@ "fn.name": "1.x.x" } }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -4631,6 +4833,28 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/preact": { + "version": "10.25.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz", + "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -4639,6 +4863,12 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", @@ -5101,9 +5331,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "optional": true, - "peer": true + ] }, "node_modules/safe-regex-test": { "version": "1.0.3", @@ -5138,7 +5366,6 @@ }, "node_modules/semver": { "version": "7.6.3", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5917,6 +6144,15 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "license": "ISC", @@ -6136,6 +6372,12 @@ "dev": true, "license": "ISC" }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/yaml": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", diff --git a/Front-End/package.json b/Front-End/package.json index 4eaf8d8..9677262 100644 --- a/Front-End/package.json +++ b/Front-End/package.json @@ -15,9 +15,11 @@ "date-fns": "^4.1.0", "framer-motion": "^11.11.11", "ics": "^3.8.1", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "lucide-react": "^0.453.0", "next": "14.2.11", + "next-auth": "^4.24.11", "next-intl": "^3.24.0", "next-logger": "^5.0.1", "pino": "^9.6.0", diff --git a/Front-End/src/app/[locale]/users/login/page.js b/Front-End/src/app/[locale]/users/login/page.js index 783872f..f980710 100644 --- a/Front-End/src/app/[locale]/users/login/page.js +++ b/Front-End/src/app/[locale]/users/login/page.js @@ -1,7 +1,6 @@ 'use client' -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import DjangoCSRFToken from '@/components/DjangoCSRFToken' - import Logo from '@/components/Logo'; import { useSearchParams, useRouter } from 'next/navigation' import InputTextIcon from '@/components/InputTextIcon'; @@ -9,13 +8,11 @@ import Loader from '@/components/Loader'; // Importez le composant Loader import Button from '@/components/Button'; // Importez le composant Button import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires import { - FE_ADMIN_SUBSCRIPTIONS_EDIT_URL, - FE_ADMIN_SUBSCRIPTIONS_URL, - FE_PARENTS_HOME_URL, - FE_USERS_NEW_PASSWORD_URL } from '@/utils/Url'; + FE_USERS_NEW_PASSWORD_URL, + BE_AUTH_INFO_SESSION } from '@/utils/Url'; import useLocalStorage from '@/hooks/useLocalStorage'; -import useCsrfToken from '@/hooks/useCsrfToken'; -import { login } from '@/app/lib/authAction'; +import { signIn } from 'next-auth/react'; +import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -30,77 +27,38 @@ export default function Page() { const [userId, setUserId] = useLocalStorage("userId", '') ; const router = useRouter(); - const csrfToken = useCsrfToken(); - + const csrfToken = useCsrfToken(); // Utilisez le hook useCsrfToken function isOK(data) { return data.errorMessage === "" } function handleFormLogin(formData) { - if (useFakeData) { - // Simuler une réponse réussie - const data = { - errorFields: {}, - errorMessage: "", - profil: "fakeProfileId" - }; - setUserFieldError("") - setPasswordFieldError("") - setErrorMessage("") - if(isOK(data)){ - localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur - router.push(`${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?id=${data.profil}`); - } else { - if(data.errorFields){ - setUserFieldError(data.errorFields.email) - setPasswordFieldError(data.errorFields.password); - } - if(data.errorMessage){ - setErrorMessage(data.errorMessage) - } - } - } else { - const data = { - email: formData.get('login'), - password: formData.get('password'), - } - login(data,csrfToken) - .then(data => { - console.log('Success:', data); - setUserFieldError("") - setPasswordFieldError("") - setErrorMessage("") - if(isOK(data)){ - localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur - if (data.droit == 0) { - // Vue ECOLE - } else if (data.droit == 1) { - // Vue ADMIN - router.push(`${FE_ADMIN_SUBSCRIPTIONS_URL}`); - } else if (data.droit == 2) { - // Vue PARENT - router.push(`${FE_PARENTS_HOME_URL}`); - } else { - // Cas anormal - } + setIsLoading(true); + console.log('Form Data', Object.fromEntries(formData.entries())); // Affichez les entrées du FormData + console.log('csrf passé ', csrfToken); // Affichez le token CSRF - } else { - if(data.errorFields){ - setUserFieldError(data.errorFields.email) - setPasswordFieldError(data.errorFields.password); - } - if(data.errorMessage){ - setErrorMessage(data.errorMessage) - } - } - }) - .catch(error => { - console.error('Error fetching data:', error); - error = error.message; - console.log(error); - }); - } + signIn('credentials', { + redirect: false, + email: formData.get('login'), + password: formData.get('password'), + csrfToken: csrfToken // Utilisez le token CSRF récupéré par le hook + }) + .then(result => { + console.log('Sign In Result', result); + setIsLoading(false); + + if (result.error) { + setErrorMessage(result.error); + } else { + router.push(result.url); + } + }) + .catch(error => { + console.error('Error during sign in:', error); + setIsLoading(false); + setErrorMessage('An error occurred during sign in.'); + }); } if (isLoading === true) { diff --git a/Front-End/src/app/layout.js b/Front-End/src/app/layout.js index d290675..d953956 100644 --- a/Front-End/src/app/layout.js +++ b/Front-End/src/app/layout.js @@ -1,7 +1,8 @@ import React from 'react'; import { NextIntlClientProvider } from 'next-intl'; +import { CsrfProvider } from '@/context/CsrfContext'; // Importez le CsrfProvider -import {getMessages} from 'next-intl/server'; +import { getMessages } from 'next-intl/server'; import "@/css/tailwind.css"; export const metadata = { @@ -27,9 +28,11 @@ export default async function RootLayout({ children, params: { locale } }) { return ( - - {children} - + {/* Enveloppez votre application avec le CsrfProvider */} + + {children} + + ); diff --git a/Front-End/src/app/lib/authAction.js b/Front-End/src/app/lib/authAction.js index 6b01afb..099a178 100644 --- a/Front-End/src/app/lib/authAction.js +++ b/Front-End/src/app/lib/authAction.js @@ -25,8 +25,8 @@ const requestResponseHandler = async (response) => { throw error; } - export const login = (data, csrfToken) => { + console.log('data', data); const request = new Request( `${BE_AUTH_LOGIN_URL}`, { diff --git a/Front-End/src/context/CsrfContext.js b/Front-End/src/context/CsrfContext.js new file mode 100644 index 0000000..cf96e94 --- /dev/null +++ b/Front-End/src/context/CsrfContext.js @@ -0,0 +1,39 @@ +'use client'; // Ajoutez cette ligne pour marquer ce fichier comme un composant client + +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { BE_AUTH_CSRF_URL } from '@/utils/Url'; +import { setCsrfToken } from '@/utils/getCsrfToken'; + +const CsrfContext = createContext(); + +export const CsrfProvider = ({ children }) => { + const [csrfToken, setCsrfTokenState] = useState(''); + + useEffect(() => { + fetch(`${BE_AUTH_CSRF_URL}`, { + method: 'GET', + credentials: 'include' // Inclut les cookies dans la requête + }) + .then(response => response.json()) + .then(data => { + if (data && data.csrfToken) { + setCsrfTokenState(data.csrfToken); + setCsrfToken(data.csrfToken); // Définir le token CSRF global + console.log('CSRF Token reçu:', data.csrfToken); + } + }) + .catch(error => { + console.error('Error fetching CSRF token:', error); + }); + }, []); + + return ( + + {children} + + ); +}; + +export const useCsrfToken = () => { + return useContext(CsrfContext); +}; \ No newline at end of file diff --git a/Front-End/src/csrfMiddleware.js b/Front-End/src/csrfMiddleware.js new file mode 100644 index 0000000..9638410 --- /dev/null +++ b/Front-End/src/csrfMiddleware.js @@ -0,0 +1,12 @@ +import { getCsrfToken } from '@/utils/getCsrfToken'; + +export const csrfMiddleware = (handler) => { + return async (req, res) => { + const csrfToken = getCsrfToken(); + if (!csrfToken) { + console.error('CSRF Token is undefined'); + } + req.csrfToken = csrfToken; + return handler(req, res); + }; +}; \ No newline at end of file diff --git a/Front-End/src/hooks/useCsrfToken.js b/Front-End/src/hooks/useCsrfToken.js index 1489d70..a5b9ca8 100644 --- a/Front-End/src/hooks/useCsrfToken.js +++ b/Front-End/src/hooks/useCsrfToken.js @@ -1,29 +1,29 @@ -import { useEffect, useState } from 'react'; -import { BE_AUTH_CSRF_URL } from '@/utils/Url'; +// import { useEffect, useState } from 'react'; +// import { BE_AUTH_CSRF_URL } from '@/utils/Url'; -const useCsrfToken = () => { - const [token, setToken] = useState(''); +// const useCsrfToken = () => { +// const [token, setToken] = useState(''); - useEffect(() => { - fetch(`${BE_AUTH_CSRF_URL}`, { - method: 'GET', - credentials: 'include' // Inclut les cookies dans la requête - }) - .then(response => response.json()) - .then(data => { - if (data) { - if(data.csrfToken != token) { - setToken(data.csrfToken); - //console.log('------------> CSRF Token reçu:', data.csrfToken); - } - } - }) - .catch(error => { - console.error('Error fetching CSRF token:', error); - }); - }, []); +// useEffect(() => { +// fetch(`${BE_AUTH_CSRF_URL}`, { +// method: 'GET', +// credentials: 'include' // Inclut les cookies dans la requête +// }) +// .then(response => response.json()) +// .then(data => { +// if (data) { +// if(data.csrfToken != token) { +// setToken(data.csrfToken); +// console.log('------------> CSRF Token reçu:', data.csrfToken); +// } +// } +// }) +// .catch(error => { +// console.error('Error fetching CSRF token:', error); +// }); +// }, []); - return token; -}; +// return token; +// }; -export default useCsrfToken; +// export default useCsrfToken; diff --git a/Front-End/src/pages/api/auth/[...nextauth].js b/Front-End/src/pages/api/auth/[...nextauth].js new file mode 100644 index 0000000..e08d741 --- /dev/null +++ b/Front-End/src/pages/api/auth/[...nextauth].js @@ -0,0 +1,79 @@ +import NextAuth from 'next-auth'; +import CredentialsProvider from 'next-auth/providers/credentials'; +import jwt from 'jsonwebtoken'; +import { csrfMiddleware } from '@/csrfMiddleware'; // Importez le middleware csrfMiddleware + +const options = { + providers: [ + CredentialsProvider({ + name: 'Credentials', + credentials: { + email: { label: 'Email', type: 'email' }, + password: { label: 'Password', type: 'password' } + }, + authorize: (credentials, req) => { + console.log('Credentials:', credentials); // Vérifiez si ce log s'affiche + + // Utilisez le token CSRF injecté par le middleware + const csrfToken = req.csrfToken; + console.log("data to send : ", JSON.stringify({ + email: credentials.email, + password: credentials.password + }), "csrfToken : ", csrfToken); + + return fetch(`${process.env.NEXT_PUBLIC_API_URL}/Auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken // Utiliser le token CSRF ici + }, + body: JSON.stringify({ + email: credentials.email, + password: credentials.password + }), + credentials: 'include' + }) + .then(response => response.text()) + .then(text => { + console.log('Response Text:', text); // Loggez la réponse + const user = JSON.parse(text); // Parsez la réponse en JSON + + if (response.ok && user) { + return user; + } else { + throw new Error(user.errorMessage || 'Invalid credentials'); + } + }) + .catch(error => { + console.error('Error during authentication:', error); + throw new Error('Authentication failed'); + }); + } + }) + ], + session: { + jwt: true + }, + callbacks: { + async jwt(token, user) { + if (user) { + token.id = user.id; + token.email = user.email; + token.role = user.role; + } + return token; + }, + async session(session, token) { + session.user.id = token.id; + session.user.email = token.email; + session.user.role = token.role; + return session; + } + }, + pages: { + signIn: '/[locale]/users/login' + }, + csrf: false // Désactiver la gestion CSRF de NextAuth.js +}; + +export default csrfMiddleware((req, res) => NextAuth(req, res, options)); \ No newline at end of file diff --git a/Front-End/src/pages/api/auth/signin.js b/Front-End/src/pages/api/auth/signin.js new file mode 100644 index 0000000..2957e3c --- /dev/null +++ b/Front-End/src/pages/api/auth/signin.js @@ -0,0 +1,22 @@ +import { getCsrfToken } from 'next-auth/react'; +import useCsrfToken from '@/hooks/useCsrfToken'; +import DjangoCSRFToken from '@/components/DjangoCSRFToken' + +export default function SignIn({ csrfToken }) { + + const csrfToken = useCsrfToken(); + return ( +
+ + + + + + ); +} \ No newline at end of file diff --git a/Front-End/src/pages/api/auth/signout.js b/Front-End/src/pages/api/auth/signout.js new file mode 100644 index 0000000..456ffea --- /dev/null +++ b/Front-End/src/pages/api/auth/signout.js @@ -0,0 +1,9 @@ +import { signOut } from 'next-auth/client'; + +export default function SignOut() { + return ( + + ); +} \ No newline at end of file diff --git a/Front-End/src/pages/protected-page.js b/Front-End/src/pages/protected-page.js new file mode 100644 index 0000000..341959b --- /dev/null +++ b/Front-End/src/pages/protected-page.js @@ -0,0 +1,42 @@ +import { useSession, getSession } from 'next-auth/react'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +export default function ProtectedPage() { + const [session, loading] = useSession(); + const router = useRouter(); + + useEffect(() => { + if (!loading && !session) { + router.push('/auth/signin'); + } + }, [loading, session, router]); + + if (loading || !session) { + return

Loading...

; + } + + return ( +
+

Protected Page

+

Welcome, {session.user.email}

+
+ ); +} + +export async function getServerSideProps(context) { + const session = await getSession(context); + + if (!session) { + return { + redirect: { + destination: '/auth/signin', + permanent: false + } + }; + } + + return { + props: { session } + }; +} \ No newline at end of file diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index 0266d87..3db9814 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -13,6 +13,7 @@ export const BE_AUTH_LOGIN_URL = `${BASE_URL}/Auth/login` export const BE_AUTH_LOGOUT_URL = `${BASE_URL}/Auth/logout` export const BE_AUTH_PROFILES_URL = `${BASE_URL}/Auth/profiles` export const BE_AUTH_CSRF_URL = `${BASE_URL}/Auth/csrf` +export const BE_AUTH_INFO_SESSION = `${BASE_URL}/Auth/infoSession` // GESTION INSCRIPTION export const BE_SUBSCRIPTION_STUDENTS_URL = `${BASE_URL}/Subscriptions/students` // Récupère la liste des élèves inscrits ou en cours d'inscriptions diff --git a/Front-End/src/utils/getCsrfToken.js b/Front-End/src/utils/getCsrfToken.js new file mode 100644 index 0000000..5e26dc0 --- /dev/null +++ b/Front-End/src/utils/getCsrfToken.js @@ -0,0 +1,9 @@ +let csrfToken = ''; + +export const setCsrfToken = (token) => { + csrfToken = token; +}; + +export const getCsrfToken = () => { + return csrfToken; +}; \ No newline at end of file