Metadata-Version: 2.4
Name: uvb-flask
Version: 0.2.1
Summary: Flask extension for Universal Verification Broker (UVB)
Project-URL: Homepage, https://gitlab.com/sparkz-community/security/uvb
Project-URL: Documentation, https://gitlab.com/sparkz-community/security/uvb/-/blob/main/docs/README.md
Project-URL: Repository, https://gitlab.com/sparkz-community/security/uvb
Project-URL: Issues, https://gitlab.com/sparkz-community/security/uvb/-/issues
Author: UVB Team
License: MIT
Keywords: authentication,extension,flask,mfa,security,uvb
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: flask>=2.0.0
Requires-Dist: uvb-client>=0.1.0
Provides-Extra: dev
Requires-Dist: black>=23.0.0; extra == 'dev'
Requires-Dist: mypy>=1.0.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# uvb-flask

Flask extension for Universal Verification Broker (UVB) authentication.

## Installation

```bash
pip install uvb-flask
```

## Quick Start

```python
from flask import Flask, jsonify
from uvb_flask import UVBFlask, get_session, require_factors, uvb_required

app = Flask(__name__)

# Initialize UVB extension
uvb = UVBFlask(
    app,
    tenant_id='my-tenant',
    uvb_url='http://localhost:8080',
    exclude_paths=['/login', '/health', '/public']
)

# Access authenticated user
@app.route('/profile')
def profile():
    session = get_session()
    return jsonify({
        'user_id': session.user_id,
        'factors': session.factors_verified
    })

# Require authentication with decorator
@app.route('/protected')
@uvb_required
def protected():
    user_id = g.uvb_session.user_id
    return jsonify({'message': f'Hello {user_id}'})

# Require specific MFA factors
@app.route('/admin/delete', methods=['POST'])
@require_factors('totp', 'webauthn')
def admin_delete():
    return jsonify({'message': 'Admin action completed'})

if __name__ == '__main__':
    app.run(port=3000)
```

## API Reference

### UVBFlask Extension

#### Direct initialization:

```python
from flask import Flask
from uvb_flask import UVBFlask

app = Flask(__name__)
uvb = UVBFlask(
    app,
    tenant_id='my-tenant',
    uvb_url='http://localhost:8080',  # Optional, defaults to http://localhost:8080
    api_key='optional-api-key',  # Optional
    cookie_name='uvb_session',  # Optional, defaults to uvb_session
    exclude_paths=['/login', '/health']  # Optional, defaults to []
)
```

#### Factory pattern with init_app:

```python
from flask import Flask
from uvb_flask import UVBFlask

uvb = UVBFlask()

def create_app():
    app = Flask(__name__)
    uvb.init_app(
        app,
        tenant_id='my-tenant',
        uvb_url='http://localhost:8080'
    )
    return app
```

#### Configuration from app.config:

```python
app = Flask(__name__)
app.config['UVB_TENANT_ID'] = 'my-tenant'
app.config['UVB_URL'] = 'http://localhost:8080'
app.config['UVB_API_KEY'] = 'optional-api-key'
app.config['UVB_COOKIE_NAME'] = 'uvb_session'
app.config['UVB_EXCLUDE_PATHS'] = ['/login', '/health']

uvb = UVBFlask(app)
```

**Session Access:**

After successful authentication, `g.uvb_session` contains a `UVBSession` object:

```python
from flask import g

@app.route('/user')
def user():
    session = g.uvb_session
    return {
        'user_id': session.user_id,
        'tenant_id': session.tenant_id,
        'session_id': session.session_id,
        'factors_verified': session.factors_verified,
        'expires_at': session.expires_at.isoformat()
    }
```

### Decorators

#### `@uvb_required`

Require UVB authentication for a route.

```python
from uvb_flask import uvb_required

@app.route('/profile')
@uvb_required
def profile():
    user_id = g.uvb_session.user_id
    return {'user_id': user_id}
```

#### `@require_factors(*factors)`

Require specific MFA factors for a route.

```python
from uvb_flask import require_factors

@app.route('/sensitive', methods=['POST'])
@require_factors('totp', 'webauthn')
def sensitive():
    return {'message': 'Sensitive operation completed'}
```

### Helper Functions

#### `get_session()`

Get the UVB session from Flask's g object.

```python
from uvb_flask import get_session

@app.route('/info')
def info():
    session = get_session()
    if session:
        return {'user_id': session.user_id}
    return {'error': 'Not authenticated'}, 401
```

## Examples

### Protecting Specific Routes

```python
from flask import Flask
from uvb_flask import UVBFlask, uvb_required

app = Flask(__name__)
uvb = UVBFlask(app, tenant_id='my-tenant')

# Public routes (no authentication required)
@app.route('/health')
def health():
    return {'status': 'ok'}

# Protected routes (authentication required)
@app.route('/api/data')
@uvb_required
def data():
    return {'data': 'protected'}
```

### Using Blueprints

```python
from flask import Blueprint, g
from uvb_flask import uvb_required, require_factors

# API Blueprint
api = Blueprint('api', __name__, url_prefix='/api')

@api.route('/profile')
@uvb_required
def profile():
    return {'user_id': g.uvb_session.user_id}

# Admin Blueprint with strict MFA
admin = Blueprint('admin', __name__, url_prefix='/admin')

@admin.before_request
@require_factors('totp', 'webauthn')
def require_admin_auth():
    pass

@admin.route('/users')
def admin_users():
    return {'users': [...]}

# Register blueprints
app.register_blueprint(api)
app.register_blueprint(admin)
```

### Conditional MFA Requirements

```python
from flask import request
from uvb_flask import get_session

@app.route('/transfer', methods=['POST'])
def transfer():
    session = get_session()
    if not session:
        return {'error': 'Not authenticated'}, 401

    amount = request.json.get('amount', 0)

    # Require additional auth for large transfers
    if amount > 10000:
        if not all(f in session.factors_verified for f in ['totp', 'webauthn']):
            return {
                'error': 'Additional authentication required',
                'required': ['totp', 'webauthn'],
                'verified': session.factors_verified
            }, 403

    return {'message': 'Transfer initiated'}
```

### Custom Error Handling

```python
@app.errorhandler(401)
def unauthorized(error):
    return {
        'error': 'Unauthorized',
        'message': 'Please authenticate to access this resource'
    }, 401

@app.errorhandler(403)
def forbidden(error):
    return {
        'error': 'Forbidden',
        'message': 'Insufficient permissions'
    }, 403
```

### Flask-RESTful Integration

```python
from flask import Flask
from flask_restful import Api, Resource
from uvb_flask import UVBFlask, get_session, require_factors

app = Flask(__name__)
api = Api(app)
uvb = UVBFlask(app, tenant_id='my-tenant')

class UserProfile(Resource):
    def get(self):
        session = get_session()
        if not session:
            return {'error': 'Not authenticated'}, 401
        return {'user_id': session.user_id}

class AdminResource(Resource):
    method_decorators = [require_factors('totp', 'webauthn')]

    def delete(self, user_id):
        return {'message': f'User {user_id} deleted'}

api.add_resource(UserProfile, '/profile')
api.add_resource(AdminResource, '/admin/user/<string:user_id>')
```

### Application Factory Pattern

```python
from flask import Flask
from uvb_flask import UVBFlask

uvb = UVBFlask()

def create_app(config=None):
    app = Flask(__name__)

    if config:
        app.config.from_object(config)

    # Initialize extensions
    uvb.init_app(
        app,
        tenant_id=app.config.get('UVB_TENANT_ID'),
        uvb_url=app.config.get('UVB_URL')
    )

    # Register blueprints
    from .api import api_bp
    app.register_blueprint(api_bp)

    return app

# Run the app
app = create_app()
if __name__ == '__main__':
    app.run()
```

### Testing

```python
import pytest
from flask import Flask
from uvb_flask import UVBFlask

@pytest.fixture
def app():
    app = Flask(__name__)
    app.config['TESTING'] = True
    app.config['UVB_TENANT_ID'] = 'test-tenant'

    uvb = UVBFlask(app)

    @app.route('/test')
    def test_route():
        return {'message': 'test'}

    return app

@pytest.fixture
def client(app):
    return app.test_client()

def test_authentication_required(client):
    response = client.get('/test')
    assert response.status_code == 401

def test_with_valid_token(client):
    response = client.get(
        '/test',
        headers={'Authorization': 'Bearer valid-token'}
    )
    assert response.status_code == 200
```

## Development

```bash
# Install development dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Type checking
mypy uvb_flask
```

## License

MIT
