corclient 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- corclient-0.1.0.dist-info/METADATA +393 -0
- corclient-0.1.0.dist-info/RECORD +12 -0
- corclient-0.1.0.dist-info/WHEEL +5 -0
- corclient-0.1.0.dist-info/entry_points.txt +2 -0
- corclient-0.1.0.dist-info/licenses/LICENSE +0 -0
- corclient-0.1.0.dist-info/top_level.txt +1 -0
- corecli/__init__.py +5 -0
- corecli/_version.py +9 -0
- corecli/auth.py +440 -0
- corecli/cli.py +43 -0
- corecli/pkce.py +14 -0
- corecli/utils.py +42 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: corclient
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Internal CLI tool for microservices management, infrastructure administration and monitoring with AWS Cognito authentication
|
|
5
|
+
Author: Carlos Ferrer
|
|
6
|
+
Author-email: Carlos Ferrer <cferrer@projectcor.com>
|
|
7
|
+
Maintainer-email: Carlos Ferrer <cferrer@projectcor.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/ProjectCORTeam/corcli
|
|
10
|
+
Project-URL: Documentation, https://github.com/ProjectCORTeam/corcli#readme
|
|
11
|
+
Project-URL: Repository, https://github.com/ProjectCORTeam/corcli.git
|
|
12
|
+
Project-URL: Bug Tracker, https://github.com/ProjectCORTeam/corcli/issues
|
|
13
|
+
Project-URL: Changelog, https://github.com/ProjectCORTeam/corcli/releases
|
|
14
|
+
Keywords: cli,microservices,infrastructure,devops,monitoring,aws,internal-tools
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: Intended Audience :: Information Technology
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Classifier: Environment :: Console
|
|
32
|
+
Classifier: Natural Language :: English
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: typer>=0.12
|
|
37
|
+
Requires-Dist: rich>=13
|
|
38
|
+
Requires-Dist: flask>=2.3
|
|
39
|
+
Requires-Dist: requests>=2.31
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
42
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: types-requests>=2.31.0; extra == "dev"
|
|
46
|
+
Dynamic: author
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
Dynamic: requires-python
|
|
49
|
+
|
|
50
|
+
# CoreCLI
|
|
51
|
+
|
|
52
|
+
[](https://pypi.org/project/corclient/)
|
|
53
|
+
[](https://pypi.org/project/corclient/)
|
|
54
|
+
[](https://opensource.org/licenses/MIT)
|
|
55
|
+
|
|
56
|
+
Internal CLI tool for microservices management, infrastructure administration and monitoring with AWS Cognito authentication.
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
- 🔐 **AWS Cognito Authentication** - Secure authentication using PKCE (Proof Key for Code Exchange) flow
|
|
61
|
+
- 🚀 **Microservices Management** - Interact with internal microservices infrastructure
|
|
62
|
+
- 📊 **Basic Monitoring** - Monitor and manage microservices health
|
|
63
|
+
- 🔄 **Token Management** - Automatic token refresh and session handling
|
|
64
|
+
- 💻 **CLI-First Design** - Built with modern CLI best practices using Typer
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
- Python 3.9 or higher
|
|
69
|
+
- AWS Cognito user pool configured with Hosted UI
|
|
70
|
+
- Valid AWS Cognito credentials
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
### Via pip (Recommended)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install corclient
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Via Homebrew (macOS/Linux)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Add the tap
|
|
84
|
+
brew tap ProjectCORTeam/corcli https://github.com/ProjectCORTeam/corcli.git
|
|
85
|
+
|
|
86
|
+
# Install
|
|
87
|
+
brew install corclient
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
To update:
|
|
91
|
+
```bash
|
|
92
|
+
brew update && brew upgrade corclient
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Development Installation
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/ProjectCORTeam/corcli.git
|
|
99
|
+
cd corcli
|
|
100
|
+
pip install -e .
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Quick Start
|
|
104
|
+
|
|
105
|
+
### 1. Login
|
|
106
|
+
|
|
107
|
+
Authenticate with AWS Cognito using the Hosted UI:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cor login
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This will:
|
|
114
|
+
- Open your browser to the Cognito Hosted UI
|
|
115
|
+
- Start a local callback server
|
|
116
|
+
- Complete the PKCE flow
|
|
117
|
+
- Store tokens securely
|
|
118
|
+
|
|
119
|
+
#### Custom Redirect URI
|
|
120
|
+
|
|
121
|
+
If you need to redirect to a custom app scheme:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cor login --app-redirect myapp://callback
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 2. Check Authentication Status
|
|
128
|
+
|
|
129
|
+
View your current authentication details:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cor whoami
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This displays:
|
|
136
|
+
- User information from the ID token
|
|
137
|
+
- Token expiration time
|
|
138
|
+
- Active session details
|
|
139
|
+
|
|
140
|
+
### 3. Refresh Tokens
|
|
141
|
+
|
|
142
|
+
Manually refresh your access tokens:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
cor refresh
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The CLI automatically refreshes tokens when needed, but you can manually trigger a refresh if required.
|
|
149
|
+
|
|
150
|
+
### 4. Logout
|
|
151
|
+
|
|
152
|
+
Clear local session and revoke tokens:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
cor logout
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This will:
|
|
159
|
+
- Revoke active tokens with Cognito
|
|
160
|
+
- Clear local token storage
|
|
161
|
+
- End your authentication session
|
|
162
|
+
|
|
163
|
+
## Configuration
|
|
164
|
+
|
|
165
|
+
CoreCLI requires the following environment variables or configuration:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# AWS Cognito Configuration
|
|
169
|
+
COGNITO_DOMAIN=your-domain.auth.region.amazoncognito.com
|
|
170
|
+
COGNITO_CLIENT_ID=your-client-id
|
|
171
|
+
COGNITO_REDIRECT_URI=http://localhost:8080/callback
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Configuration can be set via:
|
|
175
|
+
- Environment variables
|
|
176
|
+
- Configuration file (`.corecli/config`)
|
|
177
|
+
- Command-line arguments
|
|
178
|
+
|
|
179
|
+
## Usage Examples
|
|
180
|
+
|
|
181
|
+
### Basic Authentication Flow
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Login to Cognito
|
|
185
|
+
cor login
|
|
186
|
+
|
|
187
|
+
# Check who is logged in
|
|
188
|
+
cor whoami
|
|
189
|
+
|
|
190
|
+
# When done, logout
|
|
191
|
+
cor logout
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Using with Custom Applications
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Login and redirect to custom app
|
|
198
|
+
cor login --app-redirect myapp://authenticated
|
|
199
|
+
|
|
200
|
+
# The CLI will handle authentication and redirect to your app
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Command Reference
|
|
204
|
+
|
|
205
|
+
| Command | Description | Options |
|
|
206
|
+
|---------|-------------|---------|
|
|
207
|
+
| `cor login` | Authenticate with Cognito | `--app-redirect, -r` - Custom redirect URI |
|
|
208
|
+
| `cor whoami` | Show authenticated user info | None |
|
|
209
|
+
| `cor refresh` | Refresh authentication tokens | None |
|
|
210
|
+
| `cor logout` | End session and revoke tokens | None |
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
### Setup Development Environment
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Clone repository
|
|
218
|
+
git clone https://github.com/ProjectCORTeam/corcli.git
|
|
219
|
+
cd corcli
|
|
220
|
+
|
|
221
|
+
# Install in development mode with dev dependencies
|
|
222
|
+
pip install -e ".[dev]"
|
|
223
|
+
|
|
224
|
+
# Install pre-commit hooks (optional but recommended)
|
|
225
|
+
pip install pre-commit
|
|
226
|
+
pre-commit install
|
|
227
|
+
|
|
228
|
+
# Run the CLI
|
|
229
|
+
cor --help
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Development Tools
|
|
233
|
+
|
|
234
|
+
The project uses modern Python development tools:
|
|
235
|
+
|
|
236
|
+
- **Ruff** - Fast Python linter (replaces flake8, isort, and more)
|
|
237
|
+
- **Black** - Code formatter
|
|
238
|
+
- **MyPy** - Static type checker
|
|
239
|
+
- **Pre-commit** - Git hooks for automated checks
|
|
240
|
+
|
|
241
|
+
### Running Checks Locally
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# Run linting
|
|
245
|
+
make lint
|
|
246
|
+
|
|
247
|
+
# Fix linting issues automatically
|
|
248
|
+
make lint-fix
|
|
249
|
+
|
|
250
|
+
# Format code
|
|
251
|
+
make format
|
|
252
|
+
|
|
253
|
+
# Check formatting (without modifying)
|
|
254
|
+
make format-check
|
|
255
|
+
|
|
256
|
+
# Run type checking
|
|
257
|
+
make type-check
|
|
258
|
+
|
|
259
|
+
# Build and verify package
|
|
260
|
+
make build
|
|
261
|
+
|
|
262
|
+
# Run all checks
|
|
263
|
+
make all
|
|
264
|
+
|
|
265
|
+
# Clean build artifacts
|
|
266
|
+
make clean
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Or manually:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Linting
|
|
273
|
+
ruff check corecli/
|
|
274
|
+
|
|
275
|
+
# Formatting
|
|
276
|
+
black corecli/
|
|
277
|
+
|
|
278
|
+
# Type checking
|
|
279
|
+
mypy corecli/
|
|
280
|
+
|
|
281
|
+
# Build
|
|
282
|
+
python -m build
|
|
283
|
+
twine check dist/*
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Project Structure
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
corecli/
|
|
290
|
+
├── corecli/
|
|
291
|
+
│ ├── __init__.py # Package initialization
|
|
292
|
+
│ ├── cli.py # CLI commands and interface
|
|
293
|
+
│ ├── auth.py # Authentication logic
|
|
294
|
+
│ ├── pkce.py # PKCE flow implementation
|
|
295
|
+
│ └── utils.py # Utility functions
|
|
296
|
+
├── pyproject.toml # Project metadata
|
|
297
|
+
├── setup.py # Package setup
|
|
298
|
+
└── README.md # This file
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Security Considerations
|
|
302
|
+
|
|
303
|
+
- **PKCE Flow**: Uses the industry-standard PKCE flow for secure OAuth 2.0 authentication
|
|
304
|
+
- **Token Storage**: Tokens are stored securely in the local user directory
|
|
305
|
+
- **Automatic Refresh**: Access tokens are automatically refreshed before expiration
|
|
306
|
+
- **Session Management**: Proper logout flow revokes tokens server-side
|
|
307
|
+
|
|
308
|
+
## Troubleshooting
|
|
309
|
+
|
|
310
|
+
### Authentication Fails
|
|
311
|
+
|
|
312
|
+
If authentication fails, check:
|
|
313
|
+
1. Cognito domain and client ID are correct
|
|
314
|
+
2. Redirect URI is whitelisted in Cognito
|
|
315
|
+
3. User pool has Hosted UI enabled
|
|
316
|
+
4. Network connectivity to Cognito endpoints
|
|
317
|
+
|
|
318
|
+
### Token Refresh Issues
|
|
319
|
+
|
|
320
|
+
If token refresh fails:
|
|
321
|
+
1. Check that refresh token hasn't expired
|
|
322
|
+
2. Verify Cognito client settings allow refresh tokens
|
|
323
|
+
3. Try logging out and logging in again
|
|
324
|
+
|
|
325
|
+
### Port Already in Use
|
|
326
|
+
|
|
327
|
+
If the callback server can't start:
|
|
328
|
+
```bash
|
|
329
|
+
# The default port (8080) might be in use
|
|
330
|
+
# Kill the process or configure a different port
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Contributing
|
|
334
|
+
|
|
335
|
+
This is an internal tool for ProjectCOR Team. If you're a team member:
|
|
336
|
+
|
|
337
|
+
1. Fork the repository
|
|
338
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
339
|
+
3. Make your changes and ensure they pass all checks:
|
|
340
|
+
```bash
|
|
341
|
+
make lint
|
|
342
|
+
make format
|
|
343
|
+
make type-check
|
|
344
|
+
make build
|
|
345
|
+
```
|
|
346
|
+
4. Commit your changes using [Conventional Commits](https://www.conventionalcommits.org/):
|
|
347
|
+
```bash
|
|
348
|
+
git commit -m 'feat: add amazing feature'
|
|
349
|
+
git commit -m 'fix: resolve authentication issue'
|
|
350
|
+
git commit -m 'docs: update README'
|
|
351
|
+
```
|
|
352
|
+
5. Push to the branch (`git push origin feature/amazing-feature`)
|
|
353
|
+
6. Open a Pull Request
|
|
354
|
+
|
|
355
|
+
### Quality Checks
|
|
356
|
+
|
|
357
|
+
All pull requests must pass:
|
|
358
|
+
- ✅ **Ruff linting** - Code quality and style checks
|
|
359
|
+
- ✅ **Black formatting** - Consistent code formatting
|
|
360
|
+
- ✅ **MyPy type checking** - Static type validation (warnings allowed)
|
|
361
|
+
- ✅ **Package build** - Successful package creation
|
|
362
|
+
- ✅ **Twine validation** - PyPI metadata verification
|
|
363
|
+
|
|
364
|
+
These checks run automatically on every release.
|
|
365
|
+
|
|
366
|
+
## Versioning
|
|
367
|
+
|
|
368
|
+
We use [Semantic Versioning](https://semver.org/) via semantic-release. Versions are automatically generated based on commit messages following [Conventional Commits](https://www.conventionalcommits.org/).
|
|
369
|
+
|
|
370
|
+
### Commit Message Format
|
|
371
|
+
|
|
372
|
+
- `feat:` - New feature (minor version bump)
|
|
373
|
+
- `fix:` - Bug fix (patch version bump)
|
|
374
|
+
- `feat!:` or `BREAKING CHANGE:` - Breaking change (major version bump)
|
|
375
|
+
|
|
376
|
+
## License
|
|
377
|
+
|
|
378
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
379
|
+
|
|
380
|
+
## Support
|
|
381
|
+
|
|
382
|
+
For internal support:
|
|
383
|
+
- Report issues: [GitHub Issues](https://github.com/ProjectCORTeam/corcli/issues)
|
|
384
|
+
- Documentation: [GitHub Wiki](https://github.com/ProjectCORTeam/corcli#readme)
|
|
385
|
+
- Releases: [GitHub Releases](https://github.com/ProjectCORTeam/corcli/releases)
|
|
386
|
+
|
|
387
|
+
## Authors
|
|
388
|
+
|
|
389
|
+
- **Carlos Ferrer** - *Initial work* - [ProjectCOR Team](https://github.com/ProjectCORTeam)
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
Built with ❤️ by the ProjectCOR Team
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
corclient-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
corecli/__init__.py,sha256=YahZFfcGjK3dKwjCDZ9v5ZZ9R6-G3Nr28_3djZUh2kY,138
|
|
3
|
+
corecli/_version.py,sha256=KOV2vc17OypCoLGU5NvcmuqpYxasPtk2grDJBCoULbY,264
|
|
4
|
+
corecli/auth.py,sha256=F7Hq1TiFhsQ0Ai_e6TzlTEyXutbda-aX9_Q7QA6nzv4,13501
|
|
5
|
+
corecli/cli.py,sha256=mwA7pOqr96MtXL0ROIQh2Wy66_3XB_GvtzadXzGX3V0,955
|
|
6
|
+
corecli/pkce.py,sha256=eZkbryW2A74wFSr9ark-fU-wQeC6Qh1LXhHiBj5Wo7g,435
|
|
7
|
+
corecli/utils.py,sha256=9NDqjMEM5rxyZQR7FK3ivg4NLa581AeUEJN8lTBNG_I,1136
|
|
8
|
+
corclient-0.1.0.dist-info/METADATA,sha256=qBQKJST1Id8I3aTxMxFplKVoPRoeLkrlEz5aG-CxY7c,10187
|
|
9
|
+
corclient-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
corclient-0.1.0.dist-info/entry_points.txt,sha256=kVyCipMY9yXvgIMJnwONt8v-_ZfMbUPlRO7MzpuwbfQ,40
|
|
11
|
+
corclient-0.1.0.dist-info/top_level.txt,sha256=JoPv54lgmqVztIG_JrKHt7hDhK8KTGidli2_NfpK-4o,8
|
|
12
|
+
corclient-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
corecli
|
corecli/__init__.py
ADDED
corecli/_version.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# File generated by setuptools-scm
|
|
2
|
+
# DO NOT EDIT - version is managed by Git tags
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from setuptools_scm import get_version
|
|
6
|
+
|
|
7
|
+
__version__ = get_version(root="..", relative_to=__file__)
|
|
8
|
+
except (ImportError, LookupError):
|
|
9
|
+
__version__ = "0.0.0+unknown"
|
corecli/auth.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import webbrowser
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from rich import box
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from corecli.pkce import generate_pkce_pair
|
|
15
|
+
from corecli.utils import clear_tokens, decode_jwt, load_tokens, save_tokens
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# =====================
|
|
21
|
+
# CONFIG FIJA (NO SE MODIFICA)
|
|
22
|
+
# =====================
|
|
23
|
+
CLIENT_ID = "1gd8tf85qs07ra4b38vcm4mih5" # <-- tu Client ID
|
|
24
|
+
COGNITO_DOMAIN = "https://corcli.projectcor.com"
|
|
25
|
+
REDIRECT_URI = "http://localhost:8000/callback"
|
|
26
|
+
SCOPES = ["openid", "email", "profile"]
|
|
27
|
+
LOGOUT_REDIRECT_URI = "http://localhost:8000/logout"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# =====================
|
|
31
|
+
# HTTP HANDLER PARA CALLBACK
|
|
32
|
+
# =====================
|
|
33
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
34
|
+
def log_message(self, format, *args):
|
|
35
|
+
# Silencia logs de acceso/informativos
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
def log_error(self, format, *args):
|
|
39
|
+
# Muestra únicamente errores a stderr
|
|
40
|
+
try:
|
|
41
|
+
sys.stderr.write(
|
|
42
|
+
f"{self.address_string()} - - [{self.log_date_time_string()}] ERROR: {format % args}\n"
|
|
43
|
+
)
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def do_GET(self):
|
|
48
|
+
if self.path.startswith("/callback"):
|
|
49
|
+
query = parse_qs(urlparse(self.path).query)
|
|
50
|
+
self.server.auth_code = query.get("code", [None])[0]
|
|
51
|
+
app_redirect = getattr(self.server, "app_redirect", None)
|
|
52
|
+
js_app_redirect = json.dumps(app_redirect) if app_redirect else "null"
|
|
53
|
+
|
|
54
|
+
self.send_response(200)
|
|
55
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
56
|
+
self.end_headers()
|
|
57
|
+
|
|
58
|
+
html = f"""
|
|
59
|
+
<!doctype html>
|
|
60
|
+
<html lang=\"es\">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset=\"utf-8\" />
|
|
63
|
+
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
|
|
64
|
+
<title>CoreCLI · Login exitoso</title>
|
|
65
|
+
<style>
|
|
66
|
+
:root {{
|
|
67
|
+
--bg: #0b1020;
|
|
68
|
+
--card: #121a33;
|
|
69
|
+
--accent: #8ab4f8;
|
|
70
|
+
--ok: #34d399;
|
|
71
|
+
--text: #e5e7eb;
|
|
72
|
+
--muted: #94a3b8;
|
|
73
|
+
}}
|
|
74
|
+
* {{ box-sizing: border-box; }}
|
|
75
|
+
body {{
|
|
76
|
+
margin: 0;
|
|
77
|
+
min-height: 100vh;
|
|
78
|
+
display: grid;
|
|
79
|
+
place-items: center;
|
|
80
|
+
background: radial-gradient(1200px 600px at 20% -10%, #1f2a52 0%, #0b1020 70%);
|
|
81
|
+
color: var(--text);
|
|
82
|
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial, sans-serif;
|
|
83
|
+
}}
|
|
84
|
+
.card {{
|
|
85
|
+
width: min(680px, 92vw);
|
|
86
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
|
|
87
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
88
|
+
border-radius: 16px;
|
|
89
|
+
padding: 28px 24px;
|
|
90
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06);
|
|
91
|
+
backdrop-filter: blur(6px);
|
|
92
|
+
}}
|
|
93
|
+
h1 {{
|
|
94
|
+
margin: 0 0 8px;
|
|
95
|
+
font-size: 24px;
|
|
96
|
+
letter-spacing: 0.2px;
|
|
97
|
+
}}
|
|
98
|
+
p {{
|
|
99
|
+
margin: 6px 0;
|
|
100
|
+
color: var(--muted);
|
|
101
|
+
line-height: 1.6;
|
|
102
|
+
}}
|
|
103
|
+
.ok {{ color: var(--ok); font-weight: 600; }}
|
|
104
|
+
.meta {{
|
|
105
|
+
margin-top: 16px;
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: 1fr 1fr;
|
|
108
|
+
gap: 8px 16px;
|
|
109
|
+
font-size: 14px;
|
|
110
|
+
color: #c7d2fe;
|
|
111
|
+
}}
|
|
112
|
+
.countdown {{
|
|
113
|
+
margin-top: 14px;
|
|
114
|
+
padding: 10px 12px;
|
|
115
|
+
border: 1px dashed rgba(138,180,248,0.4);
|
|
116
|
+
border-radius: 10px;
|
|
117
|
+
background: rgba(26, 35, 75, 0.35);
|
|
118
|
+
color: var(--accent);
|
|
119
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
120
|
+
}}
|
|
121
|
+
.small {{ font-size: 13px; color: var(--muted); }}
|
|
122
|
+
a.btn {{
|
|
123
|
+
display: inline-block;
|
|
124
|
+
margin-top: 16px;
|
|
125
|
+
padding: 10px 14px;
|
|
126
|
+
border-radius: 10px;
|
|
127
|
+
background: #22315f;
|
|
128
|
+
color: #e8eeff;
|
|
129
|
+
text-decoration: none;
|
|
130
|
+
border: 1px solid rgba(138,180,248,0.25);
|
|
131
|
+
}}
|
|
132
|
+
a.btn:hover {{ background: #2b3c70; }}
|
|
133
|
+
</style>
|
|
134
|
+
<script>
|
|
135
|
+
const appRedirect = {js_app_redirect};
|
|
136
|
+
if (appRedirect) {{
|
|
137
|
+
// Intenta volver a la app que abrió el login
|
|
138
|
+
setTimeout(() => {{ window.location.href = appRedirect; }}, 0);
|
|
139
|
+
}}
|
|
140
|
+
let secs = 10;
|
|
141
|
+
function tick() {{
|
|
142
|
+
const el = document.getElementById('secs');
|
|
143
|
+
if (el) el.textContent = secs.toString();
|
|
144
|
+
secs -= 1;
|
|
145
|
+
if (secs < 0) {{
|
|
146
|
+
try {{
|
|
147
|
+
window.open('', '_self');
|
|
148
|
+
window.close();
|
|
149
|
+
}} catch (e) {{}}
|
|
150
|
+
}} else {{
|
|
151
|
+
setTimeout(tick, 1000);
|
|
152
|
+
}}
|
|
153
|
+
}}
|
|
154
|
+
window.addEventListener('DOMContentLoaded', tick);
|
|
155
|
+
</script>
|
|
156
|
+
<meta http-equiv=\"refresh\" content=\"35;url=about:blank\">
|
|
157
|
+
<!-- meta de respaldo: si window.close falla, al menos salimos de la página -->
|
|
158
|
+
<link rel=\"icon\" href=\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><circle cx='32' cy='32' r='30' fill='%238ab4f8'/><path d='M20 34l7 7 17-17' stroke='white' stroke-width='6' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>\" />
|
|
159
|
+
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
<div class=\"card\">
|
|
163
|
+
<h1>✅ Login exitoso</h1>
|
|
164
|
+
<p>Ya puedes volver a la terminal. Esta pestaña se cerrará automáticamente.</p>
|
|
165
|
+
<div class=\"countdown\">Cerrando en <span id=\"secs\">10</span> segundos…</div>
|
|
166
|
+
<p class=\"small\">Si no se cierra por políticas del navegador, puedes cerrarla manualmente.</p>
|
|
167
|
+
<a class=\"btn\" href=\"about:blank\">Cerrar ahora</a>
|
|
168
|
+
</div>
|
|
169
|
+
</body>
|
|
170
|
+
</html>
|
|
171
|
+
"""
|
|
172
|
+
self.wfile.write(html.encode("utf-8"))
|
|
173
|
+
elif self.path.startswith("/logout"):
|
|
174
|
+
self.send_response(200)
|
|
175
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
176
|
+
self.end_headers()
|
|
177
|
+
|
|
178
|
+
html = """
|
|
179
|
+
<!doctype html>
|
|
180
|
+
<html lang=\"es\">
|
|
181
|
+
<head>
|
|
182
|
+
<meta charset=\"utf-8\" />
|
|
183
|
+
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
|
|
184
|
+
<title>CoreCLI · Logout</title>
|
|
185
|
+
<style>
|
|
186
|
+
:root {
|
|
187
|
+
--bg: #0b1020;
|
|
188
|
+
--card: #121a33;
|
|
189
|
+
--accent: #fca5a5;
|
|
190
|
+
--ok: #34d399;
|
|
191
|
+
--text: #e5e7eb;
|
|
192
|
+
--muted: #94a3b8;
|
|
193
|
+
}
|
|
194
|
+
* { box-sizing: border-box; }
|
|
195
|
+
body {
|
|
196
|
+
margin: 0;
|
|
197
|
+
min-height: 100vh;
|
|
198
|
+
display: grid;
|
|
199
|
+
place-items: center;
|
|
200
|
+
background: radial-gradient(1200px 600px at 20% -10%, #1f2a52 0%, #0b1020 70%);
|
|
201
|
+
color: var(--text);
|
|
202
|
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial, sans-serif;
|
|
203
|
+
}
|
|
204
|
+
.card {
|
|
205
|
+
width: min(680px, 92vw);
|
|
206
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
|
|
207
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
208
|
+
border-radius: 16px;
|
|
209
|
+
padding: 28px 24px;
|
|
210
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06);
|
|
211
|
+
backdrop-filter: blur(6px);
|
|
212
|
+
}
|
|
213
|
+
h1 {
|
|
214
|
+
margin: 0 0 8px;
|
|
215
|
+
font-size: 24px;
|
|
216
|
+
letter-spacing: 0.2px;
|
|
217
|
+
}
|
|
218
|
+
p {
|
|
219
|
+
margin: 6px 0;
|
|
220
|
+
color: var(--muted);
|
|
221
|
+
line-height: 1.6;
|
|
222
|
+
}
|
|
223
|
+
.countdown {
|
|
224
|
+
margin-top: 14px;
|
|
225
|
+
padding: 10px 12px;
|
|
226
|
+
border: 1px dashed rgba(252,165,165,0.35);
|
|
227
|
+
border-radius: 10px;
|
|
228
|
+
background: rgba(75, 26, 35, 0.25);
|
|
229
|
+
color: var(--accent);
|
|
230
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
231
|
+
}
|
|
232
|
+
.small { font-size: 13px; color: var(--muted); }
|
|
233
|
+
a.btn {
|
|
234
|
+
display: inline-block;
|
|
235
|
+
margin-top: 16px;
|
|
236
|
+
padding: 10px 14px;
|
|
237
|
+
border-radius: 10px;
|
|
238
|
+
background: #5f2222;
|
|
239
|
+
color: #ffe8e8;
|
|
240
|
+
text-decoration: none;
|
|
241
|
+
border: 1px solid rgba(252,165,165,0.35);
|
|
242
|
+
}
|
|
243
|
+
a.btn:hover { background: #703030; }
|
|
244
|
+
</style>
|
|
245
|
+
<script>
|
|
246
|
+
let secs = 5;
|
|
247
|
+
function tick() {
|
|
248
|
+
const el = document.getElementById('secs');
|
|
249
|
+
if (el) el.textContent = secs.toString();
|
|
250
|
+
secs -= 1;
|
|
251
|
+
if (secs < 0) {
|
|
252
|
+
try {
|
|
253
|
+
window.open('', '_self');
|
|
254
|
+
window.close();
|
|
255
|
+
} catch (e) {}
|
|
256
|
+
} else {
|
|
257
|
+
setTimeout(tick, 1000);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
window.addEventListener('DOMContentLoaded', tick);
|
|
261
|
+
</script>
|
|
262
|
+
<meta http-equiv=\"refresh\" content=\"10;url=about:blank\">
|
|
263
|
+
</head>
|
|
264
|
+
<body>
|
|
265
|
+
<div class=\"card\">
|
|
266
|
+
<h1>👋 Sesión cerrada</h1>
|
|
267
|
+
<p>Ya puedes volver a la terminal. Esta pestaña se cerrará automáticamente.</p>
|
|
268
|
+
<div class=\"countdown\">Cerrando en <span id=\"secs\">5</span> segundos…</div>
|
|
269
|
+
<a class=\"btn\" href=\"about:blank\">Cerrar ahora</a>
|
|
270
|
+
</div>
|
|
271
|
+
</body>
|
|
272
|
+
</html>
|
|
273
|
+
"""
|
|
274
|
+
self.wfile.write(html.encode("utf-8"))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def start_local_server(app_redirect: Optional[str] = None):
|
|
278
|
+
server = HTTPServer(("localhost", 8000), CallbackHandler)
|
|
279
|
+
# adjunta destino para que la página callback pueda redirigir
|
|
280
|
+
server.app_redirect = app_redirect # type: ignore[attr-defined]
|
|
281
|
+
server.handle_request()
|
|
282
|
+
return server.auth_code # type: ignore[attr-defined]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# =====================
|
|
286
|
+
# AUTH FLOW
|
|
287
|
+
# =====================
|
|
288
|
+
def login(app_redirect: Optional[str] = None):
|
|
289
|
+
code_verifier, code_challenge = generate_pkce_pair()
|
|
290
|
+
|
|
291
|
+
auth_url = (
|
|
292
|
+
f"{COGNITO_DOMAIN}/oauth2/authorize?"
|
|
293
|
+
f"response_type=code&client_id={CLIENT_ID}"
|
|
294
|
+
f"&redirect_uri={REDIRECT_URI}"
|
|
295
|
+
f"&scope={' '.join(SCOPES)}"
|
|
296
|
+
f"&code_challenge={code_challenge}&code_challenge_method=S256"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
console.print(
|
|
300
|
+
Panel.fit(
|
|
301
|
+
"Abriendo navegador para autenticación…\n\n"
|
|
302
|
+
"Si no se abre automáticamente, copia y pega esta URL:\n"
|
|
303
|
+
f"[cyan]{auth_url}[/cyan]",
|
|
304
|
+
title="[bold cyan]CoreCLI · Login",
|
|
305
|
+
border_style="cyan",
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
webbrowser.open(auth_url)
|
|
309
|
+
|
|
310
|
+
auth_code = start_local_server(app_redirect=app_redirect)
|
|
311
|
+
if not auth_code:
|
|
312
|
+
console.print(
|
|
313
|
+
Panel(
|
|
314
|
+
"No se recibió el código de autorización.", title="[red]Error", border_style="red"
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
raise Exception("No se recibió el código de autorización.")
|
|
318
|
+
|
|
319
|
+
token_url = f"{COGNITO_DOMAIN}/oauth2/token"
|
|
320
|
+
data = {
|
|
321
|
+
"grant_type": "authorization_code",
|
|
322
|
+
"client_id": CLIENT_ID,
|
|
323
|
+
"redirect_uri": REDIRECT_URI,
|
|
324
|
+
"code": auth_code,
|
|
325
|
+
"code_verifier": code_verifier,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
resp = requests.post(
|
|
329
|
+
token_url, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
330
|
+
)
|
|
331
|
+
resp.raise_for_status()
|
|
332
|
+
tokens = resp.json()
|
|
333
|
+
|
|
334
|
+
save_tokens(tokens)
|
|
335
|
+
console.print(
|
|
336
|
+
Panel.fit(
|
|
337
|
+
"✅ Autenticación exitosa, tokens guardados.", border_style="green", title="[green]OK"
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def refresh_tokens():
|
|
343
|
+
tokens = load_tokens()
|
|
344
|
+
if not tokens or "refresh_token" not in tokens:
|
|
345
|
+
console.print(
|
|
346
|
+
Panel.fit(
|
|
347
|
+
"No hay refresh token guardado. Ejecuta `core-login login` para autenticarse de nuevo.",
|
|
348
|
+
title="[yellow]Aviso",
|
|
349
|
+
border_style="yellow",
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
token_url = f"{COGNITO_DOMAIN}/oauth2/token"
|
|
355
|
+
data = {
|
|
356
|
+
"grant_type": "refresh_token",
|
|
357
|
+
"client_id": CLIENT_ID,
|
|
358
|
+
"refresh_token": tokens["refresh_token"],
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
resp = requests.post(
|
|
362
|
+
token_url, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
363
|
+
)
|
|
364
|
+
resp.raise_for_status()
|
|
365
|
+
new_tokens = resp.json()
|
|
366
|
+
|
|
367
|
+
if "refresh_token" not in new_tokens:
|
|
368
|
+
new_tokens["refresh_token"] = tokens["refresh_token"]
|
|
369
|
+
|
|
370
|
+
save_tokens(new_tokens)
|
|
371
|
+
console.print(
|
|
372
|
+
Panel.fit("🔄 Tokens refrescados correctamente.", border_style="green", title="[green]OK")
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def whoami():
|
|
377
|
+
tokens = load_tokens()
|
|
378
|
+
if not tokens or "id_token" not in tokens:
|
|
379
|
+
console.print(
|
|
380
|
+
Panel.fit(
|
|
381
|
+
"No hay sesión activa. Ejecuta `core-login login` primero.",
|
|
382
|
+
title="[yellow]Aviso",
|
|
383
|
+
border_style="yellow",
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
id_token = tokens["id_token"]
|
|
389
|
+
claims = decode_jwt(id_token)
|
|
390
|
+
|
|
391
|
+
table = Table(
|
|
392
|
+
title="👤 Usuario autenticado",
|
|
393
|
+
show_header=False,
|
|
394
|
+
box=box.SIMPLE,
|
|
395
|
+
padding=(0, 1),
|
|
396
|
+
border_style="cyan",
|
|
397
|
+
)
|
|
398
|
+
table.add_row("sub", f"[white]{claims.get('sub')}[/white]")
|
|
399
|
+
table.add_row("email", f"[white]{claims.get('email')}[/white]")
|
|
400
|
+
table.add_row("username", f"[white]{claims.get('cognito:username')}[/white]")
|
|
401
|
+
console.print(table)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def logout(local_only: bool = True):
|
|
405
|
+
"""Cierra sesión local: revoca refresh token (best-effort) y borra tokens locales."""
|
|
406
|
+
tokens = load_tokens()
|
|
407
|
+
|
|
408
|
+
# Revocación opcional de refresh_token (si el pool lo permite)
|
|
409
|
+
try:
|
|
410
|
+
if tokens and tokens.get("refresh_token"):
|
|
411
|
+
revoke_url = f"{COGNITO_DOMAIN}/oauth2/revoke"
|
|
412
|
+
data = {
|
|
413
|
+
"token": tokens["refresh_token"],
|
|
414
|
+
"client_id": CLIENT_ID,
|
|
415
|
+
}
|
|
416
|
+
# best-effort
|
|
417
|
+
requests.post(
|
|
418
|
+
revoke_url,
|
|
419
|
+
data=data,
|
|
420
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
421
|
+
timeout=5,
|
|
422
|
+
)
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
|
|
426
|
+
# Borra tokens locales
|
|
427
|
+
cleared = clear_tokens()
|
|
428
|
+
|
|
429
|
+
# Ya no se abre Hosted UI: logout 100% local
|
|
430
|
+
|
|
431
|
+
if cleared:
|
|
432
|
+
console.print(
|
|
433
|
+
Panel.fit("🧹 Tokens locales eliminados.", border_style="green", title="[green]OK")
|
|
434
|
+
)
|
|
435
|
+
else:
|
|
436
|
+
console.print(
|
|
437
|
+
Panel.fit(
|
|
438
|
+
"No se pudieron borrar los tokens locales.", border_style="red", title="[red]Aviso"
|
|
439
|
+
)
|
|
440
|
+
)
|
corecli/cli.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from corecli import auth
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="CoreCLI - CLI de ejemplo con login en Cognito usando PKCE")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command()
|
|
11
|
+
def login(
|
|
12
|
+
app_redirect: Optional[str] = typer.Option(
|
|
13
|
+
None,
|
|
14
|
+
"--app-redirect",
|
|
15
|
+
"-r",
|
|
16
|
+
help="URL o esquema (p.ej. myapp://callback) al que redirigir tras login",
|
|
17
|
+
),
|
|
18
|
+
):
|
|
19
|
+
"""Inicia el flujo de autenticación con Cognito (Hosted UI + PKCE)."""
|
|
20
|
+
auth.login(app_redirect=app_redirect)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def refresh():
|
|
25
|
+
"""Refresca los tokens usando refresh_token guardado."""
|
|
26
|
+
auth.refresh_tokens()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def whoami():
|
|
31
|
+
"""Muestra información del usuario autenticado (id_token)."""
|
|
32
|
+
auth.whoami()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command()
|
|
36
|
+
def logout():
|
|
37
|
+
"""Cierra sesión local (revoca/borrado de tokens)."""
|
|
38
|
+
auth.logout()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
"""Compatibilidad: permite ejecutar como 'python -m corecli.cli' o entrypoint antiguo."""
|
|
43
|
+
app()
|
corecli/pkce.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import secrets
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_pkce_pair():
|
|
7
|
+
"""Genera (code_verifier, code_challenge) para PKCE."""
|
|
8
|
+
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("utf-8").rstrip("=")
|
|
9
|
+
code_challenge = (
|
|
10
|
+
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("utf-8")).digest())
|
|
11
|
+
.decode("utf-8")
|
|
12
|
+
.rstrip("=")
|
|
13
|
+
)
|
|
14
|
+
return code_verifier, code_challenge
|
corecli/utils.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
CONFIG_DIR = os.path.expanduser("~/.corecli")
|
|
7
|
+
TOKENS_FILE = os.path.join(CONFIG_DIR, "tokens.json")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def save_tokens(tokens: dict):
|
|
11
|
+
os.makedirs(CONFIG_DIR, exist_ok=True)
|
|
12
|
+
with open(TOKENS_FILE, "w") as f:
|
|
13
|
+
json.dump(tokens, f, indent=2)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_tokens():
|
|
17
|
+
if os.path.exists(TOKENS_FILE):
|
|
18
|
+
with open(TOKENS_FILE) as f:
|
|
19
|
+
return json.load(f)
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def decode_jwt(token: str) -> dict[str, Any]:
|
|
24
|
+
"""Decodifica un JWT (solo payload, sin validar firma)."""
|
|
25
|
+
parts = token.split(".")
|
|
26
|
+
if len(parts) != 3:
|
|
27
|
+
raise ValueError("Token JWT inválido")
|
|
28
|
+
|
|
29
|
+
payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4) # padding
|
|
30
|
+
payload_json = base64.urlsafe_b64decode(payload_b64.encode()).decode()
|
|
31
|
+
return json.loads(payload_json) # type: ignore[no-any-return]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def clear_tokens() -> bool:
|
|
35
|
+
"""Elimina el archivo de tokens local si existe."""
|
|
36
|
+
if os.path.exists(TOKENS_FILE):
|
|
37
|
+
try:
|
|
38
|
+
os.remove(TOKENS_FILE)
|
|
39
|
+
return True
|
|
40
|
+
except OSError:
|
|
41
|
+
return False
|
|
42
|
+
return True
|