Enhance OIDC provider support and frontend chunking

Improves OIDC provider handling in the backend by adding detailed error handling, HTTP client reuse, and new templates for Microsoft and Google providers. Updates frontend to use environment-based URLs for identity provider login, refines i18n text, and configures Vite to optimize chunk splitting and API proxying for better performance and development experience.
This commit is contained in:
João Vitória Silva
2025-10-14 12:32:14 +01:00
parent a76b58ec38
commit f6eb104260
6 changed files with 150 additions and 44 deletions

View File

@@ -901,7 +901,7 @@ def location_based_on_coordinates(latitude, longitude) -> dict | None:
# Make the request and get the response
try:
headers = {"User-Agent": "Endurain"}
headers = {"User-Agent": "Endurain/0.16.0 (ReverseGeocoding)"}
# Make the request and get the response
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()

View File

@@ -25,6 +25,29 @@ class IdentityProviderService:
self._discovery_cache: Dict[int, Dict[str, Any]] = {}
self._cache_expiry: Dict[int, datetime] = {}
self._cache_ttl = timedelta(hours=1)
self._http_client: httpx.AsyncClient | None = None
async def _get_http_client(self) -> httpx.AsyncClient:
"""
Asynchronously retrieves or creates an instance of httpx.AsyncClient for making HTTP requests.
If the HTTP client does not already exist, it initializes a new AsyncClient with a timeout of 10 seconds
and connection limits (maximum 5 keep-alive connections and 10 total connections). Returns the client instance.
Returns:
httpx.AsyncClient: The HTTP client instance for asynchronous requests.
"""
if self._http_client is None:
self._http_client = httpx.AsyncClient(
timeout=10.0,
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
follow_redirects=True,
headers={
"User-Agent": "Endurain/0.16.0 (OIDC Client)",
"Accept": "application/json",
}
)
return self._http_client
async def get_oidc_configuration(
self, idp: idp_models.IdentityProvider
@@ -54,22 +77,48 @@ class IdentityProviderService:
discovery_url = (
f"{idp.issuer_url.rstrip('/')}/.well-known/openid-configuration"
)
async with httpx.AsyncClient() as client:
response = await client.get(discovery_url, timeout=10.0)
response.raise_for_status()
config = response.json()
core_logger.print_to_log(
f"Fetching OIDC configuration from: {discovery_url}", "info"
)
client = await self._get_http_client()
response = await client.get(discovery_url)
core_logger.print_to_log(
f"OIDC discovery response status: {response.status_code}", "debug"
)
response.raise_for_status()
config = response.json()
# Cache the configuration
self._discovery_cache[idp.id] = config
self._cache_expiry[idp.id] = (
datetime.now(timezone.utc) + self._cache_ttl
)
# Cache the configuration
self._discovery_cache[idp.id] = config
self._cache_expiry[idp.id] = (
datetime.now(timezone.utc) + self._cache_ttl
)
core_logger.print_to_log(
f"Successfully fetched OIDC configuration for {idp.name}", "info"
)
return config
return config
except httpx.HTTPStatusError as err:
core_logger.print_to_log(
f"HTTP error fetching OIDC discovery for {idp.name}: {err.response.status_code} - {err.response.text}",
"warning"
)
return None
except httpx.ConnectError as err:
core_logger.print_to_log(
f"Connection error fetching OIDC discovery for {idp.name}. "
f"URL: {discovery_url}. Error: {err}. "
f"Check if the service is reachable and not using 'localhost' in Docker.",
"error"
)
return None
except httpx.RequestError as err:
core_logger.print_to_log(
f"Request error fetching OIDC discovery for {idp.name}. "
f"URL: {discovery_url}. Error: {err}",
"warning"
)
return None
except Exception as err:
core_logger.print_to_log(
f"Failed to fetch OIDC discovery for {idp.name}: {err}", "warning"
@@ -87,7 +136,7 @@ class IdentityProviderService:
str: The complete redirect URI for the specified identity provider.
"""
base_url = core_config.ENDURAIN_HOST
return f"{base_url}/api/v1/idp/callback/{idp_slug}"
return f"{base_url}/api/v1/public/idp/callback/{idp_slug}"
async def initiate_login(
self, idp: idp_models.IdentityProvider, request: Request, db: Session

View File

@@ -1,4 +1,5 @@
"""Identity Provider utility functions and templates"""
from typing import List, Dict, Any
import identity_providers.schema as idp_schema
@@ -14,10 +15,10 @@ IDP_TEMPLATES = {
"user_mapping": {
"username": ["preferred_username", "username", "email"],
"email": ["email", "mail"],
"name": ["name", "display_name", "full_name"]
"name": ["name", "display_name", "full_name"],
},
"description": "Keycloak - Open Source Identity and Access Management",
"configuration_notes": "Replace {your-keycloak-domain} with your Keycloak server domain (e.g., keycloak.example.com) and {realm} with your realm name. Create an OIDC client in Keycloak admin console."
"configuration_notes": "Replace {your-keycloak-domain} with your Keycloak server domain (e.g., keycloak.example.com) and {realm} with your realm name. Create an OIDC client in Keycloak admin console.",
},
"authentik": {
"name": "Authentik",
@@ -28,10 +29,10 @@ IDP_TEMPLATES = {
"user_mapping": {
"username": ["preferred_username", "username", "email"],
"email": ["email", "mail"],
"name": ["name", "display_name"]
"name": ["name", "display_name"],
},
"description": "Authentik - Open-source Identity Provider",
"configuration_notes": "Replace {your-authentik-domain} with your Authentik server domain (e.g., authentik.example.com) and {slug} with your application slug. Create an OAuth2/OIDC provider in Authentik."
"configuration_notes": "Replace {your-authentik-domain} with your Authentik server domain (e.g., authentik.example.com) and {slug} with your application slug. Create an OAuth2/OIDC provider in Authentik.",
},
"authelia": {
"name": "Authelia",
@@ -42,27 +43,23 @@ IDP_TEMPLATES = {
"user_mapping": {
"username": ["preferred_username", "username", "email"],
"email": ["email"],
"name": ["name"]
"name": ["name"],
},
"description": "Authelia - Open-source authentication and authorization server",
"configuration_notes": "Replace {your-authelia-domain} with your Authelia server domain (e.g., auth.example.com). Configure an OIDC client in your Authelia configuration file."
"configuration_notes": "Replace {your-authelia-domain} with your Authelia server domain (e.g., auth.example.com). Configure an OIDC client in your Authelia configuration file.",
},
"google": {
"google_consumer": {
"name": "Google",
"provider_type": "oidc",
"issuer_url": "https://accounts.google.com",
"scopes": "openid profile email",
"icon": "fa-google",
"user_mapping": {
"username": ["email"],
"email": ["email"],
"name": ["name"]
},
"user_mapping": {"username": ["email"], "email": ["email"], "name": ["name"]},
"description": "Google OAuth 2.0",
"configuration_notes": "Create OAuth 2.0 credentials in Google Cloud Console."
"configuration_notes": "Create OAuth 2.0 credentials in Google Cloud Console.",
},
"microsoft": {
"name": "Microsoft",
"microsoft_entra": {
"name": "Microsoft Entra ID",
"provider_type": "oidc",
"issuer_url": "https://login.microsoftonline.com/{tenant}/v2.0",
"scopes": "openid profile email",
@@ -70,28 +67,48 @@ IDP_TEMPLATES = {
"user_mapping": {
"username": ["preferred_username", "email"],
"email": ["email"],
"name": ["name"]
"name": ["name"],
},
"description": "Microsoft Azure AD / Entra ID",
"configuration_notes": "Replace {tenant} with your tenant ID or 'common'. Register an app in Azure Portal."
}
"description": "Microsoft Entra ID (Azure AD) - Enterprise Accounts",
"configuration_notes": "Replace {tenant} with your tenant ID (for single tenant) or 'organizations' (for multi-tenant). Register an app in Azure Portal under 'App registrations'.",
},
"microsoft_consumer": {
"name": "Microsoft Account",
"provider_type": "oidc",
"issuer_url": "https://login.microsoftonline.com/consumers/v2.0",
"scopes": "openid profile email",
"icon": "fa-microsoft",
"user_mapping": {
"username": ["preferred_username", "email"],
"email": ["email"],
"name": ["name"],
},
"description": "Microsoft Account - Consumer Accounts (Outlook, Hotmail, Xbox)",
"configuration_notes": "For personal Microsoft accounts only (@outlook.com, @hotmail.com, @live.com, Xbox accounts). Register an app in Azure Portal with 'Accounts in any organizational directory and personal Microsoft accounts' option.",
},
}
def get_idp_templates() -> List[idp_schema.IdentityProviderTemplate]:
"""
Retrieve a list of identity provider templates.
Retrieve a list of identity provider templates, excluding specific providers.
Returns:
List[idp_schema.IdentityProviderTemplate]: A list of IdentityProviderTemplate instances,
each representing a predefined identity provider template.
List[idp_schema.IdentityProviderTemplate]:
A list of IdentityProviderTemplate objects for all identity providers
except 'microsoft_consumer', 'microsoft_entra', and 'google_consumer'.
"""
templates = []
for template_id, template_data in IDP_TEMPLATES.items():
if (
template_id == "microsoft_consumer"
or template_id == "microsoft_entra"
or template_id == "google_consumer"
):
continue
templates.append(
idp_schema.IdentityProviderTemplate(
template_id=template_id,
**template_data
template_id=template_id, **template_data
)
)
return templates

View File

@@ -1,11 +1,9 @@
{
"addProviderButton": "Add Provider",
"addProviderButton": "Add identity provider",
"enabledBadge": "Enabled",
"disabledBadge": "Disabled",
"oidcType": "OpenID Connect",
"oauth2Type": "OAuth 2.0",
"enableButton": "Enable",
"disableButton": "Disable",
"editButton": "Edit",
"deleteButton": "Delete",
"infoAlert": "Identity providers allow users to authenticate using external services like Google, Microsoft, Keycloak, or Authentik.",

View File

@@ -92,6 +92,6 @@ export const identityProviders = {
* @param {string} slug - The provider slug
*/
initiateLogin(slug) {
window.location.href = `/api/v1/idp/login/${slug}`
window.location.href = `${window.env.ENDURAIN_HOST}/api/v1/public/idp/login/${slug}`
}
}

View File

@@ -7,6 +7,47 @@ import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Core Vue framework and state management
'vue-vendor': ['vue', 'vue-router', 'pinia'],
// Large charting library - only needed on stats/activity pages
'chart': ['chart.js', 'chartjs-plugin-datalabels'],
// Map library - only needed on activity detail pages with GPS data
'leaflet': ['leaflet'],
// UI framework - used across the app
'bootstrap': ['bootstrap'],
// FontAwesome icons - used throughout the app
'fontawesome': [
'@fortawesome/fontawesome-svg-core',
'@fortawesome/free-solid-svg-icons',
'@fortawesome/free-regular-svg-icons',
'@fortawesome/free-brands-svg-icons',
'@fortawesome/vue-fontawesome'
],
// Date/time utilities and notifications
'utils': ['luxon', 'notivue', 'vue-i18n']
}
}
}
},
server: {
proxy: {
// Proxy all /api requests to the backend server during development
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false
}
}
},
plugins: [
vue(),
VitePWA({
@@ -62,6 +103,7 @@ export default defineConfig({
skipWaiting: true,
sourcemap: false,
navigateFallback: '/',
navigateFallbackDenylist: [/^\/api\//],
runtimeCaching: [
{
urlPattern: ({ url }) => /^\/api\/v1(?:\/|$)/.test(url.pathname),