From ac5823234ebbab54e84d20feead108c210fab67b Mon Sep 17 00:00:00 2001 From: root Date: Mon, 27 Jul 2026 17:23:52 +0900 Subject: [PATCH] =?UTF-8?q?BARON-SSO=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.docker.example | 13 +- .gitea/workflows/manual-deploy.yml | 9 + .htaccess | 5 + BARON_SSO_QNA_MIGRATION_TASKS.md | 335 ++++++++ DEPLOY_GITEA.md | 18 + DOCKER_LOCAL.md | 52 ++ auth/baron_login.php | 28 + auth/callback.php | 170 ++++ auth/common.php | 790 ++++++++++++++++++ auth/debug_claims.php | 23 + docker-compose.yml | 9 + egbim/bbs/admin_guard.php | 27 +- egbim/css/style.css | 2 +- egbim/skin/member/basic/descope_login.php | 40 +- egbim/skin/member/basic/descope_logout.php | 13 +- egbim/skin/member/basic/descope_session.php | 151 +--- .../skin/member/basic/descope_user_search.php | 83 +- egbim/skin/member/basic/login.skin.php | 10 +- egbim/skin/member/basic/main_header.skin.php | 85 +- egbim/skin/qa/basic/descope_qa_list.skin.php | 2 +- egbim/skin/qa/basic/descope_qa_write.skin.php | 2 +- eng/bbs/admin_guard.php | 20 +- eng/js/common.js | 53 +- eng/lib/common.lib.php | 13 +- eng/skin/faq/basic/list.skin.php | 13 - eng/skin/member/basic/buy.skin.php | 13 - eng/skin/member/basic/descope_login.php | 40 +- eng/skin/member/basic/descope_logout.php | 13 +- eng/skin/member/basic/descope_session.php | 151 +--- eng/skin/member/basic/descope_user_search.php | 83 +- eng/skin/member/basic/floorplan.skin.php | 13 - eng/skin/member/basic/forbim.skin.php | 13 - eng/skin/member/basic/interface.skin.php | 13 - eng/skin/member/basic/login.skin.php | 16 +- eng/skin/member/basic/main.skin.php | 13 - eng/skin/member/basic/main_header.skin.php | 85 +- eng/skin/member/basic/primary.skin.php | 13 - eng/skin/member/basic/register.skin.php | 3 - eng/skin/member/basic/value.skin.php | 13 - eng/skin/qa/basic/descope_qa_detail.skin.php | 13 - eng/skin/qa/basic/descope_qa_list.skin.php | 15 +- eng/skin/qa/basic/descope_qa_write.skin.php | 2 +- eng/skin/qa/basic/view.skin.php | 13 - scripts/deploy_remote.sh | 8 + 44 files changed, 1804 insertions(+), 695 deletions(-) create mode 100644 BARON_SSO_QNA_MIGRATION_TASKS.md create mode 100644 auth/baron_login.php create mode 100644 auth/callback.php create mode 100644 auth/common.php create mode 100644 auth/debug_claims.php diff --git a/.env.docker.example b/.env.docker.example index 114e689..57a228f 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -4,4 +4,15 @@ G5_MYSQL_DB=egbim G5_MYSQL_USER=egbim G5_MYSQL_PASSWORD=baron3840!! MARIADB_ROOT_PASSWORD=root_local_pass -COMPOSE_PROJECT_NAME=egbim \ No newline at end of file +COMPOSE_PROJECT_NAME=egbim + +# BARON SSO local test +BARON_SSO_CLIENT_ID= +BARON_SSO_CLIENT_SECRET= +BARON_SSO_AUTHORIZE_ENDPOINT= +BARON_SSO_TOKEN_ENDPOINT= +BARON_SSO_USERINFO_ENDPOINT= +BARON_SSO_ISSUER= +BARON_SSO_REDIRECT_URI=http://localhost:8088/auth/callback +BARON_SSO_SCOPE=openid profile email tenants +BARON_SSO_CLIENT_AUTH_METHOD=basic \ No newline at end of file diff --git a/.gitea/workflows/manual-deploy.yml b/.gitea/workflows/manual-deploy.yml index 3c8a9b7..e868350 100644 --- a/.gitea/workflows/manual-deploy.yml +++ b/.gitea/workflows/manual-deploy.yml @@ -45,6 +45,15 @@ jobs: G5_MYSQL_PASSWORD=${{ secrets.G5_MYSQL_PASSWORD }} MARIADB_ROOT_PASSWORD=${{ secrets.MARIADB_ROOT_PASSWORD }} COMPOSE_PROJECT_NAME=${{ secrets.COMPOSE_PROJECT_NAME }} + BARON_SSO_CLIENT_ID=${{ secrets.BARON_SSO_CLIENT_ID }} + BARON_SSO_CLIENT_SECRET=${{ secrets.BARON_SSO_CLIENT_SECRET }} + BARON_SSO_AUTHORIZE_ENDPOINT=${{ secrets.BARON_SSO_AUTHORIZE_ENDPOINT }} + BARON_SSO_TOKEN_ENDPOINT=${{ secrets.BARON_SSO_TOKEN_ENDPOINT }} + BARON_SSO_USERINFO_ENDPOINT=${{ secrets.BARON_SSO_USERINFO_ENDPOINT }} + BARON_SSO_ISSUER=${{ secrets.BARON_SSO_ISSUER }} + BARON_SSO_REDIRECT_URI=${{ secrets.BARON_SSO_REDIRECT_URI }} + BARON_SSO_SCOPE=${{ secrets.BARON_SSO_SCOPE }} + BARON_SSO_CLIENT_AUTH_METHOD=${{ secrets.BARON_SSO_CLIENT_AUTH_METHOD }} EOF - name: Upload deployment script diff --git a/.htaccess b/.htaccess index f80bfa7..0d1a0d8 100644 --- a/.htaccess +++ b/.htaccess @@ -1,6 +1,11 @@ RewriteEngine On +# auth route compatibility +RewriteRule ^auth/baron_login/?$ /auth/baron_login.php [L,QSA] +RewriteRule ^auth/callback/?$ /auth/callback.php [L,QSA] +RewriteRule ^auth/debug_claims/?$ /auth/debug_claims.php [L,QSA] + # local docker/localhost bypass RewriteCond %{ENV:APP_ENV} =local [OR] RewriteCond %{HTTP_HOST} ^(localhost|127\.0\.0\.1)(:[0-9]+)?$ [NC] diff --git a/BARON_SSO_QNA_MIGRATION_TASKS.md b/BARON_SSO_QNA_MIGRATION_TASKS.md new file mode 100644 index 0000000..fd38931 --- /dev/null +++ b/BARON_SSO_QNA_MIGRATION_TASKS.md @@ -0,0 +1,335 @@ +# BARON-SSO 연동 및 Q&A 접근 전환 작업 목록 + +## 목표 + +- 외부 사용자는 기존 Descope 로그인을 계속 사용한다. +- 내부 사용자는 BARON-SSO 로그인으로 전환한다. +- 내부 사용자도 기존과 동일하게 Q&A 페이지를 조회, 작성, 수정, 댓글 작성할 수 있어야 한다. +- 로그인 공급자가 달라도 Q&A 는 동일한 세션 구조를 사용하도록 정리한다. + +## 현재 확인된 전제 + +- 현재 Q&A 기능은 Descope 전용 세션 파일에 직접 의존한다. +- 국문 `egbim/` 와 영문 `eng/` 에 동일한 구조가 병렬로 존재한다. +- 내부/외부 사용자 판별은 현재 Q&A 작성 로직에서 이메일 도메인 기준으로 처리한다. +- 관리자 권한은 세션 파일 내부의 이메일 화이트리스트 기반이다. +- BARON-SSO 전용 콜백, 토큰 교환, 사용자 정보 매핑 로직은 현재 코드베이스에 별도로 구현되어 있지 않다. +- BARON-SSO 애플리케이션 보안 수준은 `Server-side app` 으로 확정되었다. + +## 이번 회차 확정 사항 + +- [x] BARON-SSO 애플리케이션 유형은 `Server-side app` 으로 설정 +- [x] `client_secret` 을 사용하는 Authorization Code 기반 서버 처리 방식 사용 +- [x] 토큰 교환과 세션 생성은 브라우저가 아니라 서버에서 처리 +- [ ] BARON-SSO claim 샘플 확보 +- [ ] callback 엔드포인트 경로 확정 +- [ ] 공통 auth bootstrap 파일 구조 확정 + +## 현재 영향 파일 + +### 공통 세션/인증 진입점 + +- `egbim/skin/member/basic/descope_session.php` +- `eng/skin/member/basic/descope_session.php` +- `egbim/skin/member/basic/descope_login.php` +- `eng/skin/member/basic/descope_login.php` +- `egbim/skin/member/basic/descope_logout.php` +- `eng/skin/member/basic/descope_logout.php` + +### Q&A 주요 진입점 + +- `egbim/bbs/descope_qa_list.php` +- `egbim/bbs/descope_qa_detail.php` +- `egbim/bbs/descope_qa_write.php` +- `egbim/bbs/descope_qa_comment.php` +- `egbim/bbs/descope_qa_comment_update.php` +- `egbim/bbs/descope_qa_comment_delete.php` +- `egbim/bbs/descope_qa_delete.php` +- `egbim/bbs/descope_qa_status.php` +- `egbim/bbs/admin_guard.php` +- `eng/bbs/descope_qa_list.php` +- `eng/bbs/descope_qa_detail.php` +- `eng/bbs/descope_qa_write.php` +- `eng/bbs/descope_qa_comment.php` +- `eng/bbs/descope_qa_comment_update.php` +- `eng/bbs/descope_qa_comment_delete.php` +- `eng/bbs/descope_qa_delete.php` +- `eng/bbs/descope_qa_status.php` +- `eng/bbs/admin_guard.php` + +## 1. 정책 확정 + +- [ ] 내부 사용자와 외부 사용자의 로그인 정책을 문서로 확정한다. +- [ ] 내부 사용자 판별 기준을 확정한다. +- [ ] 내부 사용자 판별을 이메일 도메인으로 유지할지, 별도 진입 버튼으로 분기할지 결정한다. +- [ ] 로그인 화면에서 외부용 Descope 와 내부용 BARON-SSO 를 어떻게 노출할지 결정한다. +- [ ] 국문 `egbim/` 와 영문 `eng/` 모두 동일 정책을 적용할지 확인한다. + +### 정책 결정 시 확인 항목 + +- [ ] 내부 사용자 도메인 목록 확정 +- [ ] 예외 계정 처리 방식 확정 +- [ ] 관리자 계정 판단 기준 유지 여부 확정 +- [ ] 로그인 실패 시 안내 문구와 리다이렉트 방식 확정 + +## 2. BARON-SSO 연동 정보 정리 + +- [ ] BARON-SSO OIDC 설정값을 확정한다. +- [ ] `client_id`, `client_secret`, `discovery_endpoint`, `issuer`, `authorization_endpoint`, `token_endpoint`, `userinfo_endpoint` 를 운영값 기준으로 문서화한다. +- [ ] Redirect URI 를 실제 서비스 경로와 맞춘다. +- [ ] 운영 도메인이 `eg-bim.com` 인지 `eg-bim.co.kr` 인지 최종 확정한다. +- [ ] 로컬/개발/운영 환경별 Redirect URI 분리 여부를 결정한다. + +### 현재 확정된 방식 + +- [x] 보안 수준: `Server-side app` +- [x] 인증 플로우: 서버에서 Authorization Code 처리 +- [x] `client_secret` 필요 +- [ ] PKCE 지원 여부 확인 +- [ ] `scope` 목록 확정 + +### 확인 필요 항목 + +- [ ] 현재 등록된 Redirect URI: `https://eg-bim.com/auth/callback` +- [ ] 실제 애플리케이션에서 사용할 콜백 경로가 존재하는지 확인 +- [ ] 사내망 또는 VPN 환경에서만 접근 가능한지 확인 +- [ ] BARON-SSO 로그아웃 엔드포인트 제공 여부 확인 +- [ ] 로컬 테스트용 Redirect URI 추가: `http://localhost:8088/auth/callback` + +## 3. 공통 인증 계층 설계 + +- [ ] Descope 전용 세션 파일을 공급자 중립적인 공통 인증 bootstrap 구조로 분리한다. +- [ ] 로그인 공급자가 달라도 최종 세션 구조는 동일하게 맞춘다. +- [ ] Q&A 코드는 특정 공급자명을 몰라도 되도록 정리한다. +- [ ] 국문/영문 트리에서 인증 공통화 파일 위치를 통일한다. + +### 권장 방향 + +- [ ] `descope_session.php` 를 바로 확장하지 말고 공통 auth loader 로 분리 +- [ ] Descope 와 BARON-SSO 모두 동일한 `$_SESSION['user']` 구조를 채우도록 통합 +- [ ] Q&A 는 공통 auth loader 만 include 하도록 변경 +- [ ] 공급자별 쿠키 복원, 세션 복원, 권한 매핑은 내부 모듈로 캡슐화 + +### 세션 표준 구조 초안 + +- [ ] `$_SESSION['user']['userId']` +- [ ] `$_SESSION['user']['loginIds'][0]` +- [ ] `$_SESSION['user']['name']` +- [ ] `$_SESSION['user']['email']` +- [ ] `$_SESSION['user']['phone']` +- [ ] `$_SESSION['user']['customAttributes']` +- [ ] `$_SESSION['user']['roleNames']` +- [ ] 그누보드 연동용 `$_SESSION['ss_mb_id']`, `$_SESSION['ss_mb_level']` + +## 4. BARON-SSO 인증 구현 + +- [ ] 로그인 시작 엔드포인트 구현 +- [ ] state, nonce 저장 및 검증 구현 +- [ ] callback 엔드포인트 구현 +- [ ] authorization code 를 token 으로 교환하는 로직 구현 +- [ ] id token 또는 userinfo 기반 사용자 정보 파싱 구현 +- [ ] 실패 시 예외 처리 및 사용자 안내 구현 + +### 구현 상세 체크리스트 + +- [x] 브라우저 직접 토큰 저장 방식은 사용하지 않음 +- [x] callback 에서 서버가 token endpoint 호출 +- [x] callback 성공 후 서버 세션에 사용자 정보 저장 +- [ ] OIDC discovery 자동 사용 여부 결정 +- [ ] discovery 미사용 시 endpoint 고정값 사용 +- [ ] 세션 고정 공격 방지용 session regenerate 적용 여부 확인 +- [ ] callback 에서 CSRF 방지용 state 검증 +- [ ] 토큰 검증 책임 범위 결정 +- [ ] 토큰 만료와 재로그인 정책 정의 + +### Server-side app 기준 추가 작업 + +- [ ] callback 에서 `code`, `state`, `error` 파라미터 처리 규칙 정의 +- [ ] token endpoint 요청 파라미터 목록 정리 +- [ ] `client_secret` 저장 위치 확정 +- [ ] 세션 생성 직후 `session_regenerate_id(true)` 적용 +- [ ] 로그인 성공 후 이동할 기본 URL 확정 +- [ ] 로그인 실패 시 복귀 URL 과 안내 문구 확정 + +## 5. 사용자 정보 매핑 규칙 정리 + +- [ ] BARON-SSO claim 샘플을 확보한다. +- [ ] BARON-SSO 의 사용자 식별자 중 무엇을 `loginIds[0]` 으로 쓸지 확정한다. +- [ ] 이름, 이메일, 전화번호, 회사, 계열사, 부서 claim 매핑 규칙을 정한다. +- [ ] BARON claim 에 없는 값은 기본값 또는 빈값 처리 기준을 정한다. + +### 현재 Q&A 가 기대하는 값 + +- [ ] 내부 사용자 저장 시 `family_company`, `department` 사용 +- [ ] 외부 사용자 저장 시 `company`, `department` 사용 +- [ ] 작성자 식별은 `login_id` 기준 +- [ ] 내 글 여부 판단도 `login_id` 기준 + +### 확인 필요 리스크 + +- [ ] BARON-SSO 가 이메일을 직접 주지 않는 경우 대응 필요 +- [ ] 부서명 claim key 가 `team` 과 다를 가능성 확인 필요 +- [ ] 계열사명 claim 이 `familyCompany` 와 다를 가능성 확인 필요 +- [ ] 영문 사이트에서 표시명 영문화가 필요한지 검토 + +## 6. Q&A 접근 제어 변경 + +- [ ] Q&A 목록에서 로그인 사용자 식별이 공통 세션으로 동작하는지 확인 +- [ ] Q&A 상세에서 비밀글 접근 제어가 내부 사용자에게도 동일하게 동작하는지 확인 +- [ ] Q&A 작성에서 로그인 강제가 BARON-SSO 사용자에게도 정상 동작하는지 확인 +- [ ] 댓글 작성, 수정, 삭제도 동일 세션 구조 기준으로 동작하는지 확인 +- [ ] 관리자 상태 변경 기능이 정상 동작하는지 확인 + +### 변경 대상 포인트 + +- [ ] `require_once .../descope_session.php` 참조를 공통 auth bootstrap 으로 치환 +- [ ] 로그인 여부 조건식이 특정 공급자 쿠키에 의존하지 않는지 점검 +- [ ] 작성자 식별 키가 공급자별로 달라지지 않도록 통일 +- [ ] 비로그인 사용자 공개글 조회 정책 유지 여부 확인 + +## 7. 관리자 권한 체계 정리 + +- [ ] 현재 이메일 화이트리스트 방식을 유지할지 결정한다. +- [ ] BARON-SSO role 또는 group claim 기반으로 바꿀지 결정한다. +- [ ] 관리자 표시 이름 가공 로직과 충돌이 없는지 확인한다. + +### 권장 방향 + +- [ ] 1차 전환은 이메일 화이트리스트 유지 +- [ ] 2차에서 BARON role 기반 권한 이관 검토 +- [ ] 권한 기준은 국문/영문 공통으로 단일화 + +## 8. 로그인 화면 및 UX 변경 + +- [ ] 메인 헤더 또는 로그인 팝업에 내부 사용자용 BARON-SSO 진입 버튼 추가 +- [ ] 외부 사용자용 기존 Descope 로그인 버튼 유지 +- [ ] 로그인 실패 안내 문구를 공급자별로 구분할지 결정 +- [ ] 내부 사용자가 Descope 로그인으로 잘못 진입했을 때 안내 문구 제공 여부 결정 + +### 권장 UX + +- [ ] 버튼 2개 분리: `외부 사용자 로그인`, `사내 사용자 로그인` +- [ ] 최초 단계에서는 자동 도메인 판별보다 명시적 분기 우선 +- [ ] 내부 사용자 안내 문구에 사내 계정 사용 정책 명시 + +## 9. 로그아웃 및 세션 종료 처리 + +- [ ] Descope 로그아웃은 기존 방식 유지 +- [ ] BARON-SSO 로그아웃 처리 추가 +- [ ] 로컬 세션 종료와 IdP 세션 종료를 분리할지 결정 +- [ ] 로그아웃 후 재진입 시 자동 재로그인 여부 확인 + +### 점검 항목 + +- [ ] PHP 세션 제거 +- [ ] Descope 관련 쿠키 제거 +- [ ] BARON 관련 쿠키 또는 state 쿠키 제거 +- [ ] 브라우저 백버튼 진입 시 인증 상태 오동작 여부 확인 + +## 10. 설정값 및 비밀정보 관리 + +- [ ] BARON-SSO 설정값을 코드 하드코딩 없이 분리한다. +- [ ] 운영/개발/로컬 설정 파일 구조를 정리한다. +- [ ] 민감정보가 저장소에 올라가지 않도록 관리 방식을 정한다. + +### 체크리스트 + +- [ ] `client_secret` 는 환경변수 또는 비공개 설정 파일로 관리 +- [ ] callback URL 도 환경별 분리 +- [ ] issuer, endpoint 값도 환경별 override 가능하도록 구성 +- [ ] 배포 문서에 설정 절차 추가 + +## 11. 테스트 시나리오 + +- [ ] 외부 사용자 Descope 로그인 후 Q&A 목록 조회 +- [ ] 외부 사용자 Descope 로그인 후 Q&A 작성 +- [ ] 내부 사용자 BARON-SSO 로그인 후 Q&A 목록 조회 +- [ ] 내부 사용자 BARON-SSO 로그인 후 Q&A 작성 +- [ ] 내부 사용자 BARON-SSO 로그인 후 댓글 작성 +- [ ] 내부 관리자 BARON-SSO 로그인 후 상태 변경 +- [ ] 비로그인 사용자 공개글 조회 +- [ ] 비밀글 작성자 본인 접근 +- [ ] 비밀글 타 사용자 접근 차단 +- [ ] 로그아웃 후 접근 차단 +- [ ] 국문 `egbim/` 검증 +- [ ] 영문 `eng/` 검증 + +### 추가 확인 항목 + +- [ ] 모바일 브라우저 로그인 진입 확인 +- [ ] 로그인 팝업 차단 이슈 여부 확인 +- [ ] 세션 만료 후 재시도 흐름 확인 +- [ ] 첨부파일 다운로드 권한 흐름 확인 + +## 12. 배포 및 롤백 계획 + +- [ ] BARON-SSO 기능 배포 순서를 정한다. +- [ ] 운영 반영 전 내부 사용자 테스트 계정으로 사전 검증한다. +- [ ] 장애 시 내부 사용자를 임시 우회시킬 방법을 준비한다. + +### 권장 배포 순서 + +- [ ] 1차: 공통 auth bootstrap 도입 +- [ ] 2차: BARON-SSO callback 및 세션 매핑 구현 +- [ ] 3차: 로그인 UI 에 사내 사용자 버튼 노출 +- [ ] 4차: 내부 사용자 실사용 테스트 +- [ ] 5차: 운영 전환 + +### 롤백 포인트 + +- [ ] BARON-SSO 버튼 숨김만으로 즉시 차단 가능한지 확인 +- [ ] 내부 사용자 임시 Descope 허용 스위치 필요 여부 검토 +- [ ] 공통 auth bootstrap 에 공급자 fallback 허용 여부 검토 + +## 바로 실행할 우선순위 + +### P0 + +- [ ] 내부/외부 로그인 정책 확정 +- [ ] BARON-SSO claim 샘플 확보 +- [ ] Redirect URI 와 실제 콜백 경로 확정 +- [ ] 공통 세션 구조 정의 + +## 다음 진행 항목 + +### 지금 바로 할 일 + +- [ ] BARON-SSO callback URL 을 실제 구현 경로로 확정한다. +- [ ] BARON-SSO 에서 내려주는 id token 또는 userinfo claim 샘플 1건을 확보한다. +- [ ] 공통 auth bootstrap 파일명을 확정한다. +- [ ] Descope 세션 구조와 BARON 세션 구조 매핑표를 작성한다. + +### 다음 구현 순서 + +- [ ] 1단계: BARON 로그인 시작 엔드포인트 추가 +- [ ] 2단계: BARON callback 엔드포인트 추가 +- [ ] 3단계: callback 에서 공통 `$_SESSION['user']` 생성 +- [ ] 4단계: Q&A 진입 파일들의 `descope_session.php` include 를 공통 auth bootstrap 으로 교체 +- [ ] 5단계: 로그인 UI 에 사내 사용자 로그인 버튼 추가 + +### 구현 전에 확정되면 좋은 값 + +- [ ] 내부 사용자 기본 이동 페이지 +- [ ] 로그인 성공 후 이동 페이지가 메인인지 Q&A 인지 결정 +- [ ] 로그아웃 후 이동 페이지 결정 +- [ ] BARON-SSO 에서 제공하는 부서, 계열사, 이메일 claim key 명칭 확정 + +### P1 + +- [ ] 공통 auth bootstrap 구현 +- [ ] BARON-SSO callback 구현 +- [ ] Q&A 진입점 공통 auth 로 치환 +- [ ] 로그아웃 처리 추가 + +### P2 + +- [ ] 로그인 UI 개선 +- [ ] 관리자 권한 체계 정리 +- [ ] 영문 사이트 동기화 +- [ ] 운영 문서와 배포 절차 정리 + +## 메모 + +- 현재 구조에서는 Q&A 비즈니스 로직보다 인증 세션 통합이 핵심 작업이다. +- 가장 안전한 방향은 BARON-SSO 사용자도 최종적으로 기존과 동일한 `$_SESSION['user']` 구조를 갖게 만드는 것이다. +- 이렇게 하면 Q&A 자체의 수정량을 줄이고, 외부 사용자용 Descope 흐름도 그대로 유지할 수 있다. \ No newline at end of file diff --git a/DEPLOY_GITEA.md b/DEPLOY_GITEA.md index 52d83bf..2459948 100644 --- a/DEPLOY_GITEA.md +++ b/DEPLOY_GITEA.md @@ -138,6 +138,15 @@ Gitea 저장소 화면에서 아래 순서로 실행한다. - `G5_MYSQL_PASSWORD`: 운영 DB 비밀번호 - `MARIADB_ROOT_PASSWORD`: 운영 MariaDB root 비밀번호 - `COMPOSE_PROJECT_NAME`: 예) `egbim` +- `BARON_SSO_CLIENT_ID`: BARON 서버사이드 앱 Client ID +- `BARON_SSO_CLIENT_SECRET`: BARON 서버사이드 앱 Client Secret +- `BARON_SSO_AUTHORIZE_ENDPOINT`: `https://app.brsw.kr/oidc/oauth2/auth` +- `BARON_SSO_TOKEN_ENDPOINT`: `https://app.brsw.kr/oidc/oauth2/token` +- `BARON_SSO_USERINFO_ENDPOINT`: `https://app.brsw.kr/oidc/userinfo` +- `BARON_SSO_ISSUER`: `https://app.brsw.kr/oidc` +- `BARON_SSO_REDIRECT_URI`: 운영 콜백 URL 예) `https://<운영도메인>/auth/callback` +- `BARON_SSO_SCOPE`: 예) `openid profile email tenants` +- `BARON_SSO_CLIENT_AUTH_METHOD`: 보통 `basic`, 필요 시 `post` 이 워크플로는 위 시크릿으로 서버의 `.env` 파일을 생성한 뒤 배포를 수행한다. @@ -169,6 +178,15 @@ G5_MYSQL_USER=egbim G5_MYSQL_PASSWORD= MARIADB_ROOT_PASSWORD= COMPOSE_PROJECT_NAME=egbim +BARON_SSO_CLIENT_ID= +BARON_SSO_CLIENT_SECRET= +BARON_SSO_AUTHORIZE_ENDPOINT=https://app.brsw.kr/oidc/oauth2/auth +BARON_SSO_TOKEN_ENDPOINT=https://app.brsw.kr/oidc/oauth2/token +BARON_SSO_USERINFO_ENDPOINT=https://app.brsw.kr/oidc/userinfo +BARON_SSO_ISSUER=https://app.brsw.kr/oidc +BARON_SSO_REDIRECT_URI=https://<운영도메인>/auth/callback +BARON_SSO_SCOPE=openid profile email tenants +BARON_SSO_CLIENT_AUTH_METHOD=basic ``` ## 5-1. SQL 덤프 포함 주의 diff --git a/DOCKER_LOCAL.md b/DOCKER_LOCAL.md index 398239b..8abc347 100644 --- a/DOCKER_LOCAL.md +++ b/DOCKER_LOCAL.md @@ -13,6 +13,7 @@ 2. 필요하면 `.env.docker.example` 내용을 참고해 `.env` 파일을 만든다. 3. DB 덤프가 있으면 `docker/mysql/init/` 아래에 `.sql` 파일로 넣는다. 4. 기본 로컬 DB명은 운영과 맞춘 `egbim` 이다. +5. BARON-SSO 를 로컬에서 테스트하려면 BARON 앱 설정에 로컬 Redirect URI 를 추가해야 한다. ## 실행 @@ -20,6 +21,57 @@ docker compose up --build ``` +## BARON-SSO 로컬 테스트 설정 + +`.env` 에 아래 값을 채운다. + +```dotenv +BARON_SSO_CLIENT_ID= +BARON_SSO_CLIENT_SECRET= +BARON_SSO_AUTHORIZE_ENDPOINT=https://app.brsw.kr/oidc/oauth2/auth +BARON_SSO_TOKEN_ENDPOINT=https://app.brsw.kr/oidc/oauth2/token +BARON_SSO_USERINFO_ENDPOINT=https://app.brsw.kr/oidc/userinfo +BARON_SSO_ISSUER=https://app.brsw.kr/oidc +BARON_SSO_REDIRECT_URI=http://localhost:8088/auth/callback +BARON_SSO_SCOPE=openid profile email tenants +BARON_SSO_CLIENT_AUTH_METHOD=basic +``` + +`BARON_SSO_CLIENT_AUTH_METHOD` 는 우선 `basic` 으로 두고, 토큰 엔드포인트가 `invalid_client` 를 반환하면 `post` 로 바꿔 다시 확인한다. + +BARON 관리자 화면의 Redirect URI 허용 목록에도 아래 중 하나를 추가한다. + +```text +http://localhost:8088/auth/callback +http://eg-bim.com:8088/auth/callback +``` + +운영에 등록된 `https://eg-bim.com/auth/callback` 만 허용되어 있으면 로컬 callback 은 실패한다. + +## 로컬 확인 순서 + +1. `docker compose up --build -d` +2. `docker compose ps` 로 `app`, `db` 상태 확인 +3. 브라우저에서 `http://localhost:8088/egbim/` 접속 +4. 로그인 팝업에서 `사내 사용자 로그인` 버튼 클릭 +5. BARON 로그인 후 `http://localhost:8088/auth/callback` 로 복귀하는지 확인 +6. 복귀 후 `http://localhost:8088/egbim/bbs/descope_qa_list.php` 로 이동하는지 확인 + +## 빠른 점검 URL + +- BARON 시작점: `http://localhost:8088/auth/baron_login?site=egbim&return_url=/egbim/bbs/descope_qa_list.php` +- BARON 콜백: `http://localhost:8088/auth/callback` +- 국문 Q&A: `http://localhost:8088/egbim/bbs/descope_qa_list.php` +- 영문 Q&A: `http://localhost:8088/eng/bbs/descope_qa_list.php` + +## 문제 발생 시 먼저 볼 항목 + +- Redirect URI 가 BARON 허용 목록과 정확히 일치하는지 확인 +- `.env` 값이 비어 있지 않은지 확인 +- 컨테이너 재기동 전에 `.env` 변경분이 반영되었는지 확인 +- callback 경로가 `/auth/callback` 으로 등록되어 있는지 확인 +- 브라우저 개발자도구 Network 에서 token endpoint 호출 전 redirect 단계가 정상인지 확인 + ## 접속 - 기본: `http://localhost:8080/egbim/` diff --git a/auth/baron_login.php b/auth/baron_login.php new file mode 100644 index 0000000..8db5c8b --- /dev/null +++ b/auth/baron_login.php @@ -0,0 +1,28 @@ + $config['client_id'], + 'response_type' => 'code', + 'redirect_uri' => auth_baron_redirect_uri(), + 'scope' => $config['scope'], + 'state' => $state, + 'nonce' => $statePayload['nonce'], +]); + +header('Location: ' . $config['authorize_endpoint'] . '?' . $query, true, 302); +exit; +?> \ No newline at end of file diff --git a/auth/callback.php b/auth/callback.php new file mode 100644 index 0000000..6ec2c31 --- /dev/null +++ b/auth/callback.php @@ -0,0 +1,170 @@ + 'authorization_code', + 'redirect_uri' => auth_baron_redirect_uri(), + 'code' => $code, +]; + +$basicAuthUser = null; +$basicAuthPassword = null; + +if ($config['client_auth_method'] === 'post') { + $tokenRequestData['client_id'] = $config['client_id']; + $tokenRequestData['client_secret'] = $config['client_secret']; +} else { + $tokenRequestData['client_id'] = $config['client_id']; + $basicAuthUser = $config['client_id']; + $basicAuthPassword = $config['client_secret']; +} + +$tokenResponse = auth_http_post_form( + $config['token_endpoint'], + $tokenRequestData, + [], + $basicAuthUser, + $basicAuthPassword +); + +if ($tokenResponse['error'] !== '') { + auth_script_redirect('BARON-SSO 토큰 요청에 실패했습니다.', auth_site_base_url($site) . '/index.php'); +} + +$tokenData = json_decode($tokenResponse['body'], true); +if ($tokenResponse['http_code'] < 200 || $tokenResponse['http_code'] >= 300 || !is_array($tokenData)) { + if (auth_is_local_debug()) { + $debugMessage = sprintf( + 'BARON token error HTTP %d | body=%s', + $tokenResponse['http_code'], + auth_compact_debug_value((string) $tokenResponse['body']) + ); + auth_script_redirect($debugMessage, auth_site_base_url($site) . '/index.php'); + } + + auth_script_redirect('BARON-SSO 토큰 응답을 처리할 수 없습니다.', auth_site_base_url($site) . '/index.php'); +} + +$claims = []; +$idToken = trim((string) ($tokenData['id_token'] ?? '')); +if ($idToken !== '') { + $claims = auth_jwt_payload($idToken); +} + +if ($config['userinfo_endpoint'] !== '' && !empty($tokenData['access_token'])) { + $userInfoResponse = auth_http_get_json($config['userinfo_endpoint'], (string) $tokenData['access_token']); + $userInfoData = json_decode($userInfoResponse['body'], true); + if ($userInfoResponse['http_code'] >= 200 && $userInfoResponse['http_code'] < 300 && is_array($userInfoData)) { + $claims = array_merge($claims, $userInfoData); + } +} + +$user = auth_baron_user_from_claims($claims); +if (empty($user['userId']) || empty($user['loginIds'])) { + auth_script_redirect('BARON-SSO 사용자 정보를 확인할 수 없습니다.', auth_site_base_url($site) . '/index.php'); +} + +$normalizedUser = auth_apply_user_session($user, 'baron', [ + 'baron_access_token' => (string) ($tokenData['access_token'] ?? ''), + 'baron_id_token' => $idToken, +]); + +setcookie('descope_login_id', '', auth_cookie_options(time() - 42000)); +setcookie('descope_user_id', '', auth_cookie_options(time() - 42000)); +setcookie('descope_user_name', '', auth_cookie_options(time() - 42000)); +setcookie('descope_user_email', '', auth_cookie_options(time() - 42000)); +setcookie('descope_user_phone', '', auth_cookie_options(time() - 42000)); +setcookie('descope_custom_attributes', '', auth_cookie_options(time() - 42000)); +setcookie('descope_role_names', '', auth_cookie_options(time() - 42000)); +auth_set_descope_session_cookies($normalizedUser); +auth_set_baron_session_cookies($normalizedUser, $claims); + +$clientSessionPayload = [ + 'loginId' => (string) ($normalizedUser['loginIds'][0] ?? ''), + 'descopeUserId' => (string) ($normalizedUser['userId'] ?? ''), + 'userName' => (string) ($normalizedUser['name'] ?? ''), + 'phone' => (string) ($normalizedUser['phone'] ?? ''), + 'company' => (string) ($normalizedUser['customAttributes']['company'] ?? ''), + 'familyCompany' => (string) ($normalizedUser['customAttributes']['familyCompany'] ?? ''), + 'team' => (string) ($normalizedUser['customAttributes']['team'] ?? ''), + 'position' => (string) ($normalizedUser['customAttributes']['position'] ?? ''), + 'familyUniqueKey' => (string) ($normalizedUser['customAttributes']['familyUniqueKey'] ?? ''), + 'userRole' => (string) (($normalizedUser['roleNames'][0] ?? '')), + 'authProvider' => 'baron', + 'sessionJwt' => $idToken !== '' ? $idToken : (string) ($tokenData['access_token'] ?? ''), + 'baronClaims' => json_encode($claims, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), +]; + +$jsonPayload = json_encode($clientSessionPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$jsonRedirectUrl = json_encode($redirectUrl, JSON_UNESCAPED_SLASHES); + +header('Content-Type: text/html; charset=UTF-8'); +echo << + + + + + + + Signing in... + + + + + +HTML; +exit; +?> \ No newline at end of file diff --git a/auth/common.php b/auth/common.php new file mode 100644 index 0000000..dd40b64 --- /dev/null +++ b/auth/common.php @@ -0,0 +1,790 @@ + $expires, + 'path' => '/', + 'samesite' => 'Lax', + ]; +} + +function auth_json_cookie_encode($value): string +{ + return base64_encode(json_encode($value, JSON_UNESCAPED_UNICODE)); +} + +function auth_json_cookie_decode(string $value, $default) +{ + if ($value === '') { + return $default; + } + + $decoded = base64_decode($value, true); + if ($decoded === false) { + return $default; + } + + $json = json_decode($decoded, true); + + return $json === null ? $default : $json; +} + +function auth_current_host(): string +{ + return $_SERVER['HTTP_HOST'] ?? 'eg-bim.com'; +} + +function auth_current_scheme(): string +{ + $https = $_SERVER['HTTPS'] ?? ''; + $forwardedProto = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''; + + if ($https === 'on' || $https === '1' || strtolower((string) $forwardedProto) === 'https') { + return 'https'; + } + + return 'http'; +} + +function auth_absolute_url(string $path): string +{ + return auth_current_scheme() . '://' . auth_current_host() . $path; +} + +function auth_site_base_url(string $site): string +{ + return $site === 'eng' ? '/eng' : '/egbim'; +} + +function auth_site_qna_url(string $site): string +{ + return auth_site_base_url($site) . '/bbs/descope_qa_list.php'; +} + +function auth_baron_config(): array +{ + $scopes = trim((string) (getenv('BARON_SSO_SCOPE') ?: 'openid profile email')); + + return [ + 'client_id' => trim((string) getenv('BARON_SSO_CLIENT_ID')), + 'client_secret' => trim((string) getenv('BARON_SSO_CLIENT_SECRET')), + 'authorize_endpoint' => trim((string) getenv('BARON_SSO_AUTHORIZE_ENDPOINT')), + 'token_endpoint' => trim((string) getenv('BARON_SSO_TOKEN_ENDPOINT')), + 'userinfo_endpoint' => trim((string) getenv('BARON_SSO_USERINFO_ENDPOINT')), + 'issuer' => trim((string) getenv('BARON_SSO_ISSUER')), + 'redirect_uri' => trim((string) getenv('BARON_SSO_REDIRECT_URI')), + 'scope' => $scopes, + 'client_auth_method' => trim((string) (getenv('BARON_SSO_CLIENT_AUTH_METHOD') ?: 'basic')), + ]; +} + +function auth_baron_redirect_uri(): string +{ + $config = auth_baron_config(); + if ($config['redirect_uri'] !== '') { + return $config['redirect_uri']; + } + + return auth_absolute_url('/auth/callback'); +} + +function auth_baron_is_configured(): bool +{ + $config = auth_baron_config(); + + return $config['client_id'] !== '' + && $config['client_secret'] !== '' + && $config['authorize_endpoint'] !== '' + && $config['token_endpoint'] !== ''; +} + +function auth_normalize_user(array $user): array +{ + $loginIds = $user['loginIds'] ?? []; + if (!is_array($loginIds)) { + $loginIds = []; + } + + $loginId = trim((string) ($loginIds[0] ?? $user['loginId'] ?? $user['email'] ?? '')); + if ($loginId !== '' && empty($loginIds)) { + $loginIds = [$loginId]; + } + + $customAttributes = $user['customAttributes'] ?? []; + $roleNames = $user['roleNames'] ?? []; + + return [ + 'userId' => trim((string) ($user['userId'] ?? $loginId)), + 'loginIds' => $loginIds, + 'name' => trim((string) ($user['name'] ?? '')), + 'email' => trim((string) ($user['email'] ?? $loginId)), + 'phone' => trim((string) ($user['phone'] ?? '')), + 'customAttributes' => is_array($customAttributes) ? $customAttributes : [], + 'roleNames' => is_array($roleNames) ? $roleNames : [], + ]; +} + +function auth_descope_user_from_cookies(): ?array +{ + $loginId = trim((string) ($_COOKIE['descope_login_id'] ?? '')); + if ($loginId === '') { + return null; + } + + return auth_normalize_user([ + 'userId' => trim((string) ($_COOKIE['descope_user_id'] ?? $loginId)), + 'loginIds' => [$loginId], + 'name' => trim((string) ($_COOKIE['descope_user_name'] ?? '')), + 'email' => trim((string) ($_COOKIE['descope_user_email'] ?? $loginId)), + 'phone' => trim((string) ($_COOKIE['descope_user_phone'] ?? '')), + 'customAttributes' => auth_json_cookie_decode((string) ($_COOKIE['descope_custom_attributes'] ?? ''), []), + 'roleNames' => auth_json_cookie_decode((string) ($_COOKIE['descope_role_names'] ?? ''), []), + ]); +} + +function auth_baron_user_from_cookies(): ?array +{ + $encodedUser = (string) ($_COOKIE['baron_user'] ?? ''); + if ($encodedUser === '') { + return null; + } + + $user = auth_json_cookie_decode($encodedUser, []); + if (!is_array($user) || empty($user)) { + return null; + } + + return auth_normalize_user($user); +} + +function auth_restore_user_session(): void +{ + auth_session_start_if_needed(); + + if (!empty($_SESSION['user']['userId'])) { + $_SESSION['user'] = auth_normalize_user($_SESSION['user']); + return; + } + + $user = auth_baron_user_from_cookies(); + if ($user === null) { + $user = auth_descope_user_from_cookies(); + } + + if ($user !== null) { + $_SESSION['user'] = $user; + } +} + +function auth_apply_user_session(array $user, string $provider, array $tokens = []): array +{ + auth_session_start_if_needed(); + session_regenerate_id(true); + + $normalizedUser = auth_normalize_user($user); + + $_SESSION['user'] = $normalizedUser; + $_SESSION['auth_provider'] = $provider; + + foreach ($tokens as $key => $value) { + $_SESSION[$key] = $value; + } + + return $normalizedUser; +} + +function auth_set_descope_session_cookies(array $user): void +{ + $normalizedUser = auth_normalize_user($user); + $expire = time() + 86400; + $cookieOptions = auth_cookie_options($expire); + + setcookie('descope_login_id', (string) ($normalizedUser['loginIds'][0] ?? ''), $cookieOptions); + setcookie('descope_user_id', (string) $normalizedUser['userId'], $cookieOptions); + setcookie('descope_user_name', (string) $normalizedUser['name'], $cookieOptions); + setcookie('descope_user_email', (string) $normalizedUser['email'], $cookieOptions); + setcookie('descope_user_phone', (string) $normalizedUser['phone'], $cookieOptions); + setcookie('descope_custom_attributes', auth_json_cookie_encode($normalizedUser['customAttributes']), $cookieOptions); + setcookie('descope_role_names', auth_json_cookie_encode($normalizedUser['roleNames']), $cookieOptions); +} + +function auth_set_baron_session_cookies(array $user, array $claims = []): void +{ + $normalizedUser = auth_normalize_user($user); + $expire = time() + 86400; + $cookieOptions = auth_cookie_options($expire); + + setcookie('baron_user', auth_json_cookie_encode($normalizedUser), $cookieOptions); + setcookie('baron_claims', auth_json_cookie_encode($claims), $cookieOptions); + setcookie('baron_provider', 'baron', $cookieOptions); +} + +function auth_clear_all_cookies(): void +{ + $expired = auth_cookie_options(time() - 42000); + + foreach ([ + 'PHPSESSID', + 'G5sessphp', + 'descope_login_id', + 'descope_user_id', + 'descope_user_name', + 'descope_user_email', + 'descope_user_phone', + 'descope_custom_attributes', + 'descope_role_names', + 'baron_user', + 'baron_claims', + 'baron_provider', + ] as $cookieName) { + setcookie($cookieName, '', $expired); + } +} + +function auth_admin_emails(): array +{ + return [ + 'b23008@baroncs.co.kr', + 'kjy0426@hanmaceng.co.kr', + 'b24014@hanmaceng.co.kr', + 'b23065@hanmaceng.co.kr', + 'shyeom1@samaneng.com', + 'cjy627@hanmaceng.co.kr', + 'b23072@hanmaceng.co.kr', + 'cozyjin@hanmaceng.co.kr', + 'm24031@hanmaceng.co.kr', + 'b24051@hanmaceng.co.kr', + 'rmsgud1202@hanmaceng.co.kr', + 'm21318@hanmaceng.co.kr', + 'b21367@hanmaceng.co.kr', + 'b25023@hanmaceng.co.kr', + 'sdi9429@naver.com', + 'junsuy@hanmail.net', + 'ilphilo92@gmail.com', + ]; +} + +function auth_normalize_identity(string $value): string +{ + return strtolower(trim($value)); +} + +function auth_is_admin_login(string $loginId): bool +{ + $normalizedLoginId = auth_normalize_identity($loginId); + if ($normalizedLoginId === '') { + return false; + } + + foreach (auth_admin_emails() as $adminLoginId) { + if ($normalizedLoginId === auth_normalize_identity($adminLoginId)) { + return true; + } + } + + return false; +} + +function auth_is_admin_user(array $user, ?string $loginId = null): bool +{ + $candidates = []; + + if ($loginId !== null) { + $candidates[] = $loginId; + } + + $loginIds = $user['loginIds'] ?? []; + if (is_array($loginIds)) { + foreach ($loginIds as $candidate) { + if (is_string($candidate)) { + $candidates[] = $candidate; + } + } + } + + $candidates[] = (string) ($user['email'] ?? ''); + + $customAttributes = $user['customAttributes'] ?? []; + if (is_array($customAttributes)) { + $candidates[] = (string) ($customAttributes['employeeId'] ?? ''); + $candidates[] = (string) ($customAttributes['familyUniqueKey'] ?? ''); + } + + foreach ($candidates as $candidate) { + if (auth_is_admin_login((string) $candidate)) { + return true; + } + } + + $roleNames = $user['roleNames'] ?? []; + if (is_array($roleNames)) { + foreach ($roleNames as $roleName) { + $normalizedRole = auth_normalize_identity((string) $roleName); + if ($normalizedRole === 'super' || $normalizedRole === 'admin' || $normalizedRole === 'administrator') { + return true; + } + } + } + + return false; +} + +function auth_sync_gnuboard_session(): array +{ + auth_restore_user_session(); + + $loginIds = $_SESSION['user']['loginIds'] ?? []; + $loginId = (is_array($loginIds) && !empty($loginIds)) ? trim((string) $loginIds[0]) : ''; + $userName = trim((string) ($_SESSION['user']['name'] ?? '')); + $userEmail = trim((string) ($_SESSION['user']['email'] ?? '')); + $isAdmin = !empty($_SESSION['user']) && is_array($_SESSION['user']) + ? auth_is_admin_user($_SESSION['user'], $loginId) + : false; + + if ($loginId !== '') { + $_SESSION['ss_mb_id'] = $loginId; + $_SESSION['ss_mb_level'] = $isAdmin ? 10 : 2; + + return [ + 'loginId' => $loginId, + 'isAdmin' => $isAdmin, + 'is_member' => true, + 'is_admin' => $isAdmin ? 'super' : '', + 'member' => [ + 'mb_id' => $loginId, + 'mb_name' => $userName !== '' ? $userName : $loginId, + 'mb_email' => $userEmail, + 'mb_level' => $_SESSION['ss_mb_level'], + 'mb_nick' => $userName !== '' ? $userName : $loginId, + ], + ]; + } + + unset($_SESSION['ss_mb_id'], $_SESSION['ss_mb_level']); + + return [ + 'loginId' => '', + 'isAdmin' => false, + 'is_member' => false, + 'is_admin' => '', + 'member' => [], + ]; +} + +function auth_url_is_safe(string $url, string $site): bool +{ + if ($url === '') { + return false; + } + + if (strpos($url, '://') !== false) { + return false; + } + + return strpos($url, auth_site_base_url($site) . '/') === 0; +} + +function auth_baron_state_payload(string $site, string $returnUrl): array +{ + return [ + 'site' => $site, + 'return_url' => auth_url_is_safe($returnUrl, $site) ? $returnUrl : auth_site_qna_url($site), + 'nonce' => bin2hex(random_bytes(16)), + 'created_at' => time(), + ]; +} + +function auth_store_baron_state(array $payload): string +{ + auth_session_start_if_needed(); + + $state = bin2hex(random_bytes(16)); + $_SESSION['baron_oauth_state'] = [ + 'value' => $state, + 'payload' => $payload, + ]; + + return $state; +} + +function auth_consume_baron_state(string $state): ?array +{ + auth_session_start_if_needed(); + + $stored = $_SESSION['baron_oauth_state'] ?? null; + unset($_SESSION['baron_oauth_state']); + + if (!is_array($stored) || ($stored['value'] ?? '') !== $state) { + return null; + } + + $payload = $stored['payload'] ?? null; + + return is_array($payload) ? $payload : null; +} + +function auth_http_post_form(string $url, array $data, array $headers = [], ?string $username = null, ?string $password = null): array +{ + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + + $mergedHeaders = array_merge([ + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded', + ], $headers); + curl_setopt($ch, CURLOPT_HTTPHEADER, $mergedHeaders); + + if ($username !== null) { + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_USERPWD, $username . ':' . ($password ?? '')); + } + + $response = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + return [ + 'body' => $response === false ? '' : $response, + 'http_code' => $httpCode, + 'error' => $error, + ]; +} + +function auth_http_get_json(string $url, string $accessToken): array +{ + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Accept: application/json', + 'Authorization: Bearer ' . $accessToken, + ]); + + $response = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + return [ + 'body' => $response === false ? '' : $response, + 'http_code' => $httpCode, + 'error' => $error, + ]; +} + +function auth_jwt_payload(string $jwt): array +{ + $parts = explode('.', $jwt); + if (count($parts) < 2) { + return []; + } + + $payload = strtr($parts[1], '-_', '+/'); + $padding = strlen($payload) % 4; + if ($padding > 0) { + $payload .= str_repeat('=', 4 - $padding); + } + + $decoded = base64_decode($payload, true); + if ($decoded === false) { + return []; + } + + $json = json_decode($decoded, true); + + return is_array($json) ? $json : []; +} + +function auth_first_non_empty_string(...$values): string +{ + foreach ($values as $value) { + if (is_string($value)) { + $trimmed = trim($value); + if ($trimmed !== '') { + return $trimmed; + } + } + } + + return ''; +} + +function auth_claim_list($value): array +{ + if (is_array($value)) { + return $value; + } + + $stringValue = trim((string) $value); + if ($stringValue === '') { + return []; + } + + return [$stringValue]; +} + +function auth_extract_phone_from_claims(array $claims): string +{ + $profile = isset($claims['profile']) && is_array($claims['profile']) ? $claims['profile'] : []; + $phones = auth_claim_list($claims['phones'] ?? ($profile['phones'] ?? [])); + + foreach ($phones as $phoneEntry) { + if (is_array($phoneEntry)) { + $candidate = auth_first_non_empty_string( + $phoneEntry['value'] ?? '', + $phoneEntry['phone_number'] ?? '', + $phoneEntry['number'] ?? '' + ); + if ($candidate !== '') { + return $candidate; + } + continue; + } + + $candidate = trim((string) $phoneEntry); + if ($candidate !== '') { + return $candidate; + } + } + + return auth_first_non_empty_string( + $claims['phone_number'] ?? '', + $claims['phone'] ?? '', + $profile['phone_number'] ?? '', + $profile['phone'] ?? '' + ); +} + +function auth_extract_company_fields(array $claims): array +{ + $company = auth_first_non_empty_string( + $claims['company'] ?? '', + $claims['company_name'] ?? '', + $claims['organization'] ?? '', + $claims['organization_name'] ?? '', + $claims['tenant_name'] ?? '' + ); + $familyCompany = auth_first_non_empty_string( + $claims['familyCompany'] ?? '', + $claims['family_company'] ?? '', + $claims['group_name'] ?? '', + $claims['affiliate'] ?? '' + ); + $team = auth_first_non_empty_string( + $claims['team'] ?? '', + $claims['department'] ?? '', + $claims['dept'] ?? '', + $claims['division'] ?? '', + $claims['org_unit'] ?? '' + ); + + $tenants = auth_claim_list($claims['tenants'] ?? []); + + $primaryTenant = null; + $tenantId = trim((string) ($claims['tenant_id'] ?? '')); + + foreach ($tenants as $tenantKey => $tenant) { + if (!is_array($tenant)) { + continue; + } + + $candidateId = trim((string) ($tenant['id'] ?? (is_string($tenantKey) ? $tenantKey : ''))); + if (($tenant['isPrimary'] ?? false) === true || ($tenant['representative'] ?? false) === true || ($tenantId !== '' && $candidateId === $tenantId)) { + $primaryTenant = $tenant; + break; + } + } + + if ($primaryTenant === null) { + foreach ($tenants as $tenant) { + if (is_array($tenant)) { + $primaryTenant = $tenant; + break; + } + } + } + + if (is_array($primaryTenant)) { + $companyAncestor = ''; + $familyCompanyAncestor = ''; + $departmentAncestor = ''; + + $ancestors = $primaryTenant['ancestors'] ?? []; + if (is_array($ancestors)) { + foreach ($ancestors as $ancestor) { + if (!is_array($ancestor)) { + continue; + } + + $ancestorType = strtoupper(trim((string) ($ancestor['type'] ?? ''))); + $ancestorName = trim((string) ($ancestor['name'] ?? '')); + if ($ancestorName === '') { + continue; + } + + if ($companyAncestor === '' && $ancestorType === 'COMPANY') { + $companyAncestor = $ancestorName; + } + + if ($familyCompanyAncestor === '' && $ancestorType === 'COMPANY_GROUP') { + $familyCompanyAncestor = $ancestorName; + } + + if ($ancestorType === 'ORGANIZATION') { + $departmentAncestor = $ancestorName; + } + } + } + + $company = auth_first_non_empty_string( + $company, + $companyAncestor, + trim((string) ($primaryTenant['name'] ?? '')) + ); + + $familyCompany = auth_first_non_empty_string( + $familyCompany, + $companyAncestor, + $familyCompanyAncestor, + $company + ); + + $team = auth_first_non_empty_string( + $team, + $departmentAncestor, + trim((string) ($primaryTenant['name'] ?? '')) + ); + } + + foreach ($tenants as $tenant) { + if (!is_array($tenant)) { + if ($company === '') { + $company = trim((string) $tenant); + } + continue; + } + + if ($company === '') { + $company = auth_first_non_empty_string( + $tenant['name'] ?? '', + $tenant['display_name'] ?? '', + $tenant['company'] ?? '', + $tenant['company_name'] ?? '', + $tenant['organization'] ?? '' + ); + } + + if ($familyCompany === '') { + $familyCompany = auth_first_non_empty_string( + $tenant['familyCompany'] ?? '', + $tenant['family_company'] ?? '', + $tenant['group_name'] ?? '', + $tenant['affiliate'] ?? '' + ); + } + + if ($team === '') { + $team = auth_first_non_empty_string( + $tenant['team'] ?? '', + $tenant['department'] ?? '', + $tenant['dept'] ?? '', + $tenant['division'] ?? '', + $tenant['org_unit'] ?? '' + ); + } + } + + return [ + 'company' => $company, + 'familyCompany' => $familyCompany, + 'team' => $team, + ]; +} + +function auth_baron_user_from_claims(array $claims): array +{ + $profile = isset($claims['profile']) && is_array($claims['profile']) ? $claims['profile'] : []; + $email = trim((string) ($claims['email'] ?? $profile['email'] ?? $claims['upn'] ?? $claims['preferred_username'] ?? $claims['sub'] ?? '')); + $name = trim((string) ($claims['name'] ?? $profile['name'] ?? $claims['display_name'] ?? $claims['preferred_username'] ?? $email)); + $phone = auth_extract_phone_from_claims($claims); + $companyFields = auth_extract_company_fields($claims); + $company = $companyFields['company']; + $familyCompany = $companyFields['familyCompany']; + $team = $companyFields['team']; + $employeeId = trim((string) ($claims['employee_id'] ?? $profile['employee_id'] ?? $claims['employeeId'] ?? $claims['familyUniqueKey'] ?? '')); + $status = trim((string) ($claims['status'] ?? $profile['status'] ?? '')); + $tenants = auth_claim_list($claims['tenants'] ?? []); + $secondaryEmails = auth_claim_list($claims['secondary_emails'] ?? $claims['secondaryEmails'] ?? ($profile['secondary_emails'] ?? [])); + $roles = auth_claim_list($claims['roles'] ?? $claims['roleNames'] ?? []); + $position = auth_first_non_empty_string( + $claims['position'] ?? '', + $profile['position'] ?? '' + ); + + foreach ($tenants as $tenant) { + if (!is_array($tenant)) { + continue; + } + + if ($position === '') { + $position = auth_first_non_empty_string( + $tenant['grade'] ?? '', + $tenant['position'] ?? '' + ); + } + } + + return auth_normalize_user([ + 'userId' => trim((string) ($claims['sub'] ?? $email)), + 'loginIds' => $email !== '' ? [$email] : [], + 'name' => $name, + 'email' => $email, + 'phone' => $phone, + 'customAttributes' => [ + 'company' => $company, + 'familyCompany' => $familyCompany, + 'team' => $team, + 'position' => $position, + 'employeeId' => $employeeId, + 'familyUniqueKey' => $employeeId, + 'status' => $status, + 'tenants' => $tenants, + 'secondaryEmails' => $secondaryEmails, + ], + 'roleNames' => $roles, + ]); +} + +function auth_script_redirect(string $message, string $redirectUrl): void +{ + $safeMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); + $safeUrl = htmlspecialchars($redirectUrl, ENT_QUOTES, 'UTF-8'); + + echo ""; + exit; +} + +function auth_is_local_debug(): bool +{ + return (getenv('APP_ENV') ?: '') === 'local'; +} + +function auth_compact_debug_value(string $value, int $maxLength = 300): string +{ + $value = preg_replace('/\s+/', ' ', trim($value)) ?? ''; + if (strlen($value) <= $maxLength) { + return $value; + } + + return substr($value, 0, $maxLength) . '...'; +} \ No newline at end of file diff --git a/auth/debug_claims.php b/auth/debug_claims.php new file mode 100644 index 0000000..8924bdc --- /dev/null +++ b/auth/debug_claims.php @@ -0,0 +1,23 @@ + $_SESSION['auth_provider'] ?? '', + 'session_user' => is_array($sessionUser) ? $sessionUser : [], + 'baron_user_cookie' => $baronUser, + 'baron_claims_cookie' => is_array($baronClaims) ? $baronClaims : [], +], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +exit; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 080b9d1..ae20287 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,15 @@ services: G5_MYSQL_USER: ${G5_MYSQL_USER:-egbim} G5_MYSQL_PASSWORD: ${G5_MYSQL_PASSWORD:-baron3840!!} G5_MYSQL_DB: ${G5_MYSQL_DB:-egbim} + BARON_SSO_CLIENT_ID: ${BARON_SSO_CLIENT_ID:-} + BARON_SSO_CLIENT_SECRET: ${BARON_SSO_CLIENT_SECRET:-} + BARON_SSO_AUTHORIZE_ENDPOINT: ${BARON_SSO_AUTHORIZE_ENDPOINT:-} + BARON_SSO_TOKEN_ENDPOINT: ${BARON_SSO_TOKEN_ENDPOINT:-} + BARON_SSO_USERINFO_ENDPOINT: ${BARON_SSO_USERINFO_ENDPOINT:-} + BARON_SSO_ISSUER: ${BARON_SSO_ISSUER:-} + BARON_SSO_REDIRECT_URI: ${BARON_SSO_REDIRECT_URI:-} + BARON_SSO_SCOPE: ${BARON_SSO_SCOPE:-openid profile email} + BARON_SSO_CLIENT_AUTH_METHOD: ${BARON_SSO_CLIENT_AUTH_METHOD:-basic} ports: - "${APP_PORT:-8088}:80" volumes: diff --git a/egbim/bbs/admin_guard.php b/egbim/bbs/admin_guard.php index 931735a..e6048f2 100644 --- a/egbim/bbs/admin_guard.php +++ b/egbim/bbs/admin_guard.php @@ -1,31 +1,12 @@ $expire, - 'path' => '/', - 'samesite' => 'Lax', - ]; - - $customAttributes = $user['customAttributes'] ?? []; - $roleNames = $user['roleNames'] ?? []; - - setcookie('descope_login_id', (string)($user['loginIds'][0] ?? ''), $cookieOptions); - setcookie('descope_user_id', (string)($user['userId'] ?? ''), $cookieOptions); - setcookie('descope_user_name', (string)($user['name'] ?? ''), $cookieOptions); - setcookie('descope_user_email', (string)($user['email'] ?? ''), $cookieOptions); - setcookie('descope_user_phone', (string)($user['phone'] ?? ''), $cookieOptions); - setcookie('descope_custom_attributes', base64_encode(json_encode($customAttributes, JSON_UNESCAPED_UNICODE)), $cookieOptions); - setcookie('descope_role_names', base64_encode(json_encode($roleNames, JSON_UNESCAPED_UNICODE)), $cookieOptions); -} +auth_session_start_if_needed(); $email = trim($_POST['mb_id'] ?? ''); $password = trim($_POST['mb_password'] ?? ''); @@ -60,16 +42,24 @@ if ($http_code === 200 && isset($res_data['sessionJwt'])) { // 로그인 성공 → 실패횟수 초기화 $_SESSION['login_fail_count'] = 0; //session으로도 저장 - $_SESSION['user'] = $res_data['user']; // userId, email 등 - $_SESSION['sessionJwt'] = $res_data['sessionJwt']; - $_SESSION['refreshJwt'] = $res_data['refreshJwt']; - set_descope_session_cookies($res_data['user'] ?? []); + $normalizedUser = auth_apply_user_session( + $res_data['user'] ?? [], + 'descope', + [ + 'sessionJwt' => $res_data['sessionJwt'] ?? '', + 'refreshJwt' => $res_data['refreshJwt'] ?? '', + ] + ); + setcookie('baron_user', '', auth_cookie_options(time() - 42000)); + setcookie('baron_claims', '', auth_cookie_options(time() - 42000)); + setcookie('baron_provider', '', auth_cookie_options(time() - 42000)); + auth_set_descope_session_cookies($normalizedUser); echo json_encode([ 'status' => 'ok', 'sessionJwt' => $res_data['sessionJwt'] ?? '', // 'refreshJwt' => $res_data['refreshJwt'] ?? '', - 'user' => $res_data['user'] ?? [], + 'user' => $normalizedUser, 'session' => $_SESSION, ]); // } else { diff --git a/egbim/skin/member/basic/descope_logout.php b/egbim/skin/member/basic/descope_logout.php index 8132aeb..3a6983c 100644 --- a/egbim/skin/member/basic/descope_logout.php +++ b/egbim/skin/member/basic/descope_logout.php @@ -1,4 +1,5 @@ 'ok']); // exit; -session_start(); +auth_session_start_if_needed(); $_SESSION = []; - -foreach (['PHPSESSID', 'G5sessphp', 'descope_login_id', 'descope_user_id', 'descope_user_name', 'descope_user_email', 'descope_user_phone', 'descope_custom_attributes', 'descope_role_names'] as $cookieName) { - setcookie($cookieName, '', [ - 'expires' => time() - 42000, - 'path' => '/', - 'samesite' => 'Lax', - ]); -} - +auth_clear_all_cookies(); session_destroy(); http_response_code(200); diff --git a/egbim/skin/member/basic/descope_session.php b/egbim/skin/member/basic/descope_session.php index 0e6bebb..b363113 100644 --- a/egbim/skin/member/basic/descope_session.php +++ b/egbim/skin/member/basic/descope_session.php @@ -1,148 +1,11 @@ -// // alert('Q&A를 이용하시려면 로그인이 필요합니다.'); -// // window.location.href = '/egbim/index.php'; -// // -// // HTML; -// // exit; -// // } - -// // ✅ Descope 로그인 성공 시 g5_member 세션 동기화 -// $loginId = $_SESSION['user']['loginIds'][0] ?? ''; -// if ($loginId === 'b24014@hanmaceng.co.kr') { -// // g5_member 계정이 이미 존재한다고 가정 -// $_SESSION['ss_mb_id'] = $loginId; -// $_SESSION['ss_mb_level'] = 10; - -// $is_member = true; -// $is_admin = 'super'; - -// $member = [ -// 'mb_id' => $loginId, -// 'mb_name' => $_SESSION['user']['name'] ?? '관리자', -// 'mb_email' => $_SESSION['user']['email'] ?? '', -// 'mb_level' => 10, -// 'mb_nick' => '관리자', -// ]; -// } -// // Descope 로그인 성공 시 (예: descope_session.php 내부) -// if (!empty($_SESSION['user']['loginIds'][0])) { -// $loginId = $_SESSION['user']['loginIds'][0]; - -// // g5_member 테이블에 계정이 존재한다고 가정 -// $_SESSION['ss_mb_id'] = $loginId; -// $_SESSION['ss_mb_level'] = 10; // 관리자 레벨 - -// // 그누보드 전역 변수 보정 -// $is_member = true; -// $is_admin = 'super'; - -// $member = [ -// 'mb_id' => $loginId, -// 'mb_name' => $_SESSION['user']['name'] ?? '관리자', -// 'mb_email' => $_SESSION['user']['email'] ?? '', -// 'mb_level' => 10, -// 'mb_nick' => '관리자', -// ]; -// } -// descope_session.php - -// 1) 세션 시작 -if (session_status() === PHP_SESSION_NONE) { - session_start(); -} - -if (empty($_SESSION['user']['userId']) && !empty($_COOKIE['descope_login_id'])) { - $customAttributes = json_decode(base64_decode($_COOKIE['descope_custom_attributes'] ?? ''), true); - $roleNames = json_decode(base64_decode($_COOKIE['descope_role_names'] ?? ''), true); - $loginIdFromCookie = trim((string)($_COOKIE['descope_login_id'] ?? '')); - - $_SESSION['user'] = [ - 'userId' => trim((string)($_COOKIE['descope_user_id'] ?? $loginIdFromCookie)), - 'loginIds' => $loginIdFromCookie !== '' ? [$loginIdFromCookie] : [], - 'name' => trim((string)($_COOKIE['descope_user_name'] ?? '')), - 'email' => trim((string)($_COOKIE['descope_user_email'] ?? '')), - 'phone' => trim((string)($_COOKIE['descope_user_phone'] ?? '')), - 'customAttributes' => is_array($customAttributes) ? $customAttributes : [], - 'roleNames' => is_array($roleNames) ? $roleNames : [], - ]; -} - -// 2) 로그인 검사 (비로그인 차단) -// if (empty($_SESSION['user']['userId'])) { -// // 세션 없으면 바로 차단 -// session_destroy(); -// echo ""; -// exit; -// } - -// 3) 로그인 정보 추출 -$loginIds = $_SESSION['user']['loginIds'] ?? []; -$loginId = (is_array($loginIds) && !empty($loginIds)) ? $loginIds[0] : ''; - -$userName = $_SESSION['user']['name'] ?? ''; -$userEmail = $_SESSION['user']['email'] ?? ''; - -// 4) 관리자/일반 사용자 구분 -$ADMIN_EMAILS = [ - 'kjy0426@hanmaceng.co.kr', - 'b24014@hanmaceng.co.kr', - 'b23065@hanmaceng.co.kr', - 'shyeom1@samaneng.com', - 'cjy627@hanmaceng.co.kr', - 'b23072@hanmaceng.co.kr', - 'cozyjin@hanmaceng.co.kr', - 'm24031@hanmaceng.co.kr', - 'b24051@hanmaceng.co.kr', - 'rmsgud1202@hanmaceng.co.kr', - 'm21318@hanmaceng.co.kr', - 'b25023@hanmaceng.co.kr', - 'sdi9429@naver.com', - 'junsuy@hanmail.net', - 'ilphilo92@gmail.com' -]; -// $isAdmin = in_array($loginId, $ADMIN_EMAILS, true); -$isAdmin = $loginId && in_array($loginId, $ADMIN_EMAILS, true); - -// ✅ 그누보드 전역 세션 동기화 -if ($loginId) { - $_SESSION['ss_mb_id'] = $loginId; - $_SESSION['ss_mb_level'] = $isAdmin ? 10 : 2; - - $is_member = true; - $is_admin = $isAdmin ? 'super' : ''; - - $member = [ - 'mb_id' => $loginId, - 'mb_name' => $userName ?: $loginId, - 'mb_email' => $userEmail, - 'mb_level' => $_SESSION['ss_mb_level'], - 'mb_nick' => $userName ?: $loginId, - ]; -} else { - // ✅ 비회원: 로그인 세션 없음 - $is_member = false; - $is_admin = ''; - $member = []; -} +$loginId = $authState['loginId']; +$isAdmin = $authState['isAdmin']; +$is_member = $authState['is_member']; +$is_admin = $authState['is_admin']; +$member = $authState['member']; ?> \ No newline at end of file diff --git a/egbim/skin/member/basic/descope_user_search.php b/egbim/skin/member/basic/descope_user_search.php index 3854cd9..be63d54 100644 --- a/egbim/skin/member/basic/descope_user_search.php +++ b/egbim/skin/member/basic/descope_user_search.php @@ -1,66 +1,38 @@ $loginId, -// "limit" => 1 -// ]); +$loginId = trim((string) ($_POST['loginId'] ?? '')); +$sessionUser = $_SESSION['user'] ?? []; +$sessionLoginId = trim((string) ($sessionUser['loginIds'][0] ?? '')); -// $headers = [ -// "Authorization: Bearer $management_key", -// "Content-Type: application/json" -// ]; +if ($loginId !== '' && $sessionLoginId !== '' && strcasecmp($loginId, $sessionLoginId) === 0) { + echo json_encode(['status' => 'ok', 'user' => auth_normalize_user($sessionUser)]); + exit; +} -// $ch = curl_init($url); -// curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -// curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); -// curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); -// $response = curl_exec($ch); -// $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); -// curl_close($ch); - -// if ($http_code === 200) { -// $res = json_decode($response, true); - -// if (!empty($res['users'][0])) { -// // ✅ 먼저 $user에 복사 -// $user = $res['users'][0]; - -// // ✅ 값이 없으면 테스트용 기본값 삽입 -// $user['name'] = $user['name'] ?: '홍길동'; -// $user['phone'] = $user['phone'] ?: '01012345678'; -// $user['customAttributes']['company'] = $user['customAttributes']['company'] ?? '한맥ENG'; -// $user['customAttributes']['team'] = $user['customAttributes']['team'] ?? '설계팀'; -// $user['customAttributes']['employeeId'] = $user['customAttributes']['employeeId'] ?? 'HM2024'; - -// // ✅ 수정된 $user 사용 -// echo json_encode(['status' => 'ok', 'user' => $user]); -// } else { -// echo json_encode(['status' => 'fail', 'message' => '사용자를 찾을 수 없음']); -// } -// } else { -// echo json_encode(['status' => 'fail', 'message' => 'Descope API 오류', 'code' => $http_code]); -// } -?> - 'ok', 'user' => $cookieUser]); + exit; + } +} $management_key = 'P2wON5fy1K6kyia269VpeIzYP8oP:K32l5ORmzy32OvaaPvpdZsMY3JmKQb7a3vvrl10PgjlJUGk3K7EssMH3uW5VGQSbrgtEdPj'; -$loginId = $_POST['loginId'] ?? ''; -$url = "https://api.descope.com/v2/mgmt/user/search"; +$url = 'https://api.descope.com/v2/mgmt/user/search'; $payload = json_encode([ - "loginId" => $loginId, - "limit" => 1 + 'loginId' => $loginId, + 'limit' => 1, ]); $headers = [ "Authorization: Bearer $management_key", - "Content-Type: application/json" + 'Content-Type: application/json', ]; $ch = curl_init($url); @@ -76,18 +48,17 @@ if ($http_code === 200) { if (!empty($res['users'][0])) { $user = $res['users'][0]; - - // ✅ 실제 값이 있으면 그대로 사용, 없으면 빈 문자열 $user['name'] = $user['name'] ?? ''; $user['phone'] = $user['phone'] ?? ''; $user['customAttributes']['company'] = $user['customAttributes']['company'] ?? ''; + $user['customAttributes']['familyCompany'] = $user['customAttributes']['familyCompany'] ?? ''; $user['customAttributes']['team'] = $user['customAttributes']['team'] ?? ''; $user['customAttributes']['employeeId'] = $user['customAttributes']['employeeId'] ?? ''; + $user['customAttributes']['familyUniqueKey'] = $user['customAttributes']['familyUniqueKey'] ?? ''; echo json_encode(['status' => 'ok', 'user' => $user]); - } else { - echo json_encode(['status' => 'fail', 'message' => '사용자를 찾을 수 없음']); + exit; } -} else { - echo json_encode(['status' => 'fail', 'message' => 'Descope API 오류', 'code' => $http_code]); } + +echo json_encode(['status' => 'fail', 'message' => '사용자를 찾을 수 없음', 'code' => $http_code]); diff --git a/egbim/skin/member/basic/login.skin.php b/egbim/skin/member/basic/login.skin.php index 7fe0c0e..b5b7b82 100644 --- a/egbim/skin/member/basic/login.skin.php +++ b/egbim/skin/member/basic/login.skin.php @@ -52,8 +52,12 @@
+ +

eg-bim 1.5.1 버전 이상 사용자는 baron-sso로 로그인해야 합니다.

@@ -127,6 +131,10 @@ $(document).ready(function () { e.preventDefault(); // 기본 제출 막기 $('#btn_login').click(); // 버튼 클릭 이벤트 재사용 }); + + $('.signup-disabled-link').on('click', function (e) { + e.preventDefault(); + }); }); document.addEventListener("DOMContentLoaded", () => { diff --git a/egbim/skin/member/basic/main_header.skin.php b/egbim/skin/member/basic/main_header.skin.php index 3474945..24462be 100644 --- a/egbim/skin/member/basic/main_header.skin.php +++ b/egbim/skin/member/basic/main_header.skin.php @@ -55,7 +55,7 @@ $download_url = "https://release-download.hmac.kr/eg-bim/{$version}/setup_eg-bim 로그아웃
  • - 회원가입 +