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:
+3
-3
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user