You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
Matteo Benedetto 412181b663 Update .gitignore to exclude NiceGUI files and modify user storage JSON 6 months ago
.nicegui Update .gitignore to exclude NiceGUI files and modify user storage JSON 6 months ago
middlewares first commit 6 months ago
pages first commit 6 months ago
services first commit 6 months ago
static first commit 6 months ago
tests first commit 6 months ago
.gitignore Update .gitignore to exclude NiceGUI files and modify user storage JSON 6 months ago
README.md first commit 6 months ago
app.py first commit 6 months ago
requirements.txt first commit 6 months ago

README.md

NiceGUI OIDC Authentication Demo

A modern authentication system built with NiceGUI and FastAPI, demonstrating how to implement OpenID Connect (OIDC) authentication with session management and route protection.

🚀 What This App Does

This application demonstrates:

  • OIDC Authentication: OpenID Connect integration with external identity providers
  • JWT Token Validation: Proper signature verification using JWKS
  • Session Management: Secure user sessions with automatic token refresh
  • Route Protection: Middleware that restricts access to authenticated users only
  • Automatic Redirects: Redirects users to OIDC provider and back to intended pages
  • Access Logging: Comprehensive request logging with user context

📁 Project Structure

authentication/
├── services/
│   ├── auth/
│   │   └── oidc.py            # OIDC configuration and token handling
│   └── log/
│       └── __init__.py        # Logging configuration
├── pages/
│   ├── main_page.py           # Main/home page
│   ├── subpage.py             # Example protected page
│   ├── login.py               # OIDC login initiation
│   └── auth_callback.py       # OIDC callback handler
├── middlewares/
│   ├── auth_middleware.py     # OIDC authentication middleware
│   └── access_middleware.py   # Request logging middleware
├── routes/
│   └── logout.py              # Logout functionality
├── tests/
│   └── test_authentication.py # Test suite
├── app.py                     # Main application
├── requirements.txt           # Dependencies
└── README.md

🔧 How to Run

  1. Install dependencies:

    pip install -r requirements.txt
    
  2. Configure OIDC settings in services/auth/oidc.py:

    self.client_id = "your-client-id"
    self.client_secret = "your-client-secret"
    self.discovery_url = "https://your-provider/.well-known/openid-configuration"
    self.redirect_uri = "http://127.0.0.1:8080/auth/callback"
    
  3. Run the application:

    python app.py
    
  4. Access the app:

    • Open your browser to http://localhost:8080
    • You'll be redirected to /login
    • Click "Log in with OIDC" to authenticate

🧪 Running Tests

# Run all tests
pytest tests/

# Run with verbose output
pytest tests/ -v

🏗 How to Build a Similar OIDC App

Step 1: OIDC Configuration

class OIDCConfig:
    def __init__(self):
        self.client_id = "your-client-id"
        self.client_secret = "your-client-secret"
        self.discovery_url = "https://provider/.well-known/openid-configuration"
        self.redirect_uri = "http://localhost:8080/auth/callback"
        self.scope = "openid profile email"

Step 2: Authentication Middleware

class OIDCAuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        excluded_paths = {'/login', '/auth/callback'}
        
        if request.url.path not in excluded_paths:
            if not app.storage.user.get('authenticated', False):
                app.storage.user['redirect_to'] = str(request.url.path)
                return RedirectResponse('/login')
                
            # Validate current token
            access_token = app.storage.user.get('access_token')
            if access_token and not oidc_config.validate_token(access_token):
                app.storage.user.clear()
                return RedirectResponse('/login')
                
        return await call_next(request)

Step 3: Login Page

@ui.page('/login')
def login():
    if app.storage.user.get('authenticated', False):
        return RedirectResponse('/')

    def initiate_oidc_login():
        state = str(uuid.uuid4())
        app.storage.user['oidc_state'] = state
        auth_url = oidc_config.get_authorization_url(state)
        ui.navigate.to(auth_url, new_tab=False)

    with ui.card().classes('absolute-center'):
        ui.label('Authentication Required')
        ui.button('Log in with OIDC', on_click=initiate_oidc_login)

Step 4: OIDC Callback Handler

@ui.page('/auth/callback')
async def auth_callback(code: str = None, state: str = None):
    # Verify state parameter
    stored_state = app.storage.user.get('oidc_state')
    if not stored_state or stored_state != state:
        ui.notify('Invalid state parameter', color='negative')
        return
    
    # Exchange code for tokens
    tokens = await oidc_config.exchange_code_for_tokens(code, oidc_config.redirect_uri)
    user_info = oidc_config.validate_token(tokens['id_token'])
    
    # Store user session
    app.storage.user.update({
        'username': user_info.get('preferred_username'),
        'email': user_info.get('email'),
        'authenticated': True,
        'access_token': tokens['access_token'],
        'refresh_token': tokens.get('refresh_token')
    })
    
    redirect_to = app.storage.user.get('redirect_to', '/')
    ui.navigate.to(redirect_to)

Step 5: Token Validation with JWKS

def validate_token(self, token: str) -> Optional[Dict]:
    try:
        unverified_header = jwt.get_unverified_header(token)
        kid = unverified_header.get('kid')
        
        jwks = self.get_jwks()
        public_key = None
        for key in jwks.get('keys', []):
            if key.get('kid') == kid:
                public_key = RSAAlgorithm.from_jwk(key)
                break
        
        payload = jwt.decode(
            token, public_key, algorithms=['RS256'],
            audience=self.client_id, options={"verify_signature": True}
        )
        return payload
    except Exception as e:
        logger.error(f"Token validation error: {e}")
        return None

🔐 Security Features

Production-ready security:

  • JWT signature verification using JWKS from identity provider
  • State parameter protection against CSRF attacks
  • Secure token storage in encrypted sessions
  • Automatic token refresh to maintain sessions
  • Proper logout with identity provider notification
  • Request logging with user context and IP tracking
  • HTTPS enforcement for token endpoints

📚 Key Features Explained

OIDC Flow

  1. User accesses protected resource → redirected to /login
  2. Login page redirects to OIDC provider with state parameter
  3. User authenticates with identity provider
  4. Provider redirects back to /auth/callback with authorization code
  5. App exchanges code for access/ID tokens
  6. ID token is validated using provider's public keys
  7. User session is established with token storage

Token Management

  • Access tokens for API authorization
  • ID tokens for user identity claims
  • Refresh tokens for automatic session renewal
  • JWKS validation ensures token authenticity

Access Logging

Every request is logged with:

  • Unique request ID
  • Client IP address (proxy-aware)
  • User information
  • Response time and status

🛠 Supported OIDC Providers

This implementation works with any standard OIDC provider:

  • Keycloak
  • Auth0
  • Azure AD
  • Google Identity
  • Okta
  • Custom OIDC providers

🤝 Contributing

Feel free to submit issues, feature requests, or pull requests to improve this OIDC authentication example!

📄 License

This example is provided as-is for educational purposes.