Metadata-Version: 2.4
Name: auth_xjtu
Version: 0.3.0
Summary: Automated XJTU unified identity authentication for Python
Author-email: Rouge Lin <rougeLin3877@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/rouge3877/Auth.xjtu
Project-URL: Issues, https://github.com/rouge3877/Auth.xjtu/issues
Keywords: xjtu,authentication,cas,sso,python
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.1
Requires-Dist: pycryptodome>=3.15.0
Requires-Dist: beautifulsoup4>=4.13.4
Provides-Extra: dev
Requires-Dist: pytest<9,>=8.4; extra == "dev"
Dynamic: license-file

# Auth.xjtu

`auth-xjtu` automates authentication for services protected by Xi'an
Jiaotong University's unified identity gateway.

The package deliberately separates two features:

1. **Entry discovery** finds the authentication entry for pages that expose a
   login link without redirecting automatically.
2. **Login** starts from a redirecting business URL or a known authentication
   entry and completes the OAuth/CAS chain.

This separation keeps the authentication flow predictable while allowing
site-specific discovery rules to evolve independently.

## Requirements

- Python 3.9+
- pip

## Installation

Install the published package:

```bash
python -m pip install auth-xjtu
```

Install a development checkout with test dependencies:

```bash
python -m pip install -e ".[dev]"
```

## Login from a business URL

`Authenticator.login()` follows the application's redirects to the XJTU
gateway, performs the current MFA preflight, submits the password form, and
follows the authenticated redirect back to the application:

```python
from auth_xjtu import Authenticator, LoginStatus

auth = Authenticator(
    username="your_netid",
    password="your_password",
)

status, message = auth.login("https://gmis.xjtu.edu.cn/pyxx/")
if status is LoginStatus.SUCCESS:
    session = auth.get_session()
    print("Login successful")
else:
    print(f"Login failed: {message}")
```

The argument may also be a known authentication entry URL. `login()` handles
HTTP redirects but does not scan arbitrary application HTML for login links.

## Discover an entry, then log in

When an application page does not redirect and only exposes a login link, use
an `EntryFinder`. Sharing the same session preserves cookies collected during
discovery:

```python
from auth_xjtu import Authenticator, EntryFinder, LoginStatus

app_url = "https://portal.example.edu/application"
auth = Authenticator("your_netid", "your_password")

finder = EntryFinder(session=auth.get_session())
entry_url = finder.find(app_url)
status, message = auth.login(entry_url)

if status is LoginStatus.SUCCESS:
    response = auth.get_session().get(app_url, timeout=10)
    response.raise_for_status()
```

For discovery without a shared session, the convenience function
`find_entry(app_url)` is also available.

## Entry discovery strategies

`EntryFinder` resolves an entry in this order:

1. a hard-coded URL in a matching `SiteRule`;
2. a redirect to a different host or a login-like URL;
3. CSS selectors configured for the matching site;
4. generic password-form and scored login-link detection.

### Per-site configuration

A site can use a hard-coded path:

```python
from auth_xjtu import EntryFinder, SiteRule

rules = (
    SiteRule(
        hostname="portal.example.edu",
        fixed_entry_url="/sso/start",
    ),
)

entry_url = EntryFinder(rules=rules).find(
    "https://portal.example.edu/application"
)
```

Or a page-specific CSS selector:

```python
rules = (
    SiteRule(
        hostname="library.example.edu",
        selectors=(
            "a.campus-sso",
            "form#unified-login",
        ),
        keywords=("campus access",),
    ),
)
```

Rules passed to `EntryFinder` replace the built-in rule set. Built-in XJTU
rules live in `auth_xjtu/entry_discovery/rules.py`; this is the intended place
for small, reviewed site-specific hard-coded entries.

If no generic strategy or rule succeeds, discovery raises
`EntryDiscoveryError` instead of silently treating the application URL as a
login entry.

## Login status codes

`Authenticator.login()` returns `(LoginStatus, message)`. `LoginStatus` is an
`IntEnum`, so it remains compatible with existing integer comparisons.

| Value | Enum | Meaning |
|---:|---|---|
| 0 | `SUCCESS` | The session is authenticated. |
| 1 | `ENTRY_REDIRECT_FAILED` | The entry URL could not reach the login page. |
| 2 | `PAYLOAD_CONSTRUCTION_FAILED` | Tokens, public key, or encrypted payload could not be created. |
| 3 | `POST_LOGIN_REDIRECT_FAILED` | The redirect after credential submission failed. |
| 4 | `LOGIN_SUBMISSION_FAILED` | Submission or credential preflight was rejected. |
| 5 | `MFA_REQUIRED` | The account requires an interactive second factor. |

## Project layout

```text
src/auth_xjtu/
├── entry_discovery/
│   ├── finder.py       # generic discovery strategies
│   └── rules.py        # per-site configuration and hard-coded entries
├── login/
│   ├── authenticator.py
│   ├── payload.py
│   └── redirects.py
├── authenticator.py    # backward-compatible import
├── exceptions.py
└── __init__.py

tests/
├── unit/               # isolated tests without network or credentials
└── integration/        # live XJTU tests

examples/
└── course_selection/   # read-only GMIS course-list query
```

## Examples

The [GMIS course-list example](examples/course_selection/README.md) logs in
from the GMIS application URL, queries every result page with a current
cache-busting timestamp, and can filter results by course name. It is
read-only and does not submit course selections.

## Migrating from 0.2.x

Version 0.3.0 contains intentional breaking changes:

- `Authenticator.login()` now takes `start_url`; replace keyword calls such as
  `login(dest_app_url=...)` with `login(start_url=...)`.
- Imports from the undocumented `auth_xjtu.core` modules are no longer
  supported. Use `Authenticator`, `EntryFinder`, `SiteRule`, and `find_entry`
  from the top-level package.
- The first value returned by `login()` is now `LoginStatus`. It is an
  `IntEnum`, so existing comparisons such as `status == 0` continue to work.
- Entry discovery and login are separate operations. Use `EntryFinder` only
  when a page exposes a login link without redirecting automatically.

## Testing

Run the complete local suite:

```bash
python -m pytest
```

Live integration cases skip automatically unless both credentials are
available. To run them in PowerShell:

```powershell
$env:TEST_USERNAME = "your_netid"
$env:TEST_PASSWORD = "your_password"
python -m pytest -m integration
```

Run only isolated tests:

```bash
python -m pytest -m "not integration"
```

## License and disclaimer

This project is licensed under the MIT License.

It is an independent project and is not affiliated with, authorized by, or
endorsed by Xi'an Jiaotong University. Use it responsibly and do not commit
credentials, cookies, or authentication logs to source control.
