mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-28 03:39:32 +00:00
OAuth/OIDC Login Support added - tested in dev local
This commit is contained in:
parent
295cfa634c
commit
43520222a1
16 changed files with 458 additions and 181 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
---
|
||||
## [v3.0.1] - 2025-09-26
|
||||
|
||||
### Added
|
||||
- **OAuth/OIDC Login Support:** Added support for external authentication providers like Google and Authentik via a new, generic OpenID Connect system.
|
||||
- **OAuth Documentation:** Created a new help document for setting up OAuth providers.
|
||||
|
||||
### Changed
|
||||
- **Redesigned Login Page:** The login page has been updated with a modern design, provider logos, and a unified flow for both password and OAuth logins.
|
||||
- **Improved Usability:** The settings page now displays the required Callback URL for OAuth providers, and DockFlare can discover its public hostname from Docker labels.
|
||||
|
||||
### Fixed
|
||||
- Resolved multiple issues with the new OAuth feature, including `redirect_uri_mismatch` errors, a broken "Edit" button, and several backend bugs.
|
||||
|
||||
---
|
||||
## [v3.0.0] - 2025-09-25
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ try:
|
|||
logging.info("Successfully connected to Docker daemon.")
|
||||
except APIError as e:
|
||||
logging.error(f"FATAL: Docker API error during initial connection: {e}")
|
||||
docker_client = None # Ensure it's None on APIError too
|
||||
docker_client = None
|
||||
except Exception as e:
|
||||
logging.error(f"FATAL: Failed to connect to Docker daemon: {e}")
|
||||
docker_client = None
|
||||
|
|
@ -129,17 +129,20 @@ def create_app():
|
|||
elif oauth_providers:
|
||||
return redirect(url_for('web.login'))
|
||||
else:
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('web.login'))
|
||||
|
||||
# Custom user loader that exempts API routes from authentication checks
|
||||
@login_manager.request_loader
|
||||
def load_user_from_request(request):
|
||||
"""Load user from request - bypass authentication for API endpoints"""
|
||||
# For API v2 endpoints, don't require Flask-Login authentication
|
||||
if request.endpoint and request.endpoint.startswith('api_v2.'):
|
||||
# Create a dummy user to satisfy Flask-Login for API endpoints
|
||||
"""Load user from request - bypass session auth for designated API endpoints."""
|
||||
|
||||
if request.path.startswith('/api/v2/auth/'):
|
||||
return None
|
||||
|
||||
elif request.endpoint and request.endpoint.startswith('api_v2.'):
|
||||
from app.core.user import User
|
||||
return User('api_user')
|
||||
|
||||
return None
|
||||
|
||||
@login_manager.user_loader
|
||||
|
|
@ -181,7 +184,7 @@ def create_app():
|
|||
logging.info("Web blueprint registered.")
|
||||
|
||||
from .web.api_v2_routes import api_v2_bp
|
||||
# Exclude the API blueprint from CSRF protection
|
||||
|
||||
csrf.exempt(api_v2_bp)
|
||||
app_instance.register_blueprint(api_v2_bp)
|
||||
logging.info("API v2 blueprint registered.")
|
||||
|
|
@ -191,9 +194,6 @@ def create_app():
|
|||
app_instance.register_blueprint(setup_bp)
|
||||
logging.info("Setup blueprint registered.")
|
||||
|
||||
from .web.auth_routes import auth_bp
|
||||
app_instance.register_blueprint(auth_bp)
|
||||
logging.info("Auth blueprint registered.")
|
||||
|
||||
from .web.help_routes import help_bp
|
||||
app_instance.register_blueprint(help_bp)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import os
|
|||
import logging
|
||||
|
||||
# --- DockFlare Version ---
|
||||
APP_VERSION = "v3.0"
|
||||
APP_VERSION = "v3.0.1"
|
||||
# --- web: https://dockflare.app ---
|
||||
# --- github: https://github.com/ChrispyBacon-dev/DockFlare ---
|
||||
|
||||
|
|
|
|||
|
|
@ -246,11 +246,38 @@ def main_application_entrypoint():
|
|||
logging.error(f"Could not decrypt credentials for provider {provider['name']}. Skipping.")
|
||||
continue
|
||||
|
||||
provider_type = provider.get('type')
|
||||
issuer_url = provider.get('issuer_url')
|
||||
|
||||
if provider_type == 'github':
|
||||
oauth_instance.register(
|
||||
name=provider['id'],
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
authorize_url='https://github.com/login/oauth/authorize',
|
||||
access_token_url='https://github.com/login/oauth/access_token',
|
||||
api_base_url='https://api.github.com/',
|
||||
client_kwargs={'scope': 'user:email'}
|
||||
)
|
||||
continue
|
||||
|
||||
if not issuer_url:
|
||||
if provider_type == 'google':
|
||||
issuer_url = 'https://accounts.google.com'
|
||||
else:
|
||||
logging.warning(f"Provider 'provider.get('name')' is of type 'provider_type' but is missing an issuer_url. It will be skipped.")
|
||||
continue
|
||||
|
||||
if not issuer_url.endswith('/'):
|
||||
issuer_url += '/'
|
||||
|
||||
metadata_url = f"{issuer_url}.well-known/openid-configuration"
|
||||
|
||||
oauth_instance.register(
|
||||
name=provider['id'],
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
server_metadata_url=f"https://accounts.google.com/.well-known/openid-configuration",
|
||||
server_metadata_url=metadata_url,
|
||||
client_kwargs={'scope': 'openid email profile'}
|
||||
)
|
||||
|
||||
|
|
@ -276,6 +303,20 @@ def main_application_entrypoint():
|
|||
load_state()
|
||||
logging.info("Initial state loading from file complete.")
|
||||
|
||||
if docker_client:
|
||||
try:
|
||||
container_id = os.getenv('HOSTNAME')
|
||||
if container_id:
|
||||
logging.info(f"Attempting to discover public hostname from container ID: {container_id}")
|
||||
container = docker_client.containers.get(container_id)
|
||||
hostname_label = container.labels.get('dockflare.hostname')
|
||||
if hostname_label:
|
||||
app.config['DOCKFLARE_PUBLIC_HOSTNAME'] = hostname_label
|
||||
config.DOCKFLARE_PUBLIC_HOSTNAME = hostname_label
|
||||
logging.info(f"Discovered public hostname from label: {hostname_label}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not discover public hostname from Docker label: {e}", exc_info=True)
|
||||
|
||||
if not docker_client:
|
||||
logging.error("Docker client is unavailable. Dockflare will operate with limited functionality.")
|
||||
if tunnel_state: tunnel_state["status_message"] = "Error: Docker client unavailable."
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,35 +0,0 @@
|
|||
{% extends "setup/base.html" %}
|
||||
|
||||
{% block setup_content %}
|
||||
<form method="POST" action="{{ url_for('auth.login') }}{% if request.args.next %}?next={{ request.args.next }}{% endif %}" class="space-y-4">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ form.username.label }}</span>
|
||||
</label>
|
||||
{{ form.username(class="input input-bordered w-full", placeholder="Your username") }}
|
||||
{% for error in form.username.errors %}
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ error }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ form.password.label }}</span>
|
||||
</label>
|
||||
{{ form.password(class="input input-bordered w-full", placeholder="Your password") }}
|
||||
{% for error in form.password.errors %}
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ error }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="card-actions mt-6">
|
||||
{{ form.submit(class="btn btn-primary w-full") }}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('auth.logout') }}" class="btn btn-ghost btn-circle" title="Logout">
|
||||
<a href="{{ url_for('web.logout') }}" class="btn btn-ghost btn-circle" title="Logout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
||||
</svg>
|
||||
|
|
|
|||
53
dockflare/app/templates/docs/OAuth-Provider-Setup.md
Normal file
53
dockflare/app/templates/docs/OAuth-Provider-Setup.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
## OAuth Provider Setup
|
||||
|
||||
DockFlare allows you to delegate user authentication to external providers using the OpenID Connect (OIDC) standard. This enables single sign-on (SSO) and allows you to integrate with identity providers like Google, Authentik, Okta, and more.
|
||||
|
||||
### Adding a New Provider
|
||||
|
||||
Follow these steps to add a new OIDC provider:
|
||||
|
||||
1. **Navigate to Settings:** From the main dashboard, go to the **Settings** page.
|
||||
2. **Locate OAuth Section:** Scroll down to the **OAuth Authentication** section.
|
||||
3. **Add Provider:** Click the **Add Provider** button to open the configuration modal.
|
||||
|
||||
You will be presented with the following fields:
|
||||
|
||||
* **Provider Type:** This is set to `OpenID Connect (OIDC)`, the modern standard for federated authentication.
|
||||
* **Issuer URL:** This is the most important field. It is the base URL of your OIDC provider, which DockFlare uses to automatically discover the provider's configuration. For example, `https://accounts.google.com` or `https://authentik.yourdomain.com/application/o/dockflare/`.
|
||||
* **Provider ID:** A short, unique, lowercase name for this provider (e.g., `google`, `authentik-corp`). This ID is used internally and in the callback URL.
|
||||
* **Display Name:** The user-friendly name that will appear on the login button (e.g., `Google`, `Corporate SSO`).
|
||||
* **Client ID:** The public identifier for the DockFlare application, which you will get from your OIDC provider's developer console.
|
||||
* **Client Secret:** The confidential secret for the DockFlare application, also from your OIDC provider's console.
|
||||
* **Enable Provider:** This checkbox allows you to enable or disable the provider at any time.
|
||||
|
||||
After filling in the details, click **Add Provider** to save.
|
||||
|
||||
### Finding Your Callback URL
|
||||
|
||||
Once you have added a provider, the required **Callback URL** (also known as an "Authorized redirect URI") will be displayed under the provider's entry on the Settings page.
|
||||
|
||||
You must copy this exact URL and add it to your provider's list of allowed callback URLs in their administration console.
|
||||
|
||||
---
|
||||
|
||||
### Example: Setting up Google
|
||||
|
||||
Here is a quick guide to configuring Google as an OAuth provider.
|
||||
|
||||
1. **Go to Google Cloud Console:** Navigate to the [APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials) page.
|
||||
2. **Create Credentials:** Click **+ CREATE CREDENTIALS** and select **OAuth client ID**.
|
||||
3. **Configure Application:**
|
||||
* Set the **Application type** to **Web application**.
|
||||
* Give it a name (e.g., "DockFlare").
|
||||
4. **Add Redirect URI:**
|
||||
* Under **Authorized redirect URIs**, click **+ ADD URI**.
|
||||
* Enter the callback URL provided by DockFlare. It will look like this: `https://your-dockflare-domain.com/auth/google/callback`.
|
||||
5. **Create and Copy:** Click **CREATE**. A window will appear showing your **Client ID** and **Client Secret**. Copy these values.
|
||||
6. **Configure in DockFlare:**
|
||||
* **Issuer URL:** `https://accounts.google.com`
|
||||
* **Provider ID:** `google`
|
||||
* **Display Name:** `Google`
|
||||
* **Client ID:** `(Your Client ID from Google)`
|
||||
* **Client Secret:** `(Your Client Secret from Google)`
|
||||
|
||||
Save the provider in DockFlare, and you will be able to log in with your Google account.
|
||||
|
|
@ -19,6 +19,7 @@ This documentation provides comprehensive information for DockFlare. Whether you
|
|||
* [State-Persistence](State-Persistence.md)
|
||||
* **Configuration**
|
||||
* [Container Labels](Container-Labels.md)
|
||||
* [OAuth Provider Setup](OAuth-Provider-Setup.md)
|
||||
* **Usage Guide**
|
||||
* [Basic Usage (Single Domain)](Basic-Usage-Single-Domain.md)
|
||||
* [Using Multiple Domains (Indexed Labels)](Using-Multiple-Domains-Indexed-Labels.md)
|
||||
|
|
|
|||
|
|
@ -3,28 +3,49 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - DockFlare</title>
|
||||
<link href="{{ url_for('static', filename='css/output.css') }}" rel="stylesheet">
|
||||
<title>Sign In - DockFlare</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-base-200 flex items-center justify-center h-screen">
|
||||
<div class="card w-96 bg-base-100 shadow-xl">
|
||||
<body class="bg-base-200 min-h-screen flex items-center justify-center p-4">
|
||||
<div class="card w-full max-w-sm bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">DockFlare Login</h2>
|
||||
<div class="text-center mb-6">
|
||||
<img src="{{ url_for('static', filename='images/logo.gif') }}" alt="DockFlare Logo" class="h-16 mx-auto">
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="space-y-2 mb-4">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category if category in ['error', 'success', 'warning', 'info'] else 'info' }} mt-4">
|
||||
<span>{{ message }}</span>
|
||||
<div class="alert alert-{{ category if category in ['success', 'error', 'warning', 'info'] else 'info' }} shadow-lg text-sm">
|
||||
<div>
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if oauth_providers %}
|
||||
<div class="flex flex-col w-full border-opacity-50">
|
||||
{% set logos = {
|
||||
'google': '<svg class="w-5 h-5" aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 488 512"><path fill="currentColor" d="M488 261.8C488 403.3 381.5 512 244 512 111.8 512 0 400.2 0 261.8 0 123.8 111.8 14 244 14c77.3 0 143.3 31.4 191.1 82.1l-73.3 73.3c-24.5-23.2-58.3-37.2-97.8-37.2-80.3 0-146.1 65.7-146.1 146.1s65.8 146.1 146.1 146.1c92.8 0 131.3-74.4 136.8-109.9H244v-89.9h239.1c4.9 26.9 7.9 54.9 7.9 84.9z"></path></svg>',
|
||||
'github': '<svg class="w-5 h-5" aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3.3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.3zm37.4 2.6c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.3zM496 256c0 137-111 248-248 248S0 393 0 256 111 8 248 8s248 111 248 248zM248 40c-118.7 0-216 96.1-216 216 0 94.8 61.5 175.2 146.5 203.8 10.8 2.3 14.6-4.6 14.6-10.6 0-5.2-.2-21.5-.3-42.1-60 13.1-72.7-28.7-72.7-28.7-9.8-24.9-24-31.5-24-31.5-19.5-13.3 1.5-13.1 1.5-13.1 21.5 1.5 32.8 22.1 32.8 22.1 19.1 32.8 50.1 23.3 62.3 17.8 2-13.9 7.5-23.3 13.9-28.7-47.5-5.5-97.5-23.6-97.5-105.8 0-23.3 8.4-42.4 22.1-57.4-2.3-5.5-9.6-27.2 2.6-56.5 0 0 18-5.8 59 22.1 17.1-4.8 35.5-7.2 53.9-7.2 18.4 0 36.8 2.4 53.9 7.2 41-27.9 59-22.1 59-22.1 12.2 29.3 4.9 51 2.6 56.5 13.7 15 22.1 34.1 22.1 57.4 0 82.4-50.1 100.3-97.7 105.6 7.7 6.5 14.6 19.5 14.6 39.2 0 28.5-.3 51.5-.3 58.5 0 6.1 3.8 12.9 14.8 10.6 85-28.6 146.5-109 146.5-203.8 0-119.9-97.3-216-216-216z"></path></svg>',
|
||||
'microsoft': '<svg class="w-5 h-5" aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M0 32h214.6v214.6H0V32zm233.4 0H448v214.6H233.4V32zM0 265.4h214.6V480H0V265.4zm233.4 0H448V480H233.4V265.4z"></path></svg>'
|
||||
} %}
|
||||
{% for provider in oauth_providers %}
|
||||
<a href="{{ url_for('web.login_provider', provider_id=provider.id) }}" class="btn btn-outline mb-2">
|
||||
Login with {{ provider.name }}
|
||||
<a href="{{ url_for('web.login_provider', provider_id=provider.id) }}" class="btn btn-outline justify-start w-full mb-2 font-medium">
|
||||
<span class="w-6 text-center">
|
||||
{{ logos[provider.type] | safe if provider.type in logos else '' }}
|
||||
</span>
|
||||
<span class="flex-grow text-center">
|
||||
Sign in with {{ provider.name }}
|
||||
</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if password_login_enabled %}
|
||||
|
|
@ -39,26 +60,26 @@
|
|||
<div class="form-control">
|
||||
{% if form %}
|
||||
{{ form.username.label(class="label") }}
|
||||
{{ form.username(class="input input-bordered", placeholder="Username") }}
|
||||
{{ form.username(class="input input-bordered w-full", placeholder="Username") }}
|
||||
{% else %}
|
||||
<label class="label">Username</label>
|
||||
<input type="text" name="username" class="input input-bordered" placeholder="Username" required>
|
||||
<label class="label"><span class="label-text">Username</span></label>
|
||||
<input type="text" name="username" class="input input-bordered w-full" placeholder="Username" required>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-control">
|
||||
{% if form %}
|
||||
{{ form.password.label(class="label") }}
|
||||
{{ form.password(class="input input-bordered", placeholder="Password") }}
|
||||
{{ form.password(class="input input-bordered w-full", placeholder="Password") }}
|
||||
{% else %}
|
||||
<label class="label">Password</label>
|
||||
<input type="password" name="password" class="input input-bordered" placeholder="Password" required>
|
||||
<label class="label"><span class="label-text">Password</span></label>
|
||||
<input type="password" name="password" class="input input-bordered w-full" placeholder="Password" required>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-control mt-6">
|
||||
{% if form %}
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
{{ form.submit(class="btn btn-primary w-full") }}
|
||||
{% else %}
|
||||
<input type="submit" value="Login" class="btn btn-primary">
|
||||
<input type="submit" value="Login" class="btn btn-primary w-full">
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
<script>
|
||||
(function() {
|
||||
const seconds = {{ countdown_seconds }};
|
||||
const redirectUrl = "{{ url_for('auth.login') }}";
|
||||
const redirectUrl = "{{ url_for('web.login') }}";
|
||||
const pingUrl = "{{ url_for('web.ping') }}";
|
||||
const countdownEl = document.getElementById('countdown');
|
||||
const progressEl = document.getElementById('progress');
|
||||
|
|
|
|||
|
|
@ -715,14 +715,16 @@
|
|||
(function() {
|
||||
let authData = null;
|
||||
|
||||
// Helper to get CSRF token
|
||||
function getCsrfToken() {
|
||||
const csrfInput = document.querySelector('input[name="csrf_token"]');
|
||||
return csrfInput ? csrfInput.value : '{{ csrf_token() }}';
|
||||
}
|
||||
|
||||
async function loadAuthSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/v2/auth/settings', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
// No auth header needed, relies on session cookie
|
||||
const response = await fetch('/api/v2/auth/settings');
|
||||
|
||||
if (response.ok) {
|
||||
authData = await response.json();
|
||||
|
|
@ -730,6 +732,8 @@
|
|||
renderUsers();
|
||||
} else {
|
||||
console.error('Failed to load auth settings');
|
||||
const providerList = document.getElementById('provider-list');
|
||||
if(providerList) providerList.innerHTML = '<div class="text-center text-error py-8">Failed to load OAuth settings. Please check browser console for errors.</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading auth settings:', error);
|
||||
|
|
@ -738,23 +742,37 @@
|
|||
|
||||
function renderProviders() {
|
||||
const providerList = document.getElementById('provider-list');
|
||||
if (!providerList) return;
|
||||
if (!authData || !authData.providers || authData.providers.length === 0) {
|
||||
providerList.innerHTML = '<div class="text-center text-sm opacity-60 py-8">No OAuth providers configured. Click "Add Provider" to get started.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
providerList.innerHTML = authData.providers.map(provider => `
|
||||
providerList.innerHTML = authData.providers.map(provider => {
|
||||
let callbackUrlHtml = '';
|
||||
const publicHostname = '{{ DOCKFLARE_PUBLIC_HOSTNAME or "" }}';
|
||||
if (publicHostname) {
|
||||
const callbackUrl = `https://${publicHostname}/auth/${provider.id}/callback`;
|
||||
callbackUrlHtml = `
|
||||
<div class="text-xs opacity-80 mt-2 pt-2 border-t border-base-200">
|
||||
<strong class="font-semibold">Callback URL:</strong>
|
||||
<code class="bg-base-300 p-1 rounded text-xs select-all">${escapeHtml(callbackUrl)}</code>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="card bg-base-100 border">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="badge badge-${provider.enabled ? 'success' : 'warning'}">${provider.enabled ? 'Enabled' : 'Disabled'}</div>
|
||||
<div>
|
||||
<h5 class="font-semibold">${escapeHtml(provider.name)}</h5>
|
||||
<p class="text-sm opacity-70">${escapeHtml(provider.type)} · ID: ${escapeHtml(provider.id)}</p>
|
||||
<div class="flex-grow">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="badge badge-sm ${provider.enabled ? 'badge-success' : 'badge-warning'}">${provider.enabled ? 'Enabled' : 'Disabled'}</div>
|
||||
<h5 class="font-semibold">${escapeHtml(provider.name)}</h5>
|
||||
</div>
|
||||
<p class="text-sm opacity-70 mt-1">${escapeHtml(provider.type)} · ID: ${escapeHtml(provider.id)}</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<div class="flex flex-shrink-0 space-x-2 ml-4">
|
||||
<button class="btn btn-sm btn-outline" onclick="editProvider('${escapeHtml(provider.id)}')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
|
|
@ -767,13 +785,15 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${callbackUrlHtml}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
const userList = document.getElementById('user-list');
|
||||
if (!userList) return;
|
||||
if (!authData || !authData.users || authData.users.length === 0) {
|
||||
userList.innerHTML = '<div class="text-center text-sm opacity-60 py-8">No authorized users configured. Click "Add User" to authorize email addresses.</div>';
|
||||
return;
|
||||
|
|
@ -808,8 +828,121 @@
|
|||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function handleProviderTypeChange(form, isEdit = false) {
|
||||
const typeSelect = form.elements['provider_type'];
|
||||
const issuerInput = form.elements['issuer_url'];
|
||||
const issuerGroup = issuerInput.closest('.form-control');
|
||||
const nameInput = form.elements['provider_name'];
|
||||
const idInput = form.elements['provider_id'];
|
||||
|
||||
// Default state
|
||||
issuerGroup.style.display = 'none';
|
||||
issuerInput.required = false;
|
||||
|
||||
if (typeSelect.value === 'google') {
|
||||
// Field is hidden, but we still set the value
|
||||
issuerInput.value = 'https://accounts.google.com';
|
||||
if (!nameInput.value) nameInput.value = 'Google';
|
||||
if (!idInput.value && !isEdit) idInput.value = 'google';
|
||||
} else if (typeSelect.value === 'oidc') {
|
||||
// Show the field for generic OIDC
|
||||
issuerGroup.style.display = '';
|
||||
issuerInput.required = true;
|
||||
if (!isEdit) {
|
||||
issuerInput.value = '';
|
||||
nameInput.value = '';
|
||||
idInput.value = '';
|
||||
}
|
||||
} else if (typeSelect.value === 'github') {
|
||||
// Field is hidden
|
||||
if (!nameInput.value) nameInput.value = 'GitHub';
|
||||
if (!idInput.value && !isEdit) idInput.value = 'github';
|
||||
}
|
||||
}
|
||||
|
||||
window.showAddProviderModal = function() {
|
||||
document.getElementById('addProviderModal').showModal();
|
||||
const modal = document.getElementById('addProviderModal');
|
||||
const form = modal.querySelector('form');
|
||||
form.reset();
|
||||
handleProviderTypeChange(form, false);
|
||||
modal.showModal();
|
||||
};
|
||||
|
||||
window.editProvider = function(providerId) {
|
||||
const provider = authData.providers.find(p => p.id === providerId);
|
||||
if (!provider) {
|
||||
showToast('Provider not found.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('editProviderModal');
|
||||
const form = document.getElementById('editProviderForm');
|
||||
const issuerGroup = form.elements['issuer_url'].closest('.form-control');
|
||||
|
||||
// Handle visibility of issuer URL field based on the provider's type
|
||||
if (provider.type === 'github' || provider.type === 'google') {
|
||||
issuerGroup.style.display = 'none';
|
||||
form.elements['issuer_url'].required = false;
|
||||
} else { // oidc
|
||||
issuerGroup.style.display = '';
|
||||
form.elements['issuer_url'].required = true;
|
||||
}
|
||||
|
||||
form.elements['provider_type'].value = provider.type || 'oidc';
|
||||
form.elements['original_provider_id'].value = provider.id;
|
||||
form.elements['issuer_url'].value = provider.issuer_url || (provider.type === 'google' ? 'https://accounts.google.com' : '');
|
||||
form.elements['provider_id'].value = provider.id;
|
||||
form.elements['provider_name'].value = provider.name;
|
||||
form.elements['client_id'].value = provider.client_id || '';
|
||||
form.elements['client_id'].placeholder = 'OAuth Client ID';
|
||||
form.elements['enabled'].checked = provider.enabled;
|
||||
|
||||
form.elements['client_secret'].value = '';
|
||||
|
||||
modal.showModal();
|
||||
};
|
||||
|
||||
window.updateProvider = async function() {
|
||||
const form = document.getElementById('editProviderForm');
|
||||
const providerId = form.elements['original_provider_id'].value;
|
||||
|
||||
const providerData = {
|
||||
name: form.elements['provider_name'].value,
|
||||
enabled: form.elements['enabled'].checked,
|
||||
issuer_url: form.elements['issuer_url'].value
|
||||
};
|
||||
|
||||
const clientId = form.elements['client_id'].value;
|
||||
if (clientId) {
|
||||
providerData.client_id = clientId;
|
||||
}
|
||||
|
||||
const clientSecret = form.elements['client_secret'].value;
|
||||
if (clientSecret) {
|
||||
providerData.client_secret = clientSecret;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v2/auth/providers/${providerId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify(providerData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('editProviderModal').close();
|
||||
await loadAuthSettings();
|
||||
showToast('Provider updated successfully!', 'success');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.message || 'Failed to update provider', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Error updating provider', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
window.showAddUserModal = function() {
|
||||
|
|
@ -833,8 +966,8 @@
|
|||
const response = await fetch('/api/v2/auth/providers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify(providerData)
|
||||
});
|
||||
|
|
@ -866,8 +999,8 @@
|
|||
const response = await fetch('/api/v2/auth/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
|
@ -893,8 +1026,8 @@
|
|||
const response = await fetch(`/api/v2/auth/providers/${providerId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -916,8 +1049,8 @@
|
|||
const response = await fetch(`/api/v2/auth/users/${encodeURIComponent(userEmail)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -945,8 +1078,14 @@
|
|||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.masterApiKey) {
|
||||
loadAuthSettings();
|
||||
loadAuthSettings();
|
||||
|
||||
const addForm = document.getElementById('addProviderForm');
|
||||
if (addForm) {
|
||||
const addTypeSelect = addForm.elements['provider_type'];
|
||||
if (addTypeSelect) {
|
||||
addTypeSelect.addEventListener('change', () => handleProviderTypeChange(addForm, false));
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -962,13 +1101,22 @@
|
|||
<span class="label-text">Provider Type</span>
|
||||
</label>
|
||||
<select name="provider_type" class="select select-bordered" required>
|
||||
<option value="">Select Provider Type</option>
|
||||
<option value="google">Google</option>
|
||||
<option value="google" selected>Google</option>
|
||||
<option value="github">GitHub</option>
|
||||
<option value="microsoft">Microsoft</option>
|
||||
<option value="oidc">Generic OIDC / Authentik</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Issuer URL</span>
|
||||
</label>
|
||||
<input name="issuer_url" type="url" placeholder="https://authentik.example.com/application/o/dockflare/" class="input input-bordered" required />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">The OIDC provider's issuer URL.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Provider ID</span>
|
||||
|
|
@ -1017,6 +1165,75 @@
|
|||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Edit Provider Modal -->
|
||||
<dialog id="editProviderModal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">Edit OAuth Provider</h3>
|
||||
<form id="editProviderForm" class="space-y-4 mt-4">
|
||||
<input type="hidden" name="original_provider_id">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Provider Type</span>
|
||||
</label>
|
||||
<select name="provider_type" class="select select-bordered" required disabled>
|
||||
<option value="google">Google</option>
|
||||
<option value="github">GitHub</option>
|
||||
<option value="oidc">Generic OIDC / Authentik</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Issuer URL</span>
|
||||
</label>
|
||||
<input name="issuer_url" type="url" placeholder="https://authentik.example.com/application/o/dockflare/" class="input input-bordered" required />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Provider ID</span>
|
||||
</label>
|
||||
<input name="provider_id" type="text" placeholder="e.g. google" class="input input-bordered" required disabled />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Display Name</span>
|
||||
</label>
|
||||
<input name="provider_name" type="text" placeholder="e.g. Google" class="input input-bordered" required />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Client ID</span>
|
||||
</label>
|
||||
<input name="client_id" type="text" placeholder="OAuth Client ID" class="input input-bordered" required />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Client Secret</span>
|
||||
</label>
|
||||
<input name="client_secret" type="password" placeholder="Leave blank to keep existing secret" class="input input-bordered" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Enable Provider</span>
|
||||
<input name="enabled" type="checkbox" class="toggle" />
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button class="btn">Cancel</button>
|
||||
</form>
|
||||
<button class="btn btn-primary" onclick="updateProvider()">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<dialog id="addUserModal" class="modal">
|
||||
<div class="modal-box">
|
||||
|
|
|
|||
|
|
@ -69,6 +69,14 @@ _AGENT_ENDPOINT_ALLOWLIST = {
|
|||
'api_v2.agents_post_events',
|
||||
}
|
||||
|
||||
_UI_ENDPOINT_ALLOWLIST = {
|
||||
'api_v2.manage_auth_settings',
|
||||
'api_v2.manage_auth_providers',
|
||||
'api_v2.manage_auth_provider',
|
||||
'api_v2.manage_auth_users',
|
||||
'api_v2.manage_auth_user',
|
||||
}
|
||||
|
||||
|
||||
@api_v2_bp.before_request
|
||||
def _enforce_master_api_key():
|
||||
|
|
@ -78,11 +86,13 @@ def _enforce_master_api_key():
|
|||
if request.method == 'OPTIONS':
|
||||
return
|
||||
|
||||
# For agent endpoints in allowlist, skip all authentication (including Flask-Login)
|
||||
if endpoint in _AGENT_ENDPOINT_ALLOWLIST:
|
||||
return
|
||||
|
||||
# For all other API endpoints, ensure proper API authentication
|
||||
# For UI endpoints, rely on Flask-Login's session auth
|
||||
if endpoint in _UI_ENDPOINT_ALLOWLIST:
|
||||
return
|
||||
|
||||
expected_key = current_app.config.get('MASTER_API_KEY') or config.MASTER_API_KEY
|
||||
if not expected_key:
|
||||
logging.warning("MASTER_AUTH: Master API key not configured; rejecting %s", endpoint)
|
||||
|
|
@ -1991,8 +2001,11 @@ def manage_auth_settings():
|
|||
users = config_data.get('authorized_users', [])
|
||||
|
||||
for p in providers:
|
||||
p.pop('client_id', None)
|
||||
p.pop('client_secret', None)
|
||||
try:
|
||||
p['client_id'] = fernet.decrypt(p['client_id'].encode()).decode()
|
||||
except Exception:
|
||||
p['client_id'] = '(could not decrypt)'
|
||||
|
||||
return jsonify({
|
||||
"settings": auth_settings,
|
||||
|
|
@ -2036,6 +2049,10 @@ def manage_auth_providers():
|
|||
if not all(field in data for field in required_fields):
|
||||
return jsonify({"error": "missing_required_fields"}), 400
|
||||
|
||||
provider_type = data.get('type')
|
||||
if provider_type in ['oidc', 'google'] and not data.get('issuer_url'):
|
||||
return jsonify({"error": "issuer_url_required_for_oidc"}), 400
|
||||
|
||||
if not config_data.get('auth_providers'):
|
||||
config_data['auth_providers'] = []
|
||||
|
||||
|
|
@ -2050,6 +2067,7 @@ def manage_auth_providers():
|
|||
'id': data['id'],
|
||||
'name': data['name'],
|
||||
'type': data['type'],
|
||||
'issuer_url': data.get('issuer_url'),
|
||||
'client_id': encrypted_client_id,
|
||||
'client_secret': encrypted_client_secret,
|
||||
'enabled': data.get('enabled', True)
|
||||
|
|
@ -2088,6 +2106,8 @@ def manage_auth_provider(provider_id):
|
|||
provider['client_id'] = fernet.encrypt(data['client_id'].encode()).decode()
|
||||
if 'client_secret' in data:
|
||||
provider['client_secret'] = fernet.encrypt(data['client_secret'].encode()).decode()
|
||||
if 'issuer_url' in data:
|
||||
provider['issuer_url'] = data['issuer_url']
|
||||
|
||||
if not _save_encrypted_config(config_data, fernet):
|
||||
return jsonify({"error": "failed_to_save_config"}), 500
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
# DockFlare: Automates Cloudflare Tunnel ingress from Docker labels.
|
||||
# Copyright (C) 2025 ChrispyBacon-Dev <https://github.com/ChrispyBacon-dev/DockFlare>
|
||||
#
|
||||
# This program is free software: you can redistribute and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# dockflare/app/web/utils.py
|
||||
import logging
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, abort
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, SubmitField
|
||||
from wtforms.validators import DataRequired
|
||||
from werkzeug.security import check_password_hash
|
||||
from app.core.user import User
|
||||
from app.web.utils import is_safe_url
|
||||
|
||||
auth_bp = Blueprint('auth', __name__, url_prefix='/auth', template_folder='../templates')
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
"""Form for the login page."""
|
||||
username = StringField('Username', validators=[DataRequired()])
|
||||
password = PasswordField('Password', validators=[DataRequired()])
|
||||
submit = SubmitField('Login')
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""Handles the user login process."""
|
||||
if current_app.config.get('DISABLE_PASSWORD_LOGIN'):
|
||||
return redirect(url_for('web.status_page'))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('web.status_page'))
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
username = form.username.data
|
||||
password = form.password.data
|
||||
|
||||
stored_username = current_app.config.get('DOCKFLARE_USERNAME')
|
||||
stored_hash = current_app.config.get('DOCKFLARE_PASSWORD_HASH')
|
||||
|
||||
if username == stored_username and stored_hash and check_password_hash(stored_hash, password):
|
||||
user = User(username)
|
||||
login_user(user)
|
||||
|
||||
next_page = request.args.get('next')
|
||||
if next_page and not is_safe_url(next_page):
|
||||
return abort(400)
|
||||
|
||||
return redirect(next_page or url_for('web.status_page'))
|
||||
else:
|
||||
flash('Invalid username or password.', 'danger')
|
||||
|
||||
return render_template('auth/login.html', form=form, title="Login")
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
def logout():
|
||||
"""Handles the user logout process."""
|
||||
logout_user()
|
||||
flash('You have been logged out.', 'success')
|
||||
if current_app.config.get('DISABLE_PASSWORD_LOGIN'):
|
||||
return redirect(url_for('web.status_page'))
|
||||
return redirect(url_for('auth.login'))
|
||||
|
|
@ -187,9 +187,22 @@ def gating_logic():
|
|||
|
||||
@bp.before_app_request
|
||||
def detect_protocol_bp():
|
||||
|
||||
forwarded_proto = request.headers.get('X-Forwarded-Proto', '').lower()
|
||||
current_app.config['PREFERRED_URL_SCHEME'] = 'https' if forwarded_proto == 'https' or request.is_secure else 'http'
|
||||
if forwarded_proto == 'https':
|
||||
current_app.config['PREFERRED_URL_SCHEME'] = 'https'
|
||||
return
|
||||
|
||||
cf_visitor = request.headers.get('Cf-Visitor')
|
||||
if cf_visitor:
|
||||
try:
|
||||
visitor_data = json.loads(cf_visitor)
|
||||
if visitor_data.get('scheme') == 'https':
|
||||
current_app.config['PREFERRED_URL_SCHEME'] = 'https'
|
||||
return
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
current_app.config['PREFERRED_URL_SCHEME'] = 'https' if request.is_secure else 'http'
|
||||
|
||||
@bp.after_app_request
|
||||
def add_security_headers_bp(response):
|
||||
|
|
@ -232,6 +245,8 @@ def inject_protocol_bp():
|
|||
if current_user.is_authenticated:
|
||||
master_key_value = current_app.config.get('MASTER_API_KEY')
|
||||
|
||||
public_hostname = current_app.config.get('DOCKFLARE_PUBLIC_HOSTNAME')
|
||||
|
||||
return {
|
||||
'protocol': preferred_scheme,
|
||||
'is_https': preferred_scheme == 'https',
|
||||
|
|
@ -241,7 +256,8 @@ def inject_protocol_bp():
|
|||
'app_version': config.APP_VERSION,
|
||||
'master_api_key': master_key_value,
|
||||
'oauth_enabled': oauth_enabled,
|
||||
'current_user_auth_method': getattr(current_user, 'auth_method', None) if current_user.is_authenticated else None
|
||||
'current_user_auth_method': getattr(current_user, 'auth_method', None) if current_user.is_authenticated else None,
|
||||
'DOCKFLARE_PUBLIC_HOSTNAME': public_hostname
|
||||
}
|
||||
|
||||
@bp.route('/')
|
||||
|
|
@ -1863,7 +1879,13 @@ def login_provider(provider_id):
|
|||
session['oauth_state'] = state_token
|
||||
|
||||
from app import oauth
|
||||
callback_url = url_for('web.auth_callback', provider_id=provider_id, _external=True)
|
||||
public_hostname = current_app.config.get('DOCKFLARE_PUBLIC_HOSTNAME')
|
||||
if public_hostname:
|
||||
path = url_for('web.auth_callback', provider_id=provider_id)
|
||||
callback_url = f"https://{public_hostname}{path}"
|
||||
logging.info(f"Constructed OAuth callback URL using public hostname: {callback_url}")
|
||||
else:
|
||||
callback_url = url_for('web.auth_callback', provider_id=provider_id, _external=True)
|
||||
return oauth.create_client(provider_id).authorize_redirect(callback_url, state=state_token)
|
||||
|
||||
@bp.route('/auth/<provider_id>/callback')
|
||||
|
|
@ -1911,10 +1933,7 @@ def logout():
|
|||
auth_method = getattr(current_user, 'auth_method', 'password')
|
||||
logout_user()
|
||||
|
||||
if auth_method == 'oauth':
|
||||
flash('You have been logged out of DockFlare. You may still be logged into your OAuth provider.', 'info')
|
||||
else:
|
||||
flash('You have been logged out.', 'success')
|
||||
flash('You have been logged out.', 'success')
|
||||
|
||||
if current_app.config.get('DISABLE_PASSWORD_LOGIN'):
|
||||
oauth_providers = current_app.config.get('OAUTH_PROVIDERS', [])
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ flask-login==0.6.3
|
|||
# via -r dockflare/requirements.in
|
||||
flask-wtf==1.2.2
|
||||
# via -r dockflare/requirements.in
|
||||
Authlib==1.3.0
|
||||
Authlib==1.6.4
|
||||
# via OAuth2 integration
|
||||
idna==3.10
|
||||
# via requests
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue