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 10 months ago
.nicegui Update .gitignore to exclude NiceGUI files and modify user storage JSON 10 months ago
middlewares first commit 10 months ago
pages first commit 10 months ago
services first commit 10 months ago
static first commit 10 months ago
tests first commit 10 months ago
.gitignore Update .gitignore to exclude NiceGUI files and modify user storage JSON 10 months ago
README.md first commit 10 months ago
app.py first commit 10 months ago
requirements.txt first commit 10 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.