diff --git a/.env.example b/.env.example index ad4c6d0..7f8277c 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,14 @@ FACEBOOK_APP_SECRET= TWITTER_APP_ID= TWITTER_APP_SECRET= # 0 or 1 -KAIST_APP_ENABLED=0 +KAIST_APP_ENABLED=1 KAIST_APP_SECRET= + +KAIST_APP_V2_ENABLED=1 +KAIST_APP_V2_HOSTNAME=ssodev.kaist.ac.kr +KAIST_APP_V2_SECRET= +KAIST_APP_V2_CLIENT_ID= + RECAPTCHA_SECRET= # Production only diff --git a/.envrc.example b/.envrc.example new file mode 100644 index 0000000..910745d --- /dev/null +++ b/.envrc.example @@ -0,0 +1,19 @@ +use flake; + +export SSO_DB_NAME=sparcssso +export SSO_DB_USER=root +export SSO_DB_PASSWORD=unsafe-password +export SSO_DB_HOST=127.0.0.1 +export SSO_DB_PORT=33306 + +export SSO_ENV=development +export SECRET_KEY=foobar +export SENTRY_DSN= +export FACEBOOK_APP_ID= +export FACEBOOK_APP_SECRET= +export TWITTER_APP_ID= +export TWITTER_APP_SECRET= +# 0 or 1 +export KAIST_APP_ENABLED=0 +export KAIST_APP_SECRET= +export RECAPTCHA_SECRET= diff --git a/.gitignore b/.gitignore index d0cf92a..96a1e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ node_modules/ /django_env/ /sso.ini .idea +.venv +.direnv +.envrc diff --git a/Makefile b/Makefile index a73f56e..9e3c75a 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,10 @@ +setup_local_cert: + cd local-dev && ./setup-cert.sh + +# mysql 5.6이 arm64 이미지가 없어 amd64 고정이 필요 +compose_up: setup_local_cert + cd local-dev && DOCKER_DEFAULT_PLATFORM=linux/amd64 docker compose up -d + test: pytest tests/ diff --git a/apps/api/urls.py b/apps/api/urls.py index 3a71cd0..a7b2187 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -1,10 +1,12 @@ import json import time +from django.conf import settings from django.http import HttpResponse from django.urls import path from apps.api.views import v2 +from apps.core.views import auth # /versions/ @@ -28,3 +30,8 @@ def versions(request): path('v2/email/', v2.EmailView.as_view()), path('v2/stats/', v2.stats), ] + +if settings.KAIST_APP_V2_ENABLED: + urlpatterns += [ + path('idp/kaist/callback', auth.callback_kaist_v2) + ] diff --git a/apps/core/backends/auth.py b/apps/core/backends/auth.py index 663e40c..b8a8825 100644 --- a/apps/core/backends/auth.py +++ b/apps/core/backends/auth.py @@ -4,6 +4,7 @@ import uuid from urllib.parse import parse_qsl, urlencode +from django.http import HttpResponseBadRequest import ldap3 import oauth2 as oauth import requests @@ -179,7 +180,7 @@ def auth_tw_callback(tokens, verifier): # KAIST Auth -def auth_kaist_init(callback_url): +def auth_kaist_init(callback_url: str): state = str(uuid.uuid4()) args = { 'client_id': 'SPARCS', @@ -190,7 +191,7 @@ def auth_kaist_init(callback_url): return f'https://iam2.kaist.ac.kr/api/sso/commonLogin?{urlencode(args)}', state -def auth_kaist_callback(token, iam_info_raw): +def auth_kaist_callback(token: str, iam_info_raw: str): iam_info = json.loads(iam_info_raw)['dataMap'] state = iam_info['state'] @@ -211,3 +212,73 @@ def auth_kaist_callback(token, iam_info_raw): kaist_profile = UserProfile.objects.filter(kaist_id=info['userid'], test_only=False).first() return kaist_profile, info, True + + +def auth_kaist_v2_init(request, callback_url: str): + state = str(uuid.uuid4()) + nonce = str(uuid.uuid4()) + + return { + 'body': { + 'client_id': settings.KAIST_APP_V2_CLIENT_ID, + 'redirect_uri': callback_url, + 'state': state, + 'nonce': nonce, + }, + 'action': f"https://{settings.KAIST_APP_V2_HOSTNAME}/auth/user/single/login/authorize", + }, state, nonce + + + +def auth_kaist_v2_callback(request: str, redirect_url: str): + if request.POST.get("code") is None: + raise HttpResponseBadRequest("auth_kaist_v2_callback: Code not found") + return None, None, False + request_code = request.POST.get("code") + + if request.POST.get("state") is None: + print("auth_kaist_v2_callback: State not found") + return None, None, False + request_state = request.POST.get("state") + + if request_state != request.session.get('kaist_v2_login_state'): + print("auth_kaist_v2_callback: State mismatch") + return None, None, False + + request_url = f"https://{settings.KAIST_APP_V2_HOSTNAME}/auth/api/single/auth" + data = { + 'client_id': settings.KAIST_APP_V2_CLIENT_ID, + 'client_secret': settings.KAIST_APP_V2_CLIENT_SECRET, + 'code': request_code, + 'redirect_uri': redirect_url, + } + response = requests.post(request_url, data=data, headers={ + "Content-Type": "application/x-www-form-urlencoded" + }) + + response_data = response.json() + if "errorCode" in response_data: + print(f"auth_kaist_v2_callback: Error {response_data['errorCode']}: {response_data['error']}") + return None, None, False + + request_nonce = request.session.get('kaist_v2_login_nonce') + if request_nonce != response_data['nonce']: + print("auth_kaist_v2_callback: Nonce mismatch") + return None, None, False + + user_data = response_data['userInfo'] + user_name_parts = [v.strip() for v in user_data.get("user_eng_nm").split(",") if v.strip() != ""] + + info = { + 'userid': user_data["kaist_uid"], + 'email': user_data.get("email"), + 'first_name': user_name_parts[1] if len(user_name_parts) > 1 else "", + 'last_name': user_name_parts[0] if len(user_name_parts) > 0 else "", + 'gender': '*H', + 'birthday': "", + 'kaist_info_v2': user_data, + } + kaist_profile = UserProfile.objects.filter(kaist_id=info['userid'], + test_only=False).first() + + return kaist_profile, info, True diff --git a/apps/core/backends/signup.py b/apps/core/backends/signup.py index cd11359..022e038 100644 --- a/apps/core/backends/signup.py +++ b/apps/core/backends/signup.py @@ -64,7 +64,10 @@ def signup_social(typ, profile): user.profile = UserProfile(gender=profile.get('gender', '*H')) if 'birthday' in profile: - user.profile.birthday = profile['birthday'] + if profile['birthday'].strip() == "": + user.profile.birthday = None + else: + user.profile.birthday = profile['birthday'] if typ == 'FB': user.profile.facebook_id = profile['userid'] @@ -73,5 +76,8 @@ def signup_social(typ, profile): elif typ == 'KAIST': user.profile.email_authed = email.endswith('@kaist.ac.kr') user.profile.save_kaist_info(profile) + elif typ == 'KAISTV2': + user.profile.email_authed = email.endswith('@kaist.ac.kr') + user.profile.save_kaist_v2_info(profile) user.profile.save() return user diff --git a/apps/core/migrations/0005_kaist_v2_sso.py b/apps/core/migrations/0005_kaist_v2_sso.py new file mode 100644 index 0000000..82290ff --- /dev/null +++ b/apps/core/migrations/0005_kaist_v2_sso.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2025-03-27 14:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_inquirymail'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='kaist_v2_info', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='userprofile', + name='kaist_v2_info_time', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/apps/core/models.py b/apps/core/models.py index 8097bd4..e2add4c 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -181,22 +181,24 @@ def __str__(self): class UserProfile(models.Model): """ denotes additional information of single user - - user: user object - - gender: gender; *H / *M / *F / *E or others - - birthday: birthday - - point: point for public services - - point_test: point for test services - - email_new: new email before auth - - email_authed: email authed state - - test_only: indicate test only account - - test_enabled: test mode state - - facebook_id: facebook unique id - - twitter_id: twitter unique id - - kaist_id: kaist uid - - kaist_info: additional kaist info - - kaist_info_time: kaist info updated time - - sparcs_id: sparcs id iff sparcs member - - expire_time: expire time for permanent deletion + - user: user object + - gender: gender; *H / *M / *F / *E or others + - birthday: birthday + - point: point for public services + - point_test: point for test services + - email_new: new email before auth + - email_authed: email authed state + - test_only: indicate test only account + - test_enabled: test mode state + - facebook_id: facebook unique id + - twitter_id: twitter unique id + - kaist_id: kaist uid + - kaist_info: additional kaist info + - kaist_info_time: kaist info updated time + - kaist_v2_info: additional kaist info + - kaist_v2_info_time: kaist info updated time + - sparcs_id: sparcs id iff sparcs member + - expire_time: expire time for permanent deletion """ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') @@ -213,6 +215,8 @@ class UserProfile(models.Model): kaist_id = models.CharField(max_length=50, blank=True, null=True) kaist_info = models.TextField(blank=True, null=True) kaist_info_time = models.DateField(blank=True, null=True) + kaist_v2_info = models.TextField(blank=True, null=True) + kaist_v2_info_time = models.DateField(blank=True, null=True) sparcs_id = models.CharField(max_length=50, blank=True, null=True) expire_time = models.DateTimeField(blank=True, null=True) @@ -250,6 +254,12 @@ def save_kaist_info(self, info): self.kaist_info_time = timezone.now() self.save() + def save_kaist_v2_info(self, info): + self.kaist_id = info['userid'] + self.kaist_v2_info = json.dumps(info['kaist_info_v2']) + self.kaist_v2_info_time = timezone.now() + self.save() + def __str__(self): return f'{self.user}''s profile' diff --git a/apps/core/urls.py b/apps/core/urls.py index f28c0a0..40cfa12 100644 --- a/apps/core/urls.py +++ b/apps/core/urls.py @@ -58,3 +58,11 @@ path('connect/kaist/', auth.init, {'mode': 'CONN', 'site': 'KAIST'}), path('renew/kaist/', auth.init, {'mode': 'RENEW', 'site': 'KAIST'}), ] + + +if settings.KAIST_APP_V2_ENABLED: + urlpatterns += [ + path('login/kaistv2/', auth.init, {'mode': 'LOGIN', 'site': 'KAISTV2'}), + path('connect/kaistv2/', auth.init, {'mode': 'CONN', 'site': 'KAISTV2'}), + path('renew/kaistv2/', auth.init, {'mode': 'RENEW', 'site': 'KAISTV2'}), + ] diff --git a/apps/core/views/auth.py b/apps/core/views/auth.py index 7b5d9cc..db12a75 100644 --- a/apps/core/views/auth.py +++ b/apps/core/views/auth.py @@ -3,7 +3,7 @@ from django.conf import settings from django.contrib import auth -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, JsonResponse, HttpResponseForbidden, HttpResponseBadRequest from django.shortcuts import redirect, render from django.utils import timezone from django.views.decorators.csrf import csrf_exempt @@ -11,11 +11,14 @@ from apps.core.backends import ( anon_required, auth_fb_callback, auth_fb_init, - auth_kaist_callback, auth_kaist_init, auth_tw_callback, - auth_tw_init, get_clean_url, get_social_name, + auth_kaist_init, auth_kaist_callback, + auth_kaist_v2_init, auth_kaist_v2_callback, + auth_tw_init, auth_tw_callback, + get_clean_url, get_social_name, ) from apps.core.constants import SocialConnectResult from apps.core.models import Notice, Service +import uuid logger = logging.getLogger('sso.auth') @@ -79,13 +82,13 @@ def login_core(request, session_name, template_name, get_user_func): if parsed_nexturl.get('show_disabled_button', None) is None: show_disabled_button = setting['show_disabled_button'] - return render(request, template_name, { 'notice': notice, 'service': service.alias if service else '', 'fail': request.session.pop(session_name, ''), 'show_internal': show_internal, 'kaist_enabled': settings.KAIST_APP_ENABLED, + 'kaist_v2_enabled': settings.KAIST_APP_V2_ENABLED, 'social_enabled': social_enabled, 'show_disabled_button': show_disabled_button, 'app_name': app_name, @@ -133,8 +136,13 @@ def logout(request): auth.logout(request) return render(request, 'account/logout.html') +def get_init_callback_url(site: str): + if site == "KAISTV2": + return urljoin(settings.DOMAIN, '/api/idp/kaist/callback/') + else: + return urljoin(settings.DOMAIN, '/account/callback/') -# /login/{fb,tw,kaist}/, /connect/{fb,tw,kaist}/, /renew/kaist/ +# /login/{fb,tw,kaist,kaistv2}/, /connect/{fb,tw,kaist,kaistv2}/, /renew/kaist/ @require_POST def init(request, mode, site): if request.method != 'POST': @@ -153,7 +161,7 @@ def init(request, mode, site): return HttpResponseRedirect(f'/account/profile/?connect_site={site}&connect_result={result_code.name}') request.session['info_auth'] = {'mode': mode, 'type': site} - callback_url = urljoin(settings.DOMAIN, '/account/callback/') + callback_url = get_init_callback_url(site) if site == 'FB': url = auth_fb_init(callback_url) @@ -163,9 +171,38 @@ def init(request, mode, site): elif site == 'KAIST': url, token = auth_kaist_init(callback_url) request.session['request_token'] = token + elif site == 'KAISTV2': + response_body, state, nonce = auth_kaist_v2_init(request, callback_url) + request.session['kaist_v2_login_state'] = state + request.session['kaist_v2_login_nonce'] = nonce + return JsonResponse(response_body) + return redirect(url) +@csrf_exempt +@require_POST +def callback_kaist_v2(request): + SITE = "KAISTV2" + info_auth = request.session.pop('info_auth', None) + if not info_auth: + raise HttpResponseForbidden('No info_auth in session') + return redirect('/') + + mode = info_auth['mode'] + + redirect_url = get_init_callback_url(SITE) + profile, info, valid = auth_kaist_v2_callback(request, redirect_url) + + if not valid: + raise HttpResponseBadRequest('Invalid') + return redirect('/') + + state = request.session.delete('kaist_v2_login_state') + nonce = request.session.delete('kaist_v2_login_nonce') + return callback_inner(request, mode, SITE, profile, info) + + # /callback/ @csrf_exempt def callback(request): @@ -192,6 +229,10 @@ def callback(request): # Should not reach here! return redirect('/') + return callback_inner(request, mode, site, profile, info) + + +def callback_inner(request, mode, site, profile, info): uid = info['userid'] if info else 'unknown' logger.info('social', { 'r': request, @@ -237,6 +278,8 @@ def callback_login(request, site, user, info): request.session.pop('info_signup', None) if site == 'KAIST': user.profile.save_kaist_info(info) + elif site == 'KAISTV2': + user.profile.save_kaist_v2_info(info) auth.login(request, user) nexturl = request.session.pop('next', '/') @@ -257,6 +300,8 @@ def callback_conn(request, site, user, info): profile.twitter_id = info['userid'] elif site == 'KAIST' and not profile.kaist_id: profile.save_kaist_info(info) + elif site == 'KAISTV2' and not profile.kaist_id: + profile.save_kaist_v2_info(info) else: result_code = SocialConnectResult.SITE_INVALID @@ -276,7 +321,7 @@ def callback_conn(request, site, user, info): # from /callback/ def callback_renew(request, site, user, info): - if site != 'KAIST': + if site != 'KAIST' and site != 'KAISTV2': result_code = SocialConnectResult.RENEW_UNNECESSARY return HttpResponseRedirect(f'/account/profile/?connect_site={site}&connect_result={result_code.name}') @@ -285,7 +330,10 @@ def callback_renew(request, site, user, info): if profile.kaist_id != info['userid']: result_code = SocialConnectResult.KAIST_IDENTITY_MISMATCH else: - user.profile.save_kaist_info(info) + if site == 'KAIST': + user.profile.save_kaist_info(info) + else: + user.profile.save_kaist_v2_info(info) request.session['result_con'] = result_code.value diff --git a/apps/core/views/profile.py b/apps/core/views/profile.py index 89a9e69..7ca6de2 100644 --- a/apps/core/views/profile.py +++ b/apps/core/views/profile.py @@ -59,6 +59,7 @@ def main(request): 'result_prof': result_prof, 'result_con': result_con, 'kaist_enabled': settings.KAIST_APP_ENABLED, + 'kaist_v2_enabled': settings.KAIST_APP_V2_ENABLED, }) diff --git a/apps/dev/views.py b/apps/dev/views.py index a4d43ba..60e234d 100644 --- a/apps/dev/views.py +++ b/apps/dev/views.py @@ -163,6 +163,15 @@ def user(request, uid): except Exception: pass + try: + kaist_info = json.loads(request.POST.get('kaist_v2_info', '')) + profile.save_kaist_v2_info({ + 'userid': kaist_info['kaist_uid'], + 'kaist_info': kaist_info, + }) + except Exception: + pass + log_msg = 'create' if uid == 'add' else 'update' logger.warning(f'account.{log_msg}', { 'r': request, diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index c4ff1cb..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,55 +0,0 @@ -version: '3' -services: - sparcssso: - build: '.' - restart: always - depends_on: - - db - volumes: - - './sparcssso/local_settings.py:/usr/app/sparcssso/sparcssso/local_settings.py:ro' - - 'sparcssso_log:/usr/app/sparcssso/archive/buffer' - - 'sparcssso_letters:/usr/app/sparcssso/letters' - environment: - - 'SSO_DB_NAME=sparcssso' - - 'SSO_DB_USER=root' - - 'SSO_DB_PASSWORD=unsafe-password' - - 'SSO_DB_HOST=db' - - 'SSO_DB_PORT=3306' - networks: - sparcssso: - aliases: - - sparcssso - - nginx: - build: './nginx' - restart: always - ports: - - '80:80' - depends_on: - - sparcssso - networks: - - sparcssso - - db: - image: 'mysql:5.7' - restart: always - environment: - - 'MYSQL_DATABASE=sparcssso' - - 'MYSQL_ROOT_PASSWORD=unsafe-password' - command: - - '--character-set-server=utf8mb4' - - '--collation-server=utf8mb4_unicode_ci' - volumes: - - 'sparcssso_db:/var/lib/mysql' - networks: - sparcssso: - aliases: - - db - -networks: - sparcssso: - -volumes: - sparcssso_db: - sparcssso_log: - sparcssso_letters: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..bd68f68 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1742889210, + "narHash": "sha256-hw63HnwnqU3ZQfsMclLhMvOezpM7RSB0dMAtD5/sOiw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "698214a32beb4f4c8e3942372c694f40848b360d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..071ad0c --- /dev/null +++ b/flake.nix @@ -0,0 +1,90 @@ +{ + description = "Python development environment with venv"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ ]; + }; + + pythonEnv = pkgs.python3; + mysqlPkg = pkgs.mysql84; + + # Shell script to create and activate venv if it doesn't exist + # and install packages from requirements.txt + setupVenvScript = pkgs.writeShellScriptBin "setup-venv" '' + if [ ! -d ./.venv ]; then + echo "Creating new Python virtual environment..." + ${pythonEnv}/bin/python -m venv ./.venv + fi + + echo "Installing packages from requirements.txt..." + ./.venv/bin/pip install -r requirements.txt + ''; + + in + { + devShells.default = pkgs.mkShell { + buildInputs = [ + pythonEnv + setupVenvScript + ] ++ (with pkgs; [ + mysqlPkg + zlib + zlib.dev + libjpeg + libtiff + freetype + lcms2 + libwebp + tcl + tk + libffi + pkg-config + zstd + ]); + + NIX_CFLAGS_COMPILE = "-I${pkgs.zlib.dev}/include -I${pkgs.libjpeg.dev}/include -I${pkgs.zstd.dev}/include"; + + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.zlib + pkgs.libjpeg + pkgs.libtiff + pkgs.freetype + pkgs.lcms2 + pkgs.libwebp + pkgs.tcl + pkgs.libffi + pkgs.zstd + ]; + + # Add explicit linker flags for common libraries + LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.zlib + pkgs.zstd + ]; + + shellHook = '' + # Run the setup script + setup-venv + + # Activate the venv + if [ -d ./.venv ]; then + source ./.venv/bin/activate + export PYTHONPATH="$PWD:$PYTHONPATH" + fi + + alias django='python manage.py' + alias dc='DOCKER_DEFAULT_PLATFORM=linux/amd64 docker compose' + ''; + }; + } + ); +} \ No newline at end of file diff --git a/local-dev/.gitignore b/local-dev/.gitignore new file mode 100644 index 0000000..a7dc984 --- /dev/null +++ b/local-dev/.gitignore @@ -0,0 +1 @@ +nginx/certs diff --git a/local-dev/docker-compose.yml b/local-dev/docker-compose.yml new file mode 100644 index 0000000..3afff7a --- /dev/null +++ b/local-dev/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3' +services: + nginx-proxy: + image: nginx:alpine + ports: + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./nginx/certs:/etc/nginx/certs + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + db: + image: 'mysql:5.7' + restart: always + environment: + - 'MYSQL_DATABASE=sparcssso' + - 'MYSQL_ROOT_PASSWORD=unsafe-password' + command: + - '--character-set-server=utf8mb4' + - '--collation-server=utf8mb4_unicode_ci' + volumes: + - 'sparcssso_db:/var/lib/mysql' + ports: + - '33306:3306' + networks: + sparcssso: + aliases: + - db + + +networks: + sparcssso: + +volumes: + sparcssso_db: diff --git a/local-dev/nginx/conf.d/default.conf b/local-dev/nginx/conf.d/default.conf new file mode 100644 index 0000000..bab061e --- /dev/null +++ b/local-dev/nginx/conf.d/default.conf @@ -0,0 +1,22 @@ +server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/selfsigned.crt; + ssl_certificate_key /etc/nginx/certs/selfsigned.key; + + # SSL configuration (you can enhance this with your preferred settings) + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; + ssl_session_timeout 10m; + ssl_session_cache shared:SSL:10m; + + location / { + proxy_pass http://host.docker.internal:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/local-dev/openssl.conf b/local-dev/openssl.conf new file mode 100644 index 0000000..30e29a4 --- /dev/null +++ b/local-dev/openssl.conf @@ -0,0 +1,21 @@ +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +x509_extensions = v3_req + +[dn] +C = US +ST = State +L = City +O = Organization +CN = localhost + +[v3_req] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = sparcssso.kaist.ac.kr +DNS.3 = sso.dev.sparcs.org diff --git a/local-dev/setup-cert.sh b/local-dev/setup-cert.sh new file mode 100755 index 0000000..59b9d20 --- /dev/null +++ b/local-dev/setup-cert.sh @@ -0,0 +1,5 @@ +rm -rf nginx/certs/* || mkdir -p nginx/certs +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout nginx/certs/selfsigned.key \ + -out nginx/certs/selfsigned.crt \ + -config openssl.conf diff --git a/locale/ko/LC_MESSAGES/django.po b/locale/ko/LC_MESSAGES/django.po index cd46726..73518fe 100644 --- a/locale/ko/LC_MESSAGES/django.po +++ b/locale/ko/LC_MESSAGES/django.po @@ -208,6 +208,10 @@ msgid "Internal Login" msgstr "내부 로그인" #: templates/account/login/main.html:60 +msgid "KAIST SSO Login / Signup (Old)" +msgstr "카이스트 통합인증으로 로그인 / 가입 (구 SSO)" + +#: templates/account/login/main.html:63 msgid "KAIST SSO Login / Signup" msgstr "카이스트 통합인증으로 로그인 / 가입" diff --git a/sparcssso/settings/common.py b/sparcssso/settings/common.py index 57e76be..b2670ac 100644 --- a/sparcssso/settings/common.py +++ b/sparcssso/settings/common.py @@ -90,6 +90,14 @@ RECAPTCHA_SECRET = os.environ.get('RECAPTCHA_SECRET', '') +KAIST_APP_V2_ENABLED = True if os.environ.get('KAIST_APP_V2_ENABLED', '0') == '1' else False + +KAIST_APP_V2_HOSTNAME = os.environ.get('KAIST_APP_V2_HOSTNAME', 'sso.kaist.ac.kr') + +KAIST_APP_V2_CLIENT_ID = os.environ.get('KAIST_APP_V2_CLIENT_ID', 'kaist-sparcs') + +KAIST_APP_V2_CLIENT_SECRET = os.environ.get('KAIST_APP_V2_CLIENT_SECRET', '') + # E-mail settings EMAIL_HOST = 'localhost' @@ -193,3 +201,5 @@ ) else: print('SENTRY_DSN not provided. Metrics will not be sent.') # noqa: T001 + +APPEND_SLASH = True diff --git a/sparcssso/settings/development.py b/sparcssso/settings/development.py index 2122e81..c66f42e 100644 --- a/sparcssso/settings/development.py +++ b/sparcssso/settings/development.py @@ -7,19 +7,26 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +DOMAIN = os.environ.get('SSO_DOMAIN', 'https://sso.dev.sparcs.org') + ALLOWED_HOSTS = [ - 'ssodev.sparcs.org', + 'sso.dev.sparcs.org', 'localhost', ] +_DOMAIN_WITHOUT_SCHEME = DOMAIN.replace("https://", "").replace("http://", "") +if _DOMAIN_WITHOUT_SCHEME not in ALLOWED_HOSTS: + ALLOWED_HOSTS += [_DOMAIN_WITHOUT_SCHEME] + if os.environ.get('SSO_LOCAL', '0') == '1': ALLOWED_HOSTS += ['localhost'] VERSION = get_version_info(DEBUG, ALLOWED_HOSTS) -DOMAIN = 'https://ssodev.sparcs.org' LOG_DIR = os.path.join(BASE_DIR, 'archive/logs/') # noqa: F405 LOG_BUFFER_DIR = os.path.join(BASE_DIR, 'archive/buffer/') # noqa: F405 STAT_FILE = os.path.join(BASE_DIR, 'archive/stats.txt') # noqa: F405 + +KAIST_APP_V2_HOSTNAME = os.environ.get('KAIST_APP_V2_HOSTNAME', 'ssodev.kaist.ac.kr') diff --git a/templates/account/base.html b/templates/account/base.html index 46b5cc9..c95dc3f 100644 --- a/templates/account/base.html +++ b/templates/account/base.html @@ -7,5 +7,59 @@ + + + {% block header %}{% endblock %} {% endblock %} diff --git a/templates/account/login/main.html b/templates/account/login/main.html index bf9f3f6..3ae711b 100644 --- a/templates/account/login/main.html +++ b/templates/account/login/main.html @@ -56,9 +56,12 @@
{% trans "Renewed at" %} {{ profile.kaist_v2_info_time|date:'Y-m-d' }}
+ {% else %} + + {% endif %} {% endif %} @@ -244,6 +253,14 @@