fix: Improve OAuth setup UX and resolve container stalling issue

- Fix container stalling during OAuth config save by making network requests non-blocking
- Add explicit callback URI documentation with provider-specific examples
- Enhance OAuth error messages with specific field names and actionable guidance
- Add UI warning when switching from OAuth to standard auth about password requirements
- Improve OAuth testing feedback with detailed endpoint validation
- Fix translation compatibility issues in error messages
- Standardize documentation placeholder domains
- Add comprehensive troubleshooting guide for common OAuth issues

Issues/feedback on new OAUTH setup
Fixes #613
This commit is contained in:
crocodilestick
2025-09-12 21:09:46 +02:00
parent 4453fd939c
commit 276e7b29ca
4 changed files with 124 additions and 29 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
CONTRIBUTORS
This file is automatically generated. DO NOT EDIT MANUALLY.
Generated on: 2025-09-12T14:51:00.876867Z
Generated on: 2025-09-12T19:09:48.157596Z
Upstream project: https://github.com/janeczku/calibre-web
Fork project (Calibre-Web Automated, since 2024): https://github.com/crocodilestick/calibre-web-automated
@@ -296,10 +296,9 @@ Copyright (C) 2024-2025 Calibre-Web Automated contributors
- ytilis (1 commits)
- zelazna (1 commits)
- zhiyue (1 commits)
# Fork Contributors (crocodilestick/calibre-web-automated)
- crocodilestick (633 commits)
- crocodilestick (645 commits)
- jmarmstrong1207 (73 commits)
- demitrix (30 commits)
- sirwolfgang (22 commits)
@@ -336,6 +335,7 @@ Copyright (C) 2024-2025 Calibre-Web Automated contributors
- have-a-boy (1 commits)
- Hobogrammer (anon) (1 commits)
- HotGarbo (1 commits)
- imajes (1 commits)
- InsideTheVoid (1 commits)
- ivantrejo41 (1 commits)
- jack1lee1995 (anon) (1 commits)
+64 -19
View File
@@ -1202,15 +1202,21 @@ def _configuration_oauth_helper(to_save):
# If metadata URL is provided, try to fetch endpoints
if metadata_url:
try:
resp = requests.get(metadata_url, timeout=5, verify=constants.OAUTH_SSL_STRICT)
resp = requests.get(metadata_url, timeout=3, verify=constants.OAUTH_SSL_STRICT)
if resp.status_code == 200:
data = resp.json()
update["oauth_base_url"] = data.get("issuer", "")
update["oauth_authorize_url"] = data.get("authorization_endpoint", "")
update["oauth_token_url"] = data.get("token_endpoint", "")
update["oauth_userinfo_url"] = data.get("userinfo_endpoint", "")
else:
log.warning(f"Failed to fetch OAuth metadata: HTTP {resp.status_code}")
except requests.exceptions.Timeout:
log.warning("OAuth metadata fetch timed out - configuration saved but endpoints not auto-discovered")
except requests.exceptions.RequestException as ex:
log.warning(f"Failed to fetch OAuth metadata: {ex}")
except Exception as ex:
return False, _configuration_result(_('Unable to fetch OAuth metadata from URL.'))
log.error(f"Unexpected error fetching OAuth metadata: {ex}")
# Handle manual server URL (fallback or override)
elif to_save["config_generic_oauth_server_url"] != element["oauth_base_url"]:
@@ -1219,7 +1225,7 @@ def _configuration_oauth_helper(to_save):
try:
resp = requests.get(
os.path.join(update["oauth_base_url"], ".well-known/openid-configuration"),
timeout=5,
timeout=3,
verify=constants.OAUTH_SSL_STRICT
)
if resp.status_code == 200:
@@ -1227,8 +1233,14 @@ def _configuration_oauth_helper(to_save):
update["oauth_authorize_url"] = data.get("authorization_endpoint", "")
update["oauth_token_url"] = data.get("token_endpoint", "")
update["oauth_userinfo_url"] = data.get("userinfo_endpoint", "")
else:
log.warning(f"Failed to fetch OIDC configuration: HTTP {resp.status_code}")
except requests.exceptions.Timeout:
log.warning("OIDC configuration fetch timed out - configuration saved but endpoints not auto-discovered")
except requests.exceptions.RequestException as ex:
log.warning(f"Failed to fetch OIDC configuration: {ex}")
except Exception as ex:
return False, _configuration_result(_('Unable to fetch OpenID configuration.'))
log.error(f"Unexpected error fetching OIDC configuration: {ex}")
# Handle manual endpoint URLs if metadata URL is not used
if not metadata_url:
@@ -2279,20 +2291,39 @@ def test_oidc():
try:
response = requests.get(discovery_url, timeout=5, verify=constants.OAUTH_SSL_STRICT)
response.raise_for_status()
# Try to parse the JSON to make sure it's valid
response.json()
return json.dumps({'success': True, 'message': _('Connection successful!')})
# Try to parse the JSON and extract useful information
oidc_config = response.json()
# Extract key endpoints for validation
endpoints = []
if 'authorization_endpoint' in oidc_config:
endpoints.append('authorization')
if 'token_endpoint' in oidc_config:
endpoints.append('token')
if 'userinfo_endpoint' in oidc_config:
endpoints.append('userinfo')
endpoint_info = " Found endpoints: " + ', '.join(endpoints) + "." if endpoints else ""
return json.dumps({
'success': True,
'message': _('Connection successful! OIDC discovery endpoint is accessible.%(endpoints)s',
endpoints=endpoint_info)
})
except requests.exceptions.HTTPError as e:
return json.dumps({'success': False, 'message': _('Connection failed: Server returned status code %(code)s', code=e.response.status_code)}), 200
if e.response.status_code == 404:
return json.dumps({'success': False, 'message': _('Connection failed: OIDC discovery endpoint not found (404). Check if the base URL is correct.')}), 200
else:
return json.dumps({'success': False, 'message': _('Connection failed: Server returned status code %(code)s', code=e.response.status_code)}), 200
except requests.exceptions.ConnectionError:
return json.dumps({'success': False, 'message': _('Connection failed: Could not connect to server.')}), 200
return json.dumps({'success': False, 'message': _('Connection failed: Could not connect to server. Check the URL and network connectivity.')}), 200
except requests.exceptions.Timeout:
return json.dumps({'success': False, 'message': _('Connection failed: Request timed out.')}), 200
return json.dumps({'success': False, 'message': _('Connection failed: Request timed out. The server may be slow or unreachable.')}), 200
except ValueError:
return json.dumps({'success': False, 'message': _('Connection failed: Invalid JSON in response.')}), 200
return json.dumps({'success': False, 'message': _('Connection failed: Server returned invalid JSON. This may not be an OIDC endpoint.')}), 200
except Exception as e:
log.error("OIDC test connection failed: %s", e)
return json.dumps({'success': False, 'message': _('An unknown error occurred.')}), 200
return json.dumps({'success': False, 'message': _('Connection failed: %(error)s', error=str(e))}), 200
@admi.route("/admin/test_metadata", methods=["POST"])
@@ -2318,17 +2349,31 @@ def test_metadata():
if missing_fields:
return json.dumps({
'success': False,
'message': _('Metadata is missing required fields: %(fields)s', fields=', '.join(missing_fields))
'message': _('Metadata is missing required OIDC fields: %(fields)s. This may not be a valid OIDC metadata endpoint.',
fields=', '.join(missing_fields))
}), 200
# Count available OAuth endpoints for user feedback
oauth_endpoints = ['authorization_endpoint', 'token_endpoint', 'userinfo_endpoint',
'end_session_endpoint', 'introspection_endpoint', 'revocation_endpoint']
found_endpoints = [ep for ep in oauth_endpoints if ep in data]
endpoint_count = len(found_endpoints)
has_userinfo = 'userinfo_endpoint' in data
message = _('Metadata URL is valid! Found %(count)s OAuth endpoints.', count=endpoint_count)
if has_userinfo:
message += _(' User info endpoint is available.')
else:
message += _(' Note: User info endpoint not found - this may cause authentication issues.')
return json.dumps({
'success': True,
'message': _('Metadata URL is valid! Found %(count)s endpoints.', count=len([k for k in data.keys() if 'endpoint' in k]))
})
return json.dumps({'success': True, 'message': message})
except requests.exceptions.HTTPError as e:
return json.dumps({'success': False, 'message': _('Connection failed: Server returned status code %(code)s', code=e.response.status_code)}), 200
if e.response.status_code == 404:
return json.dumps({'success': False, 'message': _('Metadata URL not found (404). Please check the URL is correct.')}), 200
else:
return json.dumps({'success': False, 'message': _('Connection failed: Server returned status code %(code)s', code=e.response.status_code)}), 200
except requests.exceptions.ConnectionError:
return json.dumps({'success': False, 'message': _('Connection failed: Could not connect to server.')}), 200
return json.dumps({'success': False, 'message': _('Connection failed: Could not connect to metadata URL. Check the URL and network connectivity.')}), 200
except requests.exceptions.Timeout:
return json.dumps({'success': False, 'message': _('Connection failed: Request timed out.')}), 200
except ValueError:
+20 -6
View File
@@ -105,11 +105,13 @@ def register_user_from_generic_oauth():
userinfo = resp.json()
except requests.exceptions.RequestException as e:
log.error("Failed to fetch user info from generic OIDC provider: %s", e)
flash(_("Login failed: Could not connect to the user info endpoint."), category="error")
flash(_("Login failed: Could not connect to the OAuth provider's user info endpoint. "
"Please try again or contact your administrator."), category="error")
return None
except ValueError:
log.error("Failed to parse user info from generic OIDC provider.")
flash(_("Login failed: The OAuth provider returned an invalid user profile."), category="error")
flash(_("Login failed: The OAuth provider returned invalid user profile data. "
"Please contact your administrator."), category="error")
return None
@@ -121,8 +123,18 @@ def register_user_from_generic_oauth():
provider_user_id = userinfo.get('sub')
if not provider_username or not provider_user_id:
log.error(f"User info from OIDC provider is missing '{username_field}' or 'sub' field.")
flash(_("Login failed: User profile from provider is incomplete."), category="error")
missing_fields = []
if not provider_username:
missing_fields.append(username_field)
if not provider_user_id:
missing_fields.append("sub")
missing_fields_str = ', '.join(missing_fields)
log.error(f"User info from OIDC provider is missing required fields: {missing_fields_str}. "
f"Check your OAuth scopes and field mappings.")
flash(_("Login failed: OAuth provider response is missing required fields: %(fields)s. "
"Please check your OAuth configuration or contact your administrator.",
fields=missing_fields_str), category="error")
return None
provider_username = str(provider_username)
@@ -226,8 +238,10 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
log.error_or_exception(ex)
ub.session.rollback()
else:
flash(_("Login failed, No User Linked With OAuth Account"), category="error")
log.info('Login failed, No User Linked With OAuth Account')
flash(_("Login failed: No user account is linked to your %(provider)s account. "
"Please contact your administrator to create an account or link your existing account.",
provider=provider_name), category="error")
log.info('Login failed, No User Linked With OAuth Account for provider %s', provider_name)
return redirect(url_for('web.login'))
# return redirect(url_for('web.login'))
# if config.config_public_reg:
+37 -1
View File
@@ -1,7 +1,29 @@
{% extends "layout.html" %}
{% block flash %}
<div id="spinning_success" class="row-fluid text-center" style="display:none;">
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/image <select name="config_login_type" id="config_login_type" class="form-control" data-controlall="login-settings">
<option value="0" {% if config.config_login_type == 0 %}selected{% endif %}>{{_('Use Standard Authentication')}}</option>
{% if feature_support['ldap'] %}
<option value="1" {% if config.config_login_type == 1 %}selected{% endif %}>{{_('Use LDAP Authentication')}}</option>
{% endif %}
{% if feature_support['oauth'] %}
<option value="2" {% if config.config_login_type == 2 %}selected{% endif %}>{{_('Use OAuth')}}</option>
{% endif %}
</select>
<!-- Authentication switching warning -->
{% if feature_support['oauth'] %}
<div id="auth-switch-warning" class="alert alert-warning" style="display: none; margin-top: 10px;">
<strong>⚠️ Warning:</strong>
<p>Users created via OAuth authentication do not have passwords set. If you switch from OAuth to Standard Authentication, these users will be unable to log in until you manually set passwords for them.</p>
<p>Before switching:</p>
<ul>
<li>Identify which users were created via OAuth</li>
<li>Set passwords for these users in the User Management section</li>
<li>Notify affected users about the authentication change</li>
</ul>
</div>
{% endif %}-icon.gif') }}"/></div>
</div>
{% endblock %}
@@ -561,6 +583,20 @@
<script>
$(document).ready(function() {
// Check for authentication switching warning
$('#config_login_type').on('change', function() {
var currentAuthType = $(this).val();
var warningDiv = $('#auth-switch-warning');
// Show warning when switching TO Standard Authentication (0)
// This warns about OAuth users who might lose access
if (currentAuthType === '0') {
warningDiv.show();
} else {
warningDiv.hide();
}
});
$('#test_oidc_connection').on('click', function() {
var serverUrl = $('#config_generic_oauth_server_url_test').val() || $('#config_generic_oauth_server_url').val();
var resultSpan = $('#oidc_test_result');