Metadata-Version: 2.4
Name: pxa-auth
Version: 0.1.0
Summary: PXA 인증 처리 (Opaque Token + Redis + HttpOnly 쿠키 + 사용자 DB) — 단독 사용 가능
Author: Platform Team
License-Expression: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi==0.128.*
Requires-Dist: redis>=5.0
Provides-Extra: userdb
Requires-Dist: SQLAlchemy<2.1,>=2.0; extra == "userdb"
Provides-Extra: postgresql
Requires-Dist: psycopg[binary]>=3.1; extra == "postgresql"
Provides-Extra: mariadb
Requires-Dist: PyMySQL>=1.1; extra == "mariadb"
Provides-Extra: odbc
Requires-Dist: pyodbc>=5.0; extra == "odbc"
Provides-Extra: common
Requires-Dist: pxa-common>=0.1.0; extra == "common"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Requires-Dist: fakeredis>=2.21; extra == "dev"
Requires-Dist: SQLAlchemy<2.1,>=2.0; extra == "dev"
Dynamic: license-file

# pxa-auth

PXA **인증 처리** 패키지. Opaque Token(무작위 문자열) + **Redis** + **HttpOnly 쿠키** 방식입니다.

> **단독 사용 가능** — 다른 pxa 패키지에 의존하지 않습니다. 의존성은 `fastapi`, `redis` 뿐입니다.
> 설정 관리와 응답 포맷은 **개발자의 몫**이고, pxa-auth 는 그 두 가지만 주입받아 동작합니다.

---

## 설치

```bash
pip install pxa-auth
pip install "pxa-auth[common]"    # pxa-common 과 함께 쓸 때(어댑터)
```

## 1. 최소 사용 예 (단독)

개발자가 할 일은 **① 설정 채우기 ② 로그인 검증 콜백 구현** 둘 뿐입니다.
사용자 테이블이 표준 형태라면 ②도 필요 없습니다 — [7장](#7-사용자-저장소--db-는-설정으로-교체된다) 참고.

```python
from fastapi import Depends, FastAPI
from pxa_auth import (AuthSettings, init_auth, build_auth_router,
                      current_user, register_auth_error_handler)

# ① 설정 — 값의 출처(YAML/환경변수/DB)는 개발자가 알아서 정한다
init_auth(AuthSettings(
    cookie_secure=False,            # 로컬 개발(HTTP)
    session_ttl_seconds=1800,
    redis_host="localhost",
))

# ② 로그인 검증 콜백 — 도메인 로직
def authenticate(username: str, password: str):
    user = find_user(username)                     # 개발자의 DB 조회
    if user and verify_password(password, user.password_hash):
        return {"username": user.username, "roles": ["user"]}
    return None                                    # 실패 -> pxa-20003

app = FastAPI()
register_auth_error_handler(app)                   # 인증 예외 -> 응답 변환
app.include_router(build_auth_router(authenticate))

@app.get("/orders")
def orders(user: dict = Depends(current_user)):    # 인증 강제
    ...
```

기본 제공 엔드포인트: `POST /auth/login`, `POST /auth/logout`, `GET /auth/me`

## 2. 설정 (`AuthSettings`)

pxa-auth 는 **설정 파일을 읽지 않습니다.** 개발자가 자신의 설정 체계에서 값을 꺼내 넣어 주면 됩니다.

| 항목 | 기본값 | 설명 |
|---|---|---|
| `cookie_name` | `PXA_SESSION` | 세션 쿠키 이름 |
| `cookie_secure` | `True` | 운영(HTTPS)은 True |
| `cookie_samesite` / `cookie_domain` / `cookie_path` | `lax` / `None` / `/` | 쿠키 속성 |
| `session_ttl_seconds` | `3600` | 세션 TTL(초) |
| `token_bytes` | `32` | opaque token 길이 |
| `key_prefix` | `pxa:session:` | Redis key 접두어 |
| `redis_url` / `redis_host` / `redis_port` / `redis_db` / `redis_password` | – | Redis 접속 |
| `messages` | `{}` | 기본 메시지 재정의 |

```python
cfg = my_yaml_loader("config.yaml")               # 개발자의 config 구현
init_auth(AuthSettings(redis_host=cfg["redis"]["host"]))

init_auth(settings, redis_client=my_redis)        # Redis 클라이언트 직접 주입도 가능
```

## 3. 응답 포맷 (`Responder` 주입)

응답 규격을 강제하지 않습니다. 기본은 최소 dict 이고, 원하는 포맷의 Responder 를 주입하면 됩니다.

```python
# 기본(DictResponder)
{"success": true, "code": "pxa-10000", "message": "로그인 되었습니다.", "result": {...}}
```

```python
class MyResponder:
    def ok(self, result=None, message=None):
        return {"status": "OK", "data": result, "msg": message}
    def fail(self, code, message, result=None):
        return {"status": "ERROR", "reason": code, "msg": message}

init_auth(settings, responder=MyResponder())
```

## 4. pxa-common 과 함께 쓰기 (선택)

어댑터를 **명시적으로 한 줄** 호출하면 표준 envelope(`ApiResponse`)와 `AppCode` 로 응답합니다. 자동 감지 같은 암묵적 동작은 없습니다.

```python
from pxa_auth.integrations.pxa_common import use_pxa_common

init_auth(settings)
use_pxa_common(app)        # 응답 포맷 전환 + AuthError -> 표준 에러응답 핸들러 등록
```

`pxa-20003` 등 코드 문자열은 `AppCode` 로 매핑되고, 프론트가 받는 응답 형태는 다른 pxa 서비스와 동일해집니다.

## 5. 동작 방식

1. 로그인 성공 → 무작위 opaque token 생성 → **Redis 에 세션 저장**(TTL)
2. 토큰을 **HttpOnly 쿠키**로 `Set-Cookie` 헤더에 실어 전달 (JS 접근 차단)
3. 이후 요청은 쿠키의 토큰을 Redis 에서 조회해 검증하며, 접근 시 TTL 갱신(sliding)
4. 로그아웃 → Redis 세션 폐기 + 쿠키 삭제

토큰은 의미 없는 무작위 문자열이라 **본문에 노출되지 않고**, 사용자 정보도 담기지 않습니다.

## 6. 비밀번호 해시

```python
from pxa_auth import hash_password, verify_password

user.password_hash = hash_password("평문")
verify_password("평문", user.password_hash)     # True
```

표준 라이브러리 PBKDF2-HMAC-SHA256(200k iterations, salt 포함)을 사용합니다.

## 7. 사용자 저장소 — DB 는 설정으로 교체된다

사용자 정보는 **DB 에 저장**하며 기본 DB 는 **PostgreSQL** 입니다. `UserDbSettings.dialect` 만 바꾸면 MariaDB 등으로 교체되고, 사용자 코드는 그대로입니다. 이 설정은 공통기능이 제공하는 값이므로 주니어 개발자가 바꾸지 않습니다.

```python
from pxa_auth import AuthSettings, UserDbSettings, build_auth_router, init_auth

init_auth(
    AuthSettings(redis_host="localhost"),
    user_db=UserDbSettings(                 # 기본 postgresql
        host="db", user="app", password=..., database="appdb",
    ),
)

app.include_router(build_auth_router())     # authenticator 를 넘기지 않으면 DB 인증
```

```bash
pip install "pxa-auth[userdb,postgresql]"   # 기본
pip install "pxa-auth[userdb,mariadb]"      # dialect="mariadb"
pip install "pxa-auth[userdb,odbc]"         # connector="odbc"
```

| 설정 | 기본값 | 설명 |
|---|---|---|
| `dialect` | `postgresql` | `postgresql` / `mariadb` / `mysql` / `mssql` |
| `connector` | `native` | `native` / `odbc` |
| `table` | `pxa_user` | 사용자 테이블 |
| `username_column` / `password_column` | `username` / `password_hash` | 컬럼 매핑 |
| `active_column` | `is_active` | 비활성 계정 차단 (`None` 이면 검사 안 함) |
| `session_columns` | `("id","username","roles")` | 세션에 담을 컬럼 |

기대하는 테이블 형태:

```sql
CREATE TABLE pxa_user (
  id            INT PRIMARY KEY,
  username      VARCHAR(50)  NOT NULL UNIQUE,
  roles         VARCHAR(100),
  password_hash VARCHAR(255) NOT NULL,   -- hash_password() 결과
  is_active     BOOLEAN      NOT NULL DEFAULT TRUE
);
```

보장되는 동작:

- 조회는 항상 **바인드 파라미터**로 실행되고, 테이블/컬럼명은 방언에 맞게 인용됩니다(SQL 인젝션 차단).
- **비밀번호 해시는 세션·응답에 절대 담기지 않습니다.**
- 사용자가 없을 때도 해시 검증을 수행해 **응답시간으로 계정 존재 여부가 새지 않습니다.**
- 비활성 계정(`is_active=false`)은 로그인할 수 없습니다.

DB 가 아닌 LDAP/SSO 를 쓰려면 `find_by_username(username) -> dict | None` 하나만 구현해 `init_auth(user_store=...)` 로 주입하면 됩니다. `authenticator` 콜백을 직접 넘기는 기존 방식도 그대로 동작합니다.

## 8. 테스트 & 배포

pytest 는 **외부 서버 없이** fakeredis 와 인메모리 사용자 저장소로, 다른 pxa 패키지 없이 통과합니다.

```bash
pip install -e ".[dev]"
pytest
```

실 서버 연동은 pytest 와 분리된 검증 스크립트로 확인합니다.

```bash
python scripts/verify_redis_session.py   # 실 Redis: 로그인→쿠키→보호API→로그아웃
python scripts/verify_user_db.py         # 실 MariaDB: dialect/connector 교체 검증
```

```bash
python -m build && twine upload -r nexus dist/*
```
