diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..faf2e18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug Report +about: Report a bug or unexpected behavior +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Description + +A clear and concise description of the bug. + +## To Reproduce + +Steps to reproduce the behavior: +1. +2. +3. + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. + +## Environment + +- OS: [e.g., Ubuntu 22.04, macOS 14, Windows 11] +- Rust version (if applicable): [e.g., 1.75.0] +- Node.js version (if applicable): [e.g., 20.10.0] +- ATAuth version: [e.g., 0.1.0] +- Component: [Rust library / TypeScript package / Gateway] + +## Additional Context + +Add any other context, logs, or screenshots about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f27db5c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Problem Statement + +A clear description of the problem or limitation you're experiencing. + +## Proposed Solution + +Describe the solution you'd like to see. + +## Alternatives Considered + +Any alternative solutions or features you've considered. + +## Use Case + +Describe your use case and how this feature would help. + +## Additional Context + +Add any other context, mockups, or examples about the feature request. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3874847 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,26 @@ +## Description + +Brief description of the changes in this PR. + +## Related Issues + +Fixes #(issue number) + +## Type of Change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update + +## Checklist + +- [ ] My code follows the project's code style +- [ ] I have added tests that prove my fix/feature works +- [ ] All new and existing tests pass +- [ ] I have updated the documentation accordingly +- [ ] I have added entries to CHANGELOG.md (if applicable) + +## Testing + +Describe how you tested these changes. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cfe4284 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,104 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + rust: + name: Rust + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-action@stable + with: + components: rustfmt, clippy + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run clippy + run: cargo clippy --all-features -- -D warnings + + - name: Build + run: cargo build --all-features + + - name: Run tests + run: cargo test --all-features + + typescript: + name: TypeScript + runs-on: ubuntu-latest + defaults: + run: + working-directory: ts + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: ts/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + + - name: Test + run: npm test + + gateway: + name: Gateway + runs-on: ubuntu-latest + defaults: + run: + working-directory: gateway + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: gateway/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore index 74e8917..c907be5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ Cargo.lock ts/node_modules/ ts/dist/ ts/.turbo/ +gateway/node_modules/ +gateway/dist/ +gateway/.turbo/ # IDE .idea/ @@ -21,3 +24,31 @@ Thumbs.db # Test artifacts *.db *.sqlite + +# Environment files (keep examples) +.env +.env.local +.env.*.local +!.env.example + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage +coverage/ +.nyc_output/ + +# Build artifacts +*.tsbuildinfo + +# Gateway data +gateway/data/ diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index c18a070..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,74 +0,0 @@ -# ATAuth - AT Protocol Authentication Library - -## Project Structure - -``` -atauth/ -├── src/ -│ ├── lib.rs # Library entry point -│ ├── error.rs # Error types (AuthError, AuthResult) -│ ├── token.rs # Token verification (TokenVerifier, TokenPayload) -│ ├── validation.rs # DID/handle validation -│ ├── rate_limit.rs # IP-based rate limiting -│ └── session/ -│ ├── mod.rs -│ ├── store.rs # SessionStore trait, SessionManager -│ ├── sqlite.rs # SQLite implementation -│ └── postgres.rs # PostgreSQL implementation (placeholder) -├── ts/ # TypeScript package -│ └── src/ -│ ├── index.ts # Main exports -│ ├── react.ts # React hooks (useAuthStore) -│ ├── types.ts # TypeScript types -│ ├── token.ts # Client-side token utilities -│ ├── storage.ts # Browser storage utilities -│ └── oauth.ts # OAuth flow utilities -├── examples/ -│ ├── basic_verification.rs -│ └── with_session_store.rs -├── Cargo.toml -└── README.md -``` - -## Key Components - -### Rust Library - -- **TokenVerifier**: HMAC-SHA256 token verification with constant-time comparison -- **TokenPayload**: Decoded token with DID, handle, user_id, expiration -- **SessionManager**: High-level session CRUD operations -- **SessionStore trait**: Implement for custom backends (Redis, etc.) -- **RateLimiter**: IP-based auth attempt limiting - -### TypeScript Package - -- **decodeToken()**: Client-side token decoding (NOT verification) -- **handleCallback()**: OAuth callback processing -- **useAuthStore()**: Zustand-based React hook - -## Features - -- `default` = `session-sqlite` -- `session-sqlite` - SQLite session backend -- `session-postgres` - PostgreSQL session backend (WIP) -- `rate-limit` - IP-based rate limiting -- `full` - All features - -## Build Commands - -```bash -# Rust -cargo build -cargo test -cargo run --example basic_verification -cargo run --example with_session_store --features session-sqlite - -# TypeScript -cd ts && npm install && npm run build -``` - -## Security Notes - -- Server-side verification required (client decoding is display-only) -- Constant-time comparison prevents timing attacks -- Rate limiting should be enabled in production diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f5f0061 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,41 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers. All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3e42e71 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,96 @@ +# Contributing to ATAuth + +Thank you for your interest in contributing to ATAuth! This document provides guidelines and information for contributors. + +## Getting Started + +1. Fork the repository +2. Clone your fork locally +3. Create a new branch for your changes +4. Make your changes +5. Submit a pull request + +## Development Setup + +### Rust Library + +```bash +# Build the library +cargo build + +# Run tests +cargo test + +# Run examples +cargo run --example basic_verification +cargo run --example with_session_store --features session-sqlite +``` + +### TypeScript Package + +```bash +cd ts +npm install +npm run build +npm test +``` + +### Gateway Server + +```bash +cd gateway +npm install +cp .env.example .env +# Edit .env with your configuration +npm run dev +``` + +## Code Style + +### Rust + +- Follow standard Rust formatting (`cargo fmt`) +- Run clippy for linting (`cargo clippy`) +- Write doc comments for public APIs +- Add tests for new functionality + +### TypeScript + +- Use TypeScript strict mode +- Follow ESLint rules (`npm run lint`) +- Write JSDoc comments for exported functions +- Add tests for new functionality + +## Pull Request Guidelines + +1. **Keep PRs focused**: One feature or fix per PR +2. **Write clear commit messages**: Describe what and why +3. **Add tests**: Cover new functionality with tests +4. **Update documentation**: Keep README and docs current +5. **Follow existing patterns**: Match the codebase style + +## Reporting Issues + +When reporting issues, please include: + +- A clear description of the problem +- Steps to reproduce +- Expected vs actual behavior +- Environment details (OS, Rust/Node version) +- Relevant logs or error messages + +## Feature Requests + +Feature requests are welcome! Please: + +- Check existing issues first +- Clearly describe the use case +- Explain why this would benefit users + +## Security + +If you discover a security vulnerability, please report it privately rather than opening a public issue. See SECURITY.md for details. + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/Cargo.toml b/Cargo.toml index 76d05fd..74b00e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,10 @@ name = "atauth" version = "0.1.0" edition = "2021" -authors = ["Bryan Brooks "] +authors = ["ATAuth Contributors"] description = "Generic AT Protocol (Bluesky) authentication library for Rust applications" license = "MIT" -repository = "https://gitea.cloudforest-basilisk.ts.net/Arcnode.xyz/atauth" +repository = "https://github.com/Cache8063/atauth" keywords = ["atproto", "bluesky", "authentication", "oauth", "did"] categories = ["authentication", "web-programming"] readme = "README.md" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d3eba1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 ATAuth Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 8f34ffe..072294b 100644 --- a/README.md +++ b/README.md @@ -43,22 +43,22 @@ Add to your `Cargo.toml`: ```toml [dependencies] -atauth = { git = "https://gitea.cloudforest-basilisk.ts.net/Arcnode.xyz/atauth" } +atauth = { git = "https://github.com/Cache8063/atauth" } ``` Or with specific features: ```toml [dependencies] -atauth = { git = "https://gitea.cloudforest-basilisk.ts.net/Arcnode.xyz/atauth", features = ["session-sqlite", "rate-limit"] } +atauth = { git = "https://github.com/Cache8063/atauth", features = ["session-sqlite", "rate-limit"] } ``` ### TypeScript/JavaScript ```bash -npm install @arcnode/atauth +npm install atauth # or -pnpm add @arcnode/atauth +pnpm add atauth ``` ## Quick Start @@ -143,7 +143,7 @@ limiter.record_success(&client_ip); ### TypeScript - React Integration ```tsx -import { initAuthStore, useAuthStore } from '@arcnode/atauth/react'; +import { initAuthStore, useAuthStore } from 'atauth/react'; // Initialize once at app startup initAuthStore({ @@ -180,7 +180,7 @@ import { handleCallback, isOAuthCallback, redirectToAuth -} from '@arcnode/atauth'; +} from 'atauth'; // Redirect to auth redirectToAuth({ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2b9fcfb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in ATAuth, please report it responsibly. + +**Do not open a public GitHub issue for security vulnerabilities.** + +Instead, please send an email describing the vulnerability to the maintainers. Include: + +1. Description of the vulnerability +2. Steps to reproduce +3. Potential impact +4. Any suggested fixes (optional) + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 0.1.x | Yes | + +## Security Best Practices + +When using ATAuth: + +1. **Always use HTTPS** in production environments +2. **Keep HMAC secrets secure** - never commit them to version control +3. **Enable rate limiting** to prevent brute force attacks +4. **Verify tokens server-side** - client-side decoding is for display only +5. **Rotate secrets periodically** - especially if compromise is suspected +6. **Keep dependencies updated** - regularly update all packages + +## Known Security Considerations + +- Token verification uses constant-time comparison to prevent timing attacks +- Session tokens should be transmitted over secure channels only +- The gateway should be deployed behind a reverse proxy with TLS termination diff --git a/examples/basic_verification.rs b/examples/basic_verification.rs index 84fbbc7..f4d5ee9 100644 --- a/examples/basic_verification.rs +++ b/examples/basic_verification.rs @@ -26,7 +26,7 @@ fn main() { // Verify the token match verifier.verify(&token) { Ok(payload) => { - println!("✅ Token verified successfully!"); + println!("[OK] Token verified successfully!"); println!(); println!("User Information:"); println!(" DID: {}", payload.did); @@ -40,7 +40,7 @@ fn main() { println!(" Is expired: {}", payload.is_expired()); } Err(e) => { - println!("❌ Token verification failed!"); + println!("[FAIL] Token verification failed!"); println!(" Error: {}", e); println!(" HTTP Status: {}", e.http_status_code()); } @@ -53,31 +53,31 @@ fn main() { // Test invalid signature let wrong_verifier = TokenVerifier::new(b"wrong-secret"); match wrong_verifier.verify(&token) { - Ok(_) => println!("❌ Should have failed!"), + Ok(_) => println!("[FAIL] Should have failed!"), Err(AuthError::InvalidSignature) => { - println!("✅ Correctly rejected invalid signature"); + println!("[OK] Correctly rejected invalid signature"); } - Err(e) => println!("⚠️ Unexpected error: {}", e), + Err(e) => println!("[WARN] Unexpected error: {}", e), } // Test invalid format match verifier.verify("not-a-valid-token") { - Ok(_) => println!("❌ Should have failed!"), + Ok(_) => println!("[FAIL] Should have failed!"), Err(AuthError::InvalidFormat(_)) => { - println!("✅ Correctly rejected invalid format"); + println!("[OK] Correctly rejected invalid format"); } - Err(e) => println!("⚠️ Unexpected error: {}", e), + Err(e) => println!("[WARN] Unexpected error: {}", e), } // Test expired token let expired_payload = create_expired_payload(); let expired_token = verifier.sign(&expired_payload).expect("Failed to sign"); match verifier.verify(&expired_token) { - Ok(_) => println!("❌ Should have failed!"), + Ok(_) => println!("[FAIL] Should have failed!"), Err(AuthError::Expired) => { - println!("✅ Correctly rejected expired token"); + println!("[OK] Correctly rejected expired token"); } - Err(e) => println!("⚠️ Unexpected error: {}", e), + Err(e) => println!("[WARN] Unexpected error: {}", e), } } diff --git a/examples/with_session_store.rs b/examples/with_session_store.rs index dba2e9c..86234ca 100644 --- a/examples/with_session_store.rs +++ b/examples/with_session_store.rs @@ -33,16 +33,16 @@ fn main() -> Result<(), Box> { match verifier.verify(&token) { Ok(payload) => { - println!(" ✅ Token verified for: {}", payload.handle); + println!(" [OK] Token verified for: {}", payload.handle); // Create session let session = sessions.create_session(&payload, token.clone())?; - println!(" ✅ Session created"); + println!(" [OK] Session created"); println!(" Token: {}...", &session.token[..20]); println!(" Expires in: {} seconds", session.remaining_seconds()); } Err(e) => { - println!(" ❌ Verification failed: {}", e); + println!(" [FAIL] Verification failed: {}", e); return Ok(()); } } @@ -54,12 +54,12 @@ fn main() -> Result<(), Box> { // Simulate subsequent request with session token match sessions.validate(&token) { Ok(session) => { - println!(" ✅ Session valid for: {}", session.handle); + println!(" [OK] Session valid for: {}", session.handle); println!(" DID: {}", session.did); println!(" User ID: {:?}", session.user_id); } Err(e) => { - println!(" ❌ Session invalid: {}", e); + println!(" [FAIL] Session invalid: {}", e); } } @@ -75,7 +75,7 @@ fn main() -> Result<(), Box> { // Create another session for same user let second_token = verifier.sign(&test_payload)?; sessions.create_session(&test_payload, second_token)?; - println!(" ✅ Second session created"); + println!(" [OK] Second session created"); let user_sessions = sessions.get_user_sessions(&test_payload.did)?; println!(" Now has {} sessions", user_sessions.len()); @@ -83,7 +83,7 @@ fn main() -> Result<(), Box> { println!(); println!("5. Logout (invalidate first session)"); sessions.invalidate(&token)?; - println!(" ✅ First session invalidated"); + println!(" [OK] First session invalidated"); let user_sessions = sessions.get_user_sessions(&test_payload.did)?; println!(" Remaining sessions: {}", user_sessions.len()); @@ -91,7 +91,7 @@ fn main() -> Result<(), Box> { println!(); println!("6. Logout All Devices"); let deleted = sessions.invalidate_all_for_did(&test_payload.did)?; - println!(" ✅ Deleted {} session(s)", deleted); + println!(" [OK] Deleted {} session(s)", deleted); println!(); println!("7. Session Cleanup"); diff --git a/gateway/README.md b/gateway/README.md index 844c64d..2f44a77 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -151,7 +151,7 @@ Update app config (can rotate secret). ### Frontend (React) ```tsx -import { redirectToAuth, handleCallback } from '@arcnode/atauth'; +import { redirectToAuth, handleCallback } from 'atauth'; // Start login const startLogin = async (handle: string) => { diff --git a/gateway/package.json b/gateway/package.json index 483c7e8..c87f9b7 100644 --- a/gateway/package.json +++ b/gateway/package.json @@ -1,5 +1,5 @@ { - "name": "@arcnode/atauth-gateway", + "name": "atauth-gateway", "version": "0.1.0", "description": "AT Protocol OAuth gateway for application authentication", "main": "dist/index.js", @@ -19,11 +19,11 @@ "oauth", "gateway" ], - "author": "Bryan Brooks ", + "author": "ATAuth Contributors", "license": "MIT", "repository": { "type": "git", - "url": "https://gitea.cloudforest-basilisk.ts.net/Arcnode.xyz/atauth" + "url": "https://github.com/Cache8063/atauth" }, "dependencies": { "@atproto/oauth-client-node": "^0.1.0", diff --git a/ts/package.json b/ts/package.json index fb8576b..d3ec5cc 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,5 +1,5 @@ { - "name": "@arcnode/atauth", + "name": "atauth", "version": "0.1.0", "description": "AT Protocol (Bluesky) authentication utilities for TypeScript/JavaScript", "main": "dist/index.js", @@ -36,11 +36,11 @@ "did", "react" ], - "author": "Bryan Brooks ", + "author": "ATAuth Contributors", "license": "MIT", "repository": { "type": "git", - "url": "https://gitea.cloudforest-basilisk.ts.net/Arcnode.xyz/atauth" + "url": "https://github.com/Cache8063/atauth" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/ts/src/index.ts b/ts/src/index.ts index 148e496..09dbfa3 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -1,11 +1,11 @@ /** - * @arcnode/atauth - AT Protocol Authentication Library + * atauth - AT Protocol Authentication Library * * A generic, plug-and-play authentication library for AT Protocol (Bluesky) OAuth integration. * * @example * ```typescript - * import { decodeToken, handleCallback, redirectToAuth } from '@arcnode/atauth'; + * import { decodeToken, handleCallback, redirectToAuth } from 'atauth'; * * // Redirect to authentication * redirectToAuth({ diff --git a/ts/src/react.ts b/ts/src/react.ts index 5b929fd..e145b67 100644 --- a/ts/src/react.ts +++ b/ts/src/react.ts @@ -3,7 +3,7 @@ * * @example * ```tsx - * import { useAuthStore, AuthProvider } from '@arcnode/atauth/react'; + * import { useAuthStore, AuthProvider } from 'atauth/react'; * * function App() { * const { isAuthenticated, user, login, logout } = useAuthStore(); diff --git a/ts/src/token.ts b/ts/src/token.ts index 6a3e1b0..41d176a 100644 --- a/ts/src/token.ts +++ b/ts/src/token.ts @@ -32,7 +32,7 @@ function base64UrlDecode(str: string): string { /** * Decode a token payload WITHOUT verification. * - * ⚠️ WARNING: This does NOT verify the signature! + * WARNING: This does NOT verify the signature! * Use this only for client-side display purposes. * Always verify tokens server-side before trusting them. *