mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
refactoring
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,9 +1,11 @@
|
|||||||
/application/config/database.php
|
|
||||||
/application/config/config.php
|
/application/config/config.php
|
||||||
|
/application/config/database.php
|
||||||
|
/application/config/sso.php
|
||||||
/application/config/redis.php
|
/application/config/redis.php
|
||||||
/application/config/memcached.php
|
/application/config/memcached.php
|
||||||
/application/config/**/config.php
|
/application/config/**/config.php
|
||||||
/application/config/**/database.php
|
/application/config/**/database.php
|
||||||
|
/application/config/**/sso.php
|
||||||
/application/config/**/redis.php
|
/application/config/**/redis.php
|
||||||
/application/config/**/memcached.php
|
/application/config/**/memcached.php
|
||||||
/application/logs/*
|
/application/logs/*
|
||||||
|
|||||||
@@ -109,37 +109,18 @@ $config['auth_mode'] = '3';
|
|||||||
$config['auth_level'][3] = 'Operator';
|
$config['auth_level'][3] = 'Operator';
|
||||||
$config['auth_level'][99] = 'Administrator';
|
$config['auth_level'][99] = 'Administrator';
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Third-Party Authentication (SSO)
|
| Third-Party Authentication (SSO)
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This section defines settings for third-party authentication over an external identity provider.
|
| Enable SSO support via a trusted HTTP header containing a JWT access token.
|
||||||
| When enabled, Wavelog will look for a specific HTTP header containing an access token, which can be used to authenticate users.
|
| When enabled, a sso.php config file is required (see sso.sample.php).
|
||||||
| This is particularly useful in environments where users are already authenticated through another system, such as a corporate SSO or an identity provider.
|
|
||||||
| Please refer to the documentation of Wavelog for more details on how to set up and use this feature.
|
|
||||||
| Link to documentation: https://docs.wavelog.org/admin-guide/configuration/third-party-authentication
|
|
||||||
|
|
|
|
||||||
| 'auth_header_enable' = Enable or disable third-party authentication via HTTP headers.
|
| Documentation: https://docs.wavelog.org/admin-guide/configuration/third-party-authentication
|
||||||
| 'auth_header_create' = Automatically create a local user account if a valid access token is provided and no corresponding user exists.
|
|
||||||
| 'auth_allow_direct_login' = Allow users to log in directly through the Wavelog login form even when third-party authentication is enabled. This can be useful for administrators or in cases where some users do not have access to the SSO system.
|
|
||||||
| 'auth_headers_accesstoken' = The name of the HTTP header that contains the access token. This should match the header sent by your SSO or authentication system.
|
|
||||||
| 'auth_headers_callsign_claim' = The JWT claim that contains the user's Callsign.
|
|
||||||
| 'auth_header_text' = The text displayed on the login button for third-party authentication.
|
|
||||||
| 'auth_header_club_id' = Optional club ID to assign to users authenticated via the header. This can be used to group users or assign specific permissions based on their club affiliation.
|
|
||||||
| 'auth_url_logout' = The URL to redirect users to when they log out. If OAuth2 Proxy and Keycloak is used, must hit OAuth2 Proxy first then Keycloak. whitelist_domains for OAuth2 Proxy must also be set to the IdP domain.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$config['auth_header_enable'] = false;
|
||||||
// $config['auth_header_enable'] = false;
|
|
||||||
// $config['auth_header_create'] = true;
|
|
||||||
// $config['auth_allow_direct_login'] = true;
|
|
||||||
// $config['auth_headers_accesstoken'] = "HTTP_X_FORWARDED_ACCESS_TOKEN";
|
|
||||||
// $config['auth_headers_callsign_claim'] = "callsign";
|
|
||||||
// $config['auth_header_text'] = "Login with SSO";
|
|
||||||
// $config['auth_header_club_id'] = "";
|
|
||||||
// $config['auth_url_logout'] = 'https://log.example.org/oauth2/sign_out?rd=https://auth.example.org/realms/example/protocol/openid-connect/logout';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -936,7 +917,6 @@ $config['enable_dxcluster_file_cache_worked'] = false;
|
|||||||
*/
|
*/
|
||||||
$config['dxcluster_refresh_time'] = 30;
|
$config['dxcluster_refresh_time'] = 30;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Internal tools
|
| Internal tools
|
||||||
|
|||||||
202
application/config/sso.sample.php
Normal file
202
application/config/sso.sample.php
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
* SSO / Header Authentication Settings
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* Copy this file to sso.php and adjust the values to your environment.
|
||||||
|
* This file is loaded automatically when auth_header_enable = true in config.php.
|
||||||
|
*
|
||||||
|
* Documentation: https://docs.wavelog.org/admin-guide/configuration/third-party-authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Debug Mode
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* When enabled, the decoded JWT token will be logged on every login attempt.
|
||||||
|
* This is useful for troubleshooting claim mapping and token forwarding issues.
|
||||||
|
* Requires 'log_threshold' to be set to 2 (debug) or higher in config.php.
|
||||||
|
*
|
||||||
|
* DON'T FORGET TO DISABLE THIS IN PRODUCTION ENVIRONMENTS, AS JWT TOKENS MAY CONTAIN SENSITIVE INFORMATION!
|
||||||
|
*/
|
||||||
|
$config['auth_header_debug_jwt'] = false;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
* Automatic User Creation
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* When enabled, Wavelog will automatically create a local user account the
|
||||||
|
* first time a valid JWT token is received for an unknown user. The account
|
||||||
|
* is created with a random password.
|
||||||
|
*
|
||||||
|
* Disable this if you want to provision users manually before they can log in.
|
||||||
|
* It's recommended to keep this enabled in most cases and do access control
|
||||||
|
* management through your identity provider.
|
||||||
|
*
|
||||||
|
* Recommendation: true
|
||||||
|
*/
|
||||||
|
$config['auth_header_create'] = true;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
* Allow Direct Login
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* When SSO is enabled, the standard Wavelog login form can be hidden. Default
|
||||||
|
* behavior is to show the login form alongside the SSO button, allowing users to choose.
|
||||||
|
* Set this to false to hide the login form and force users to authenticate exclusively through SSO.
|
||||||
|
*
|
||||||
|
* Recommendation: depends on your needs. You can disable direct login for a more seamless SSO experience, but enable
|
||||||
|
* it if you want to allow admins or users without SSO access to log in through the standard form.
|
||||||
|
*/
|
||||||
|
$config['auth_header_allow_direct_login'] = false;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Hide Password Field in User Profile
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* When enabled, the password field in the user profile edit page is hidden for users authenticated through SSO.
|
||||||
|
* While there are legit use cases for allowing users to set a local password (e.g. as a backup login method),
|
||||||
|
* hiding the password field can help avoid confusion and reinforce the idea that the account is managed through the identity provider.
|
||||||
|
*/
|
||||||
|
$config['auth_header_hide_password_field'] = true;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
* Access Token Header
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* The name of the HTTP header that contains the JWT access token forwarded
|
||||||
|
* by your reverse proxy or identity provider (e.g. OAuth2 Proxy, mod_auth_oidc).
|
||||||
|
*
|
||||||
|
* Note: PHP converts HTTP headers to uppercase and replaces hyphens with
|
||||||
|
* underscores, prefixed with HTTP_. For example, the header
|
||||||
|
* "X-Forwarded-Access-Token" becomes "HTTP_X_FORWARDED_ACCESS_TOKEN".
|
||||||
|
*/
|
||||||
|
$config['auth_header_accesstoken'] = "HTTP_X_FORWARDED_ACCESS_TOKEN";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
* SSO Login Button Text
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* The label shown on the SSO login button on the Wavelog login page.
|
||||||
|
*/
|
||||||
|
$config['auth_header_text'] = "Login with SSO";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
* Logout URL
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* URL to redirect the user to after logging out of Wavelog. Leave empty
|
||||||
|
* to redirect to the standard Wavelog login page while keeping the SSO session.
|
||||||
|
* This is default since logging out of Wavelog does not necessarily mean logging out of the identity provider.
|
||||||
|
*
|
||||||
|
* When using OAuth2 Proxy in front of Keycloak, the URL must first hit the
|
||||||
|
* OAuth2 Proxy sign-out endpoint, which then redirects to the Keycloak
|
||||||
|
* end-session endpoint. The whitelist_domains of OAuth2 Proxy must include
|
||||||
|
* the Keycloak domain.
|
||||||
|
*
|
||||||
|
* Example (OAuth2 Proxy + Keycloak):
|
||||||
|
* $config['auth_header_url_logout'] = 'https://log.example.org/oauth2/sign_out'
|
||||||
|
* . '?rd=https://auth.example.org/realms/example/protocol/openid-connect/logout';
|
||||||
|
*
|
||||||
|
* Recommendation: Keep it empty
|
||||||
|
*/
|
||||||
|
$config['auth_header_url_logout'] = "";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
* JWKS URI (Signature Verification)
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* URL of the JWKS endpoint of your identity provider. Wavelog uses this to
|
||||||
|
* fetch the public keys and cryptographically verify the JWT signature on
|
||||||
|
* every login. This is strongly recommended in production.
|
||||||
|
*
|
||||||
|
* Leave empty to skip signature verification (legacy / trusted-proxy mode).
|
||||||
|
* Only disable verification if your reverse proxy fully manages authentication
|
||||||
|
* and you fully trust the forwarded token without additional validation.
|
||||||
|
*
|
||||||
|
* Example (Keycloak):
|
||||||
|
* $config['auth_header_jwks_uri'] = 'https://auth.example.org/realms/example/protocol/openid-connect/certs';
|
||||||
|
*
|
||||||
|
* Recommendation: Set this to the JWKS endpoint of your identity provider for enhanced security.
|
||||||
|
*/
|
||||||
|
$config['auth_header_jwks_uri'] = "";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
* JWT Claim Mapping
|
||||||
|
*--------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* Maps Wavelog database fields to JWT claim names. Each key is a column
|
||||||
|
* name in the users table. The value is an array with the following options:
|
||||||
|
*
|
||||||
|
* 'claim' => The JWT claim name to read the value from.
|
||||||
|
*
|
||||||
|
* 'override_on_update' => If true, the field is updated from the JWT on
|
||||||
|
* every login. If false, the value is only written
|
||||||
|
* once when the account is created.
|
||||||
|
* Set to false for fields the user should be able
|
||||||
|
* to change independently (e.g. username).
|
||||||
|
*
|
||||||
|
* 'allow_manual_change'=> If true, the user can edit this field in their
|
||||||
|
* Wavelog profile. If false, the field is locked
|
||||||
|
* and managed exclusively by the identity provider.
|
||||||
|
*
|
||||||
|
* You can add any additional column from the users table here. Fields not
|
||||||
|
* listed will use their default values on account creation and will not be
|
||||||
|
* touched on subsequent logins.
|
||||||
|
*
|
||||||
|
* The following fields are required for account creation and must be present:
|
||||||
|
* - user_name
|
||||||
|
* - user_email
|
||||||
|
* - user_callsign
|
||||||
|
*/
|
||||||
|
$config['auth_headers_claim_config'] = [
|
||||||
|
'user_name' => [
|
||||||
|
'claim' => 'preferred_username',
|
||||||
|
'override_on_update' => true,
|
||||||
|
'allow_manual_change' => false
|
||||||
|
],
|
||||||
|
'user_email' => [
|
||||||
|
'claim' => 'email',
|
||||||
|
'override_on_update' => true,
|
||||||
|
'allow_manual_change' => false
|
||||||
|
],
|
||||||
|
'user_callsign' => [
|
||||||
|
'claim' => 'callsign',
|
||||||
|
'override_on_update' => true,
|
||||||
|
'allow_manual_change' => false
|
||||||
|
],
|
||||||
|
'user_locator' => [
|
||||||
|
'claim' => 'locator',
|
||||||
|
'override_on_update' => true,
|
||||||
|
'allow_manual_change' => false
|
||||||
|
],
|
||||||
|
'user_firstname' => [
|
||||||
|
'claim' => 'given_name',
|
||||||
|
'override_on_update' => true,
|
||||||
|
'allow_manual_change' => false
|
||||||
|
],
|
||||||
|
'user_lastname' => [
|
||||||
|
'claim' => 'family_name',
|
||||||
|
'override_on_update' => true,
|
||||||
|
'allow_manual_change' => false
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
|
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
|
||||||
|
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\JWK;
|
||||||
|
|
||||||
|
require_once APPPATH . '../src/jwt/src/JWT.php';
|
||||||
|
require_once APPPATH . '../src/jwt/src/Key.php';
|
||||||
|
require_once APPPATH . '../src/jwt/src/JWK.php';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Handles header based authentication
|
Handles header based authentication
|
||||||
@@ -9,19 +16,28 @@ class Header_auth extends CI_Controller {
|
|||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
if ($this->config->item('auth_header_enable')) {
|
||||||
|
$this->config->load('sso', true, true);
|
||||||
|
} else {
|
||||||
|
$this->_sso_error(__("SSO Authentication is disabled."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate using a trusted request header/JWT token.
|
* Authenticate using a trusted request header/JWT token. This endpoint is meant to be called by a reverse proxy that sits in front of Wavelog and handles the actual authentication (e.g. OAuth2 Proxy, Apache mod_auth_oidc, etc.).
|
||||||
|
* The reverse proxy validates the user's session and forwards a JWT access token containing the user's identity and claims in a trusted HTTP header. This method decodes the token, verifies it, extracts the user information
|
||||||
|
* and logs the user in. Depending on configuration, it can also automatically create a local user account if one does not exist.
|
||||||
|
*
|
||||||
|
* For more information check out the documentation: https://docs.wavelog.org/admin-guide/configuration/third-party-authentication
|
||||||
*/
|
*/
|
||||||
public function login() {
|
public function login() {
|
||||||
// Guard: feature must be enabled
|
// Guard: feature must be enabled
|
||||||
if (!$this->config->item('auth_header_enable')) {
|
if (!$this->config->item('auth_header_enable')) {
|
||||||
$this->_sso_error(__("SSO Authentication is not enabled. Check your configuration."));
|
$this->_sso_error(__("SSO Authentication is disabled. Check your configuration."));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode JWT access token forwarded by idp
|
// Decode JWT access token forwarded by idp
|
||||||
$accesstoken_path = $this->config->item('auth_headers_accesstoken') ?? false;
|
$accesstoken_path = $this->config->item('auth_header_accesstoken', 'sso') ?? false;
|
||||||
if (!$accesstoken_path) {
|
if (!$accesstoken_path) {
|
||||||
log_message('error', 'SSO Authentication: Access Token Path not configured in config.php.');
|
log_message('error', 'SSO Authentication: Access Token Path not configured in config.php.');
|
||||||
$this->_sso_error();
|
$this->_sso_error();
|
||||||
@@ -32,39 +48,43 @@ class Header_auth extends CI_Controller {
|
|||||||
$this->_sso_error();
|
$this->_sso_error();
|
||||||
}
|
}
|
||||||
|
|
||||||
$claims = $this->_decode_jwt_payload($token);
|
if ($this->config->item('auth_header_debug_jwt', 'sso')) {
|
||||||
|
log_message('debug', 'Raw JWT: ' . $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
$claims = $this->_verify_jwt($token);
|
||||||
if (empty($claims)) {
|
if (empty($claims)) {
|
||||||
log_message('error', 'SSO Authentication: Invalid access token format. Failed to decode JWT token.');
|
$this->_sso_error("SSO Authentication failed. Invalid token.");
|
||||||
$this->_sso_error();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->_verify_jwtdata($claims)) {
|
if ($this->config->item('auth_header_debug_jwt', 'sso')) {
|
||||||
log_message('error', 'SSO Authentication: Token validation failed.');
|
log_message('debug', 'Decoded and validated JWT: ' . json_encode($claims, JSON_PRETTY_PRINT));
|
||||||
$this->_sso_error();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$callsign_claim = $this->config->item('auth_headers_callsign_claim') ?? 'callsign';
|
$claim_map = $this->config->item('auth_headers_claim_config', 'sso');
|
||||||
|
|
||||||
$username = $claims['preferred_username'] ?? '';
|
// Extract all mapped claims dynamically — supports custom fields added by the admin
|
||||||
$email = $claims['email'] ?? '';
|
$mapped = [];
|
||||||
$callsign = $claims[$callsign_claim] ?? '';
|
foreach ($claim_map as $db_field => $cfg) {
|
||||||
$firstname = $claims['given_name'] ?? '';
|
$mapped[$db_field] = $claims[$cfg['claim']] ?? null;
|
||||||
$lastname = $claims['family_name'] ?? '';
|
|
||||||
|
|
||||||
if (empty($username)) {
|
|
||||||
log_message('error', 'SSO Authentication: Missing username claim in access token.');
|
|
||||||
$this->_sso_error();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up user by the header value
|
// Build composite key: JSON {iss, sub} — uniquely identifies a user across IdP and user
|
||||||
|
$iss = $claims['iss'] ?? '';
|
||||||
|
$sub = $claims['sub'] ?? '';
|
||||||
|
if (empty($iss) || empty($sub)) {
|
||||||
|
log_message('error', 'SSO Authentication: Missing iss or sub claim in access token.');
|
||||||
|
$this->_sso_error();
|
||||||
|
}
|
||||||
|
$external_identifier = json_encode(['iss' => $iss, 'sub' => $sub]);
|
||||||
|
|
||||||
$this->load->model('user_model');
|
$this->load->model('user_model');
|
||||||
$query = $this->user_model->get($username);
|
$query = $this->user_model->get_by_external_account($external_identifier);
|
||||||
|
|
||||||
if (!$query || $query->num_rows() !== 1) {
|
if (!$query || $query->num_rows() !== 1) {
|
||||||
// Config check if create user
|
if ($this->config->item('auth_header_create', 'sso')) {
|
||||||
if ($this->config->item('auth_header_create')) {
|
$this->_create_user($mapped, $external_identifier);
|
||||||
$this->_create_user($username, $email, $callsign, $firstname, $lastname);
|
$query = $this->user_model->get_by_external_account($external_identifier);
|
||||||
$query = $this->user_model->get($username);
|
|
||||||
} else {
|
} else {
|
||||||
$this->_sso_error(__("User not found."));
|
$this->_sso_error(__("User not found."));
|
||||||
return;
|
return;
|
||||||
@@ -72,13 +92,16 @@ class Header_auth extends CI_Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$query || $query->num_rows() !== 1) {
|
if (!$query || $query->num_rows() !== 1) {
|
||||||
log_message('error', 'SSO Authentication: User could not be found or created.');
|
log_message('error', 'SSO Authentication: Something went terribly wrong. Check error log.');
|
||||||
$this->_sso_error();
|
$this->_sso_error();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $query->row();
|
$user = $query->row();
|
||||||
|
|
||||||
|
// Update fields from JWT claims where override_on_update is enabled
|
||||||
|
$this->_update_user_from_claims($user->user_id, $mapped);
|
||||||
|
|
||||||
// Prevent clubstation direct login via header (mirrors User::login)
|
// Prevent clubstation direct login via header (mirrors User::login)
|
||||||
if (!empty($user->clubstation) && $user->clubstation == 1) {
|
if (!empty($user->clubstation) && $user->clubstation == 1) {
|
||||||
$this->_sso_error(__("You can't login to a clubstation directly. Use your personal account instead."));
|
$this->_sso_error(__("You can't login to a clubstation directly. Use your personal account instead."));
|
||||||
@@ -107,175 +130,199 @@ class Header_auth extends CI_Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a JWT token
|
* Decode and verify a JWT token. Returns the claims array on success, null on failure.
|
||||||
*
|
*
|
||||||
|
* JWKS mode (auth_header_jwks_uri configured):
|
||||||
|
* Firebase JWT::decode() handles signature, exp, nbf and alg validation.
|
||||||
|
*
|
||||||
|
* Low Security mode (no JWKS URI):
|
||||||
|
* Payload is decoded without signature verification. exp, nbf and alg
|
||||||
|
* are checked manually.
|
||||||
|
*
|
||||||
|
* In both modes: iat age and typ are validated after decoding.
|
||||||
|
*
|
||||||
* @param string $token
|
* @param string $token
|
||||||
*
|
*
|
||||||
* @return array|null
|
* @return array|null
|
||||||
*/
|
*/
|
||||||
private function _decode_jwt_payload(string $token): ?array {
|
private function _verify_jwt(string $token): ?array {
|
||||||
$parts = explode('.', $token);
|
$jwksUri = $this->config->item('auth_header_jwks_uri', 'sso');
|
||||||
if (count($parts) !== 3) {
|
|
||||||
|
if (!empty($jwksUri)) {
|
||||||
|
try {
|
||||||
|
$jwksJson = file_get_contents($jwksUri);
|
||||||
|
if ($jwksJson === false) {
|
||||||
|
log_message('error', 'SSO Authentication: Failed to fetch JWKS from ' . $jwksUri);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$jwks = json_decode($jwksJson, true);
|
||||||
|
$keys = JWK::parseKeySet($jwks);
|
||||||
|
$claims = (array) JWT::decode($token, $keys);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
log_message('error', 'SSO Authentication: JWT decode/verify failed: ' . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Low Security mode: decode without signature verification to provide a minimal level of security
|
||||||
|
log_message('debug', 'SSO Authentication: JWKS URI not configured, skipping signature verification.');
|
||||||
|
|
||||||
|
$parts = explode('.', $token);
|
||||||
|
if (count($parts) !== 3) {
|
||||||
|
log_message('error', 'SSO Authentication: JWT does not have 3 parts.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$b64decode = function (string $part): ?array {
|
||||||
|
$json = base64_decode(str_pad(strtr($part, '-_', '+/'), strlen($part) % 4, '=', STR_PAD_RIGHT));
|
||||||
|
if ($json === false) return null;
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
return is_array($data) ? $data : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$header = $b64decode($parts[0]);
|
||||||
|
$claims = $b64decode($parts[1]);
|
||||||
|
if ($claims === null) {
|
||||||
|
log_message('error', 'SSO Authentication: Failed to decode JWT payload.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($claims['exp'] ?? 0) < time()) {
|
||||||
|
log_message('error', 'SSO Authentication: JWT Token is expired.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($claims['nbf']) && $claims['nbf'] > time()) {
|
||||||
|
log_message('error', 'SSO Authentication: JWT Token is not valid yet.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$alg = $header['alg'] ?? '';
|
||||||
|
if (!in_array($alg, ['RS256', 'RS384', 'RS512', 'ES256', 'ES384'], true)) {
|
||||||
|
log_message('error', 'SSO Authentication: Algorithm "' . $alg . '" is not allowed.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common checks (both modes)
|
||||||
|
if (isset($claims['iat']) && $claims['iat'] < (time() - 86400)) {
|
||||||
|
log_message('error', 'SSO Authentication: Token is older than 24 hours.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$decode = function (string $part): ?array {
|
if (isset($claims['typ']) && $claims['typ'] !== 'Bearer') {
|
||||||
$json = base64_decode(str_pad(strtr($part, '-_', '+/'), strlen($part) % 4, '=', STR_PAD_RIGHT));
|
log_message('error', 'SSO Authentication: JWT Token is no Bearer Token.');
|
||||||
if ($json === false) return null;
|
|
||||||
$data = json_decode($json, true);
|
|
||||||
return is_array($data) ? $data : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
$header = $decode($parts[0]);
|
|
||||||
$payload = $decode($parts[1]);
|
|
||||||
if ($payload === null) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge alg from header into payload so _verify_jwtdata can check it
|
return $claims;
|
||||||
if (isset($header['alg'])) {
|
|
||||||
$payload['alg'] = $header['alg'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $payload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to verify some long hangig fruits. We are not verifying the JWT token against the issuer at this point.
|
* Update user fields from JWT claims where override_on_update is enabled.
|
||||||
* Reason is the need for a crypto lib which is not necessary at this point. An administrator is responsible
|
*
|
||||||
* for the proper isolation of Wavelog and needs to make sure that Wavelog is not exposed directly.
|
* @param int $user_id
|
||||||
*
|
* @param array $claim_map
|
||||||
* Additonal verificarion steps can be added at a later point.
|
* @param array $values Associative array of field => value from the JWT
|
||||||
*
|
*
|
||||||
* @param array claim data
|
* @return void
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
private function _verify_jwtdata(?array $claims = null): bool {
|
private function _update_user_from_claims(int $user_id, array $mapped): void {
|
||||||
// No claim, no verificiation
|
$updates = [];
|
||||||
if (!$claims) {
|
$claim_map = $this->config->item('auth_headers_claim_config', 'sso');
|
||||||
log_message('error', 'JWT Verification: No claim data received.');
|
foreach ($claim_map as $db_field => $cfg) {
|
||||||
return false;
|
if (!empty($cfg['override_on_update']) && $mapped[$db_field] !== null) {
|
||||||
|
$updates[$db_field] = $mapped[$db_field];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check expire date
|
if (!empty($updates)) {
|
||||||
if (($claims['exp'] ?? 0) < time()) {
|
$this->user_model->update_sso_claims($user_id, $updates);
|
||||||
log_message('error', 'JWT Verification: JWT Token is expired.');
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is the token already valid
|
|
||||||
if (isset($claims['nbf']) && $claims['nbf'] > time()) {
|
|
||||||
log_message('error', 'JWT Verification: JWT Token is not valid yet.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The token should not be older then 24 hours which would be absurd old for an JWT token
|
|
||||||
if (isset($claims['iat']) && $claims['iat'] < (time() - 86400)) {
|
|
||||||
log_message('error', 'JWT Verification: Token is older then 24 hours. This is very unusual. Verification failed.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is it a bearer token?
|
|
||||||
if (isset($claims['typ']) && $claims['typ'] !== 'Bearer') {
|
|
||||||
log_message('error', 'JWT Verification: JWT Token is no Bearer Token.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent alg: none attacks
|
|
||||||
if (!in_array($claims['alg'], ['RS256', 'RS384', 'RS512', 'ES256', 'ES384'], true)) {
|
|
||||||
log_message('error', 'JWT Verification: Algorithm ' . ($claims['alg'] ?? '???') . ' is not allowed. Create an issue on github https://github.com/wavelog/wavelog.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to create a user if it does not exist.
|
* Helper to create a user if it does not exist.
|
||||||
*
|
*
|
||||||
* @param string $username
|
* @param array $mapped All DB field => value pairs extracted from JWT claims
|
||||||
* @param string $email
|
* @param string $external_identifier Composite key JSON {iss, sub} — stored once, never updated
|
||||||
* @param string $callsign
|
*
|
||||||
* @param string $firstname
|
|
||||||
* @param string $lastname
|
|
||||||
*
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function _create_user($username, $email, $callsign, $firstname, $lastname) {
|
private function _create_user(array $mapped, string $external_identifier) {
|
||||||
if (empty($email) || empty($callsign)) {
|
if (empty($mapped['user_email']) || empty($mapped['user_callsign'])) {
|
||||||
log_message('error', 'SSO Authentication: Missing email or callsign claim in access token.');
|
log_message('error', 'SSO Authentication: Missing email or callsign claim in access token.');
|
||||||
$this->_sso_error();
|
$this->_sso_error();
|
||||||
}
|
}
|
||||||
|
|
||||||
// $club_id = $this->config->item('auth_header_club_id') ?: ''; // TODO: Add support to add a user to a clubstation
|
// $club_id = $this->config->item('auth_header_club_id', 'sso') ?: ''; // TODO: Add support to add a user to a clubstation
|
||||||
|
|
||||||
$this->load->model('user_model');
|
$this->load->model('user_model');
|
||||||
$result = $this->user_model->add(
|
$result = $this->user_model->add(
|
||||||
$username,
|
$mapped['user_name'] ?? '',
|
||||||
bin2hex(random_bytes(64)), // password
|
bin2hex(random_bytes(64)), // password is always random
|
||||||
$email,
|
$mapped['user_email'] ?? '',
|
||||||
3, // $data['user_type'], we don't create admins for security reasons
|
3, // user_type: never admin via SSO
|
||||||
$firstname,
|
$mapped['user_firstname'] ?? '',
|
||||||
$lastname,
|
$mapped['user_lastname'] ?? '',
|
||||||
$callsign,
|
$mapped['user_callsign'] ?? '',
|
||||||
"", // locator
|
$mapped['user_locator'] ?? '',
|
||||||
24, // user_timezone is default UTC
|
$mapped['user_timezone'] ?? 24,
|
||||||
"M", // measurement
|
$mapped['user_measurement_base'] ?? 'M',
|
||||||
"Y", // dashboard_map
|
$mapped['dashboard_map'] ?? 'Y',
|
||||||
"Y-m-d", // user_date_format
|
$mapped['user_date_format'] ?? 'Y-m-d',
|
||||||
'darkly', // user_stylesheet
|
$mapped['user_stylesheet'] ?? 'darkly',
|
||||||
'0', // user_qth_lookup
|
$mapped['user_qth_lookup'] ?? '0',
|
||||||
'0', // user_sota_lookup
|
$mapped['user_sota_lookup'] ?? '0',
|
||||||
'0', // user_wwff_lookup
|
$mapped['user_wwff_lookup'] ?? '0',
|
||||||
'0', // user_pota_lookup
|
$mapped['user_pota_lookup'] ?? '0',
|
||||||
1, // user_show_notes
|
$mapped['user_show_notes'] ?? 1,
|
||||||
'Mode', // user_column1
|
$mapped['user_column1'] ?? 'Mode',
|
||||||
'RSTS', // user_column2
|
$mapped['user_column2'] ?? 'RSTS',
|
||||||
'RSTR', // user_column3
|
$mapped['user_column3'] ?? 'RSTR',
|
||||||
'Band', // user_column4
|
$mapped['user_column4'] ?? 'Band',
|
||||||
'Country', // user_column5
|
$mapped['user_column5'] ?? 'Country',
|
||||||
'0', // user_show_profile_image
|
$mapped['user_show_profile_image'] ?? '0',
|
||||||
'0', // user_previous_qsl_type
|
$mapped['user_previous_qsl_type'] ?? '0',
|
||||||
'0', // user_amsat_status_upload
|
$mapped['user_amsat_status_upload'] ?? '0',
|
||||||
'', // user_mastodon_url
|
$mapped['user_mastodon_url'] ?? '',
|
||||||
'ALL', // user_default_band
|
$mapped['user_default_band'] ?? 'ALL',
|
||||||
'QL', // user_default_confirmation
|
$mapped['user_default_confirmation'] ?? 'QL',
|
||||||
'0', // user_qso_end_times
|
$mapped['user_qso_end_times'] ?? '0',
|
||||||
"Y", // user_qso_db_search_priority
|
$mapped['user_qso_db_search_priority'] ?? 'Y',
|
||||||
'0', // user_quicklog
|
$mapped['user_quicklog'] ?? '0',
|
||||||
'0', // user_quicklog_enter
|
$mapped['user_quicklog_enter'] ?? '0',
|
||||||
"english", // user_language
|
$mapped['user_language'] ?? 'english',
|
||||||
'', // user_hamsat_key
|
$mapped['user_hamsat_key'] ?? '',
|
||||||
'', // user_hamsat_workable_only
|
$mapped['user_hamsat_workable_only'] ?? '',
|
||||||
'', // user_iota_to_qso_tab
|
$mapped['user_iota_to_qso_tab'] ?? '',
|
||||||
'', // user_sota_to_qso_tab
|
$mapped['user_sota_to_qso_tab'] ?? '',
|
||||||
'', // user_wwff_to_qso_tab
|
$mapped['user_wwff_to_qso_tab'] ?? '',
|
||||||
'', // user_pota_to_qso_tab
|
$mapped['user_pota_to_qso_tab'] ?? '',
|
||||||
'', // user_sig_to_qso_tab
|
$mapped['user_sig_to_qso_tab'] ?? '',
|
||||||
'', // user_dok_to_qso_tab
|
$mapped['user_dok_to_qso_tab'] ?? '',
|
||||||
0, // user_station_to_qso_tab
|
$mapped['user_station_to_qso_tab'] ?? 0,
|
||||||
'', // user_lotw_name
|
$mapped['user_lotw_name'] ?? '',
|
||||||
'', // user_lotw_password
|
$mapped['user_lotw_password'] ?? '',
|
||||||
'', // user_eqsl_name
|
$mapped['user_eqsl_name'] ?? '',
|
||||||
'', // user_eqsl_password
|
$mapped['user_eqsl_password'] ?? '',
|
||||||
'', // user_clublog_name
|
$mapped['user_clublog_name'] ?? '',
|
||||||
'', // user_clublog_password
|
$mapped['user_clublog_password'] ?? '',
|
||||||
'0', // user_winkey
|
$mapped['user_winkey'] ?? '0',
|
||||||
"", // on_air_widget_enabled
|
$mapped['on_air_widget_enabled'] ?? '',
|
||||||
"", // on_air_widget_display_last_seen
|
$mapped['on_air_widget_display_last_seen'] ?? '',
|
||||||
"", // on_air_widget_show_only_most_recent_radio
|
$mapped['on_air_widget_show_only_most_recent_radio'] ?? '',
|
||||||
"", // qso_widget_display_qso_time
|
$mapped['qso_widget_display_qso_time'] ?? '',
|
||||||
"", // dashboard_banner
|
$mapped['dashboard_banner'] ?? '',
|
||||||
"", // dashboard_solar
|
$mapped['dashboard_solar'] ?? '',
|
||||||
"", // global_oqrs_text
|
$mapped['global_oqrs_text'] ?? '',
|
||||||
"", // oqrs_grouped_search
|
$mapped['oqrs_grouped_search'] ?? '',
|
||||||
"", // oqrs_grouped_search_show_station_name
|
$mapped['oqrs_grouped_search_show_station_name'] ?? '',
|
||||||
"", // oqrs_auto_matching
|
$mapped['oqrs_auto_matching'] ?? '',
|
||||||
"", // oqrs_direct_auto_matching
|
$mapped['oqrs_direct_auto_matching'] ?? '',
|
||||||
"", // user_dxwaterfall_enable
|
$mapped['user_dxwaterfall_enable'] ?? '',
|
||||||
"", // user_qso_show_map
|
$mapped['user_qso_show_map'] ?? '',
|
||||||
0, // clubstation
|
0, // clubstation
|
||||||
true, // external_account
|
$external_identifier, // external_account
|
||||||
);
|
);
|
||||||
|
|
||||||
switch ($result) {
|
switch ($result) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
class User extends CI_Controller {
|
class User extends CI_Controller {
|
||||||
|
|
||||||
|
private $pwd_placeholder = '**********';
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->load->model('user_model');
|
$this->load->model('user_model');
|
||||||
@@ -396,7 +398,6 @@ class User extends CI_Controller {
|
|||||||
$query = $this->user_model->get_by_id($this->uri->segment(3));
|
$query = $this->user_model->get_by_id($this->uri->segment(3));
|
||||||
|
|
||||||
$data['existing_languages'] = $this->config->item('languages');
|
$data['existing_languages'] = $this->config->item('languages');
|
||||||
$pwd_placeholder = '**********';
|
|
||||||
|
|
||||||
$this->load->model('bands');
|
$this->load->model('bands');
|
||||||
$this->load->library('form_validation');
|
$this->load->library('form_validation');
|
||||||
@@ -440,6 +441,17 @@ class User extends CI_Controller {
|
|||||||
// Max value to be present in the "QSO page last QSO count" selectbox
|
// Max value to be present in the "QSO page last QSO count" selectbox
|
||||||
$data['qso_page_last_qso_count_limit'] = QSO_PAGE_QSOS_COUNT_LIMIT;
|
$data['qso_page_last_qso_count_limit'] = QSO_PAGE_QSOS_COUNT_LIMIT;
|
||||||
|
|
||||||
|
// SSO / OIDC cases
|
||||||
|
$data['external_account'] = !empty($query->row()->external_account);
|
||||||
|
$data['auth_header_enable'] = $this->config->item('auth_header_enable') ?? false;
|
||||||
|
if ($data['auth_header_enable']) {
|
||||||
|
// expecting sso.php in the config folder
|
||||||
|
$this->config->load('sso', true, true);
|
||||||
|
}
|
||||||
|
$data['auth_header_allow_direct_login'] = $this->config->item('auth_header_allow_direct_login', 'sso') ?? true;
|
||||||
|
$data['auth_header_hide_password_field'] = $this->config->item('auth_header_hide_password_field', 'sso') ?? false;
|
||||||
|
$data['sso_claim_config'] = $this->config->item('auth_headers_claim_config', 'sso') ?: [];
|
||||||
|
|
||||||
$data['page_title'] = __("Edit User");
|
$data['page_title'] = __("Edit User");
|
||||||
|
|
||||||
if ($this->form_validation->run() == FALSE)
|
if ($this->form_validation->run() == FALSE)
|
||||||
@@ -465,7 +477,7 @@ class User extends CI_Controller {
|
|||||||
$data['user_password'] = $this->input->post('user_password',true);
|
$data['user_password'] = $this->input->post('user_password',true);
|
||||||
} else {
|
} else {
|
||||||
if ($q->user_password !== '' && $q->user_password !== null) {
|
if ($q->user_password !== '' && $q->user_password !== null) {
|
||||||
$data['user_password'] = $pwd_placeholder;
|
$data['user_password'] = $this->pwd_placeholder;
|
||||||
} else {
|
} else {
|
||||||
$data['user_password'] = '';
|
$data['user_password'] = '';
|
||||||
}
|
}
|
||||||
@@ -535,7 +547,7 @@ class User extends CI_Controller {
|
|||||||
$data['user_clublog_password'] = $this->input->post('user_clublog_password', true);
|
$data['user_clublog_password'] = $this->input->post('user_clublog_password', true);
|
||||||
} else {
|
} else {
|
||||||
if ($q->user_clublog_password !== '' && $q->user_clublog_password !== null) {
|
if ($q->user_clublog_password !== '' && $q->user_clublog_password !== null) {
|
||||||
$data['user_clublog_password'] = $pwd_placeholder;
|
$data['user_clublog_password'] = $this->pwd_placeholder;
|
||||||
} else {
|
} else {
|
||||||
$data['user_clublog_password'] = '';
|
$data['user_clublog_password'] = '';
|
||||||
}
|
}
|
||||||
@@ -545,7 +557,7 @@ class User extends CI_Controller {
|
|||||||
$data['user_lotw_password'] = $this->input->post('user_lotw_password', true);
|
$data['user_lotw_password'] = $this->input->post('user_lotw_password', true);
|
||||||
} else {
|
} else {
|
||||||
if ($q->user_lotw_password !== '' && $q->user_lotw_password !== null) {
|
if ($q->user_lotw_password !== '' && $q->user_lotw_password !== null) {
|
||||||
$data['user_lotw_password'] = $pwd_placeholder;
|
$data['user_lotw_password'] = $this->pwd_placeholder;
|
||||||
} else {
|
} else {
|
||||||
$data['user_lotw_password'] = '';
|
$data['user_lotw_password'] = '';
|
||||||
}
|
}
|
||||||
@@ -561,7 +573,7 @@ class User extends CI_Controller {
|
|||||||
$data['user_eqsl_password'] = $this->input->post('user_eqsl_password', true);
|
$data['user_eqsl_password'] = $this->input->post('user_eqsl_password', true);
|
||||||
} else {
|
} else {
|
||||||
if ($q->user_eqsl_password !== '' && $q->user_eqsl_password !== null) {
|
if ($q->user_eqsl_password !== '' && $q->user_eqsl_password !== null) {
|
||||||
$data['user_eqsl_password'] = $pwd_placeholder;
|
$data['user_eqsl_password'] = $this->pwd_placeholder;
|
||||||
} else {
|
} else {
|
||||||
$data['user_eqsl_password'] = '';
|
$data['user_eqsl_password'] = '';
|
||||||
}
|
}
|
||||||
@@ -953,7 +965,17 @@ class User extends CI_Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unset($data);
|
unset($data);
|
||||||
switch($this->user_model->edit($this->input->post())) {
|
|
||||||
|
// SSO / OIDC: Override submitted values for fields managed by the IdP
|
||||||
|
$post_data = $this->input->post();
|
||||||
|
if (!empty($query->row()->external_account)) {
|
||||||
|
$post_data['user_name'] = $query->row()->user_name;
|
||||||
|
if (!($this->config->item('auth_header_allow_direct_login') ?? true)) {
|
||||||
|
$post_data['user_password'] = $this->pwd_placeholder; // placeholder → model skips password update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch($this->user_model->edit($post_data)) {
|
||||||
// Check for errors
|
// Check for errors
|
||||||
case EUSERNAMEEXISTS:
|
case EUSERNAMEEXISTS:
|
||||||
$data['username_error'] = 'Username <b>'.$this->input->post('user_name', true).'</b> already in use!';
|
$data['username_error'] = 'Username <b>'.$this->input->post('user_name', true).'</b> already in use!';
|
||||||
@@ -1243,9 +1265,15 @@ class User extends CI_Controller {
|
|||||||
if ($this->form_validation->run() == FALSE) {
|
if ($this->form_validation->run() == FALSE) {
|
||||||
$data['page_title'] = __("Login");
|
$data['page_title'] = __("Login");
|
||||||
$data['https_check'] = $this->https_check();
|
$data['https_check'] = $this->https_check();
|
||||||
|
|
||||||
$data['auth_header_enable'] = $this->config->item('auth_header_enable') ?? false;
|
$data['auth_header_enable'] = $this->config->item('auth_header_enable') ?? false;
|
||||||
$data['auth_header_text'] = $this->config->item('auth_header_text') ?: '';
|
if ($data['auth_header_enable']) {
|
||||||
$data['hide_login_form'] = ($data['auth_header_enable'] && !($this->config->item('auth_allow_direct_login') ?? true));
|
// expecting sso.php in the config folder
|
||||||
|
$this->config->load('sso', true, true);
|
||||||
|
}
|
||||||
|
$data['auth_header_text'] = $this->config->item('auth_header_text', 'sso') ?: '';
|
||||||
|
$data['hide_login_form'] = ($data['auth_header_enable'] && !($this->config->item('auth_header_allow_direct_login', 'sso') ?? true));
|
||||||
|
|
||||||
$this->load->view('interface_assets/mini_header', $data);
|
$this->load->view('interface_assets/mini_header', $data);
|
||||||
$this->load->view('user/login');
|
$this->load->view('user/login');
|
||||||
$this->load->view('interface_assets/footer');
|
$this->load->view('interface_assets/footer');
|
||||||
@@ -1320,9 +1348,12 @@ class User extends CI_Controller {
|
|||||||
$this->input->set_cookie('tmp_msg', json_encode(['notice', sprintf(__("User %s logged out."), $user_name)]), 10, '');
|
$this->input->set_cookie('tmp_msg', json_encode(['notice', sprintf(__("User %s logged out."), $user_name)]), 10, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
$logout = $this->config->item('auth_url_logout');
|
if ($this->config->item('auth_header_enable')) {
|
||||||
if ($this->config->item('auth_header_enable') && $logout !== null && $logout !== '') {
|
$this->config->load('sso', true, true);
|
||||||
redirect($logout);
|
$logout = $this->config->item('auth_header_url_logout', 'sso') ?: null;
|
||||||
|
if ($logout !== null) {
|
||||||
|
redirect($logout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect('user/login');
|
redirect('user/login');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ defined('BASEPATH') or exit('No direct script access allowed');
|
|||||||
class Migration_external_account extends CI_Migration {
|
class Migration_external_account extends CI_Migration {
|
||||||
|
|
||||||
public function up() {
|
public function up() {
|
||||||
$this->dbtry("ALTER TABLE users ADD COLUMN external_account tinyint DEFAULT 0 AFTER clubstation");
|
$this->dbtry("ALTER TABLE users ADD COLUMN external_account TEXT DEFAULT NULL AFTER clubstation");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down() {
|
public function down() {
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ class User_Model extends CI_Model {
|
|||||||
$user_lotw_name, $user_lotw_password, $user_eqsl_name, $user_eqsl_password, $user_clublog_name, $user_clublog_password,
|
$user_lotw_name, $user_lotw_password, $user_eqsl_name, $user_eqsl_password, $user_clublog_name, $user_clublog_password,
|
||||||
$user_winkey, $on_air_widget_enabled, $on_air_widget_display_last_seen, $on_air_widget_show_only_most_recent_radio,
|
$user_winkey, $on_air_widget_enabled, $on_air_widget_display_last_seen, $on_air_widget_show_only_most_recent_radio,
|
||||||
$qso_widget_display_qso_time, $dashboard_banner, $dashboard_solar, $global_oqrs_text, $oqrs_grouped_search,
|
$qso_widget_display_qso_time, $dashboard_banner, $dashboard_solar, $global_oqrs_text, $oqrs_grouped_search,
|
||||||
$oqrs_grouped_search_show_station_name, $oqrs_auto_matching, $oqrs_direct_auto_matching,$user_dxwaterfall_enable, $user_qso_show_map, $clubstation = 0, $external_account = false) {
|
$oqrs_grouped_search_show_station_name, $oqrs_auto_matching, $oqrs_direct_auto_matching,$user_dxwaterfall_enable, $user_qso_show_map, $clubstation = 0, $external_account = null) {
|
||||||
// Check that the user isn't already used
|
// Check that the user isn't already used
|
||||||
if(!$this->exists($username)) {
|
if(!$this->exists($username)) {
|
||||||
$data = array(
|
$data = array(
|
||||||
@@ -540,7 +540,7 @@ class User_Model extends CI_Model {
|
|||||||
// This is really just a wrapper around User_Model::authenticate
|
// This is really just a wrapper around User_Model::authenticate
|
||||||
function login() {
|
function login() {
|
||||||
|
|
||||||
if (($this->config->item('auth_header_enable') ?? false) && !($this->config->item('auth_allow_direct_login') ?? true)) {
|
if (($this->config->item('auth_header_enable') ?? false) && !($this->config->item('auth_header_allow_direct_login') ?? true)) {
|
||||||
$this->session->set_flashdata('error', 'Direct login is disabled. Please use the SSO option to log in.');
|
$this->session->set_flashdata('error', 'Direct login is disabled. Please use the SSO option to log in.');
|
||||||
redirect('user/login');
|
redirect('user/login');
|
||||||
}
|
}
|
||||||
@@ -753,6 +753,18 @@ class User_Model extends CI_Model {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FUNCTION: retrieve a user by their SSO composite key (md5(iss).sub)
|
||||||
|
function get_by_external_account(string $key) {
|
||||||
|
$this->db->where('external_account', $key);
|
||||||
|
return $this->db->get($this->config->item('auth_table'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// FUNCTION: update specific user fields from SSO claims (bypass privilege check, used during login flow)
|
||||||
|
function update_sso_claims(int $user_id, array $fields): void {
|
||||||
|
$this->db->where('user_id', $user_id);
|
||||||
|
$this->db->update('users', $fields);
|
||||||
|
}
|
||||||
|
|
||||||
// FUNCTION: set's the last-login timestamp in user table
|
// FUNCTION: set's the last-login timestamp in user table
|
||||||
function set_last_seen($user_id) {
|
function set_last_seen($user_id) {
|
||||||
$data = array(
|
$data = array(
|
||||||
|
|||||||
@@ -53,18 +53,41 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><?= __("Account"); ?></div>
|
<div class="card-header"><?= __("Account"); ?></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<?php
|
||||||
|
// Returns true if the field is managed by the IdP and the user cannot change it
|
||||||
|
$idp_locked = function($field) use ($external_account, $sso_claim_config) {
|
||||||
|
return $external_account && isset($sso_claim_config[$field]) && empty($sso_claim_config[$field]['allow_manual_change']);
|
||||||
|
};
|
||||||
|
$idp_badge = '<span class="badge bg-secondary ms-1" data-bs-toggle="tooltip" title="' . __("Can't be changed. Manage this through your Identity Provider.") . '"><i class="fa fa-lock"></i> IdP</span>';
|
||||||
|
?>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label><?= __("Username"); ?></label>
|
<label>
|
||||||
<input class="form-control" type="text" name="user_name" value="<?php if(isset($user_name)) { echo $user_name; } ?>" <?php if (isset($user_name) && $user_name == 'demo' && file_exists('.demo') && $this->session->userdata('user_type') !== '99') { echo 'disabled'; } ?> />
|
<?= __("Username"); ?>
|
||||||
<?php if(isset($username_error)) { echo "<small class=\"badge bg-danger\">".$username_error."</small>"; } ?>
|
<?php if ($idp_locked('user_name')) { echo $idp_badge; } ?>
|
||||||
|
</label>
|
||||||
|
<?php if ($idp_locked('user_name')) { ?>
|
||||||
|
<input class="form-control-plaintext fw-bold" name="user_name" value="<?= htmlspecialchars($user_name ?? '') ?>" readonly/>
|
||||||
|
<?php } else { ?>
|
||||||
|
<input class="form-control" type="text" name="user_name" value="<?php if(isset($user_name)) { echo $user_name; } ?>" <?php if (isset($user_name) && $user_name == 'demo' && file_exists('.demo') && $this->session->userdata('user_type') !== '99') { echo 'disabled'; } ?> />
|
||||||
|
<?php if(isset($username_error)) { echo "<small class=\"badge bg-danger\">".$username_error."</small>"; } ?>
|
||||||
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label><?= __("Email Address"); ?></label>
|
<label>
|
||||||
<input class="form-control" type="text" name="user_email" value="<?php if(isset($user_email)) { echo $user_email; } ?>" />
|
<?= __("Email Address"); ?>
|
||||||
<?php if(isset($email_error)) { echo "<small class=\"badge bg-danger\">".$email_error."</small>"; } ?>
|
<?php if ($idp_locked('user_email')) { echo $idp_badge; } ?>
|
||||||
|
</label>
|
||||||
|
<?php if ($idp_locked('user_email')) { ?>
|
||||||
|
<input class="form-control-plaintext fw-bold" name="user_email" value="<?= htmlspecialchars($user_email ?? '') ?>" readonly/>
|
||||||
|
<?php } else { ?>
|
||||||
|
<input class="form-control" type="text" name="user_email" value="<?php if(isset($user_email)) { echo $user_email; } ?>" />
|
||||||
|
<?php if(isset($email_error)) { echo "<small class=\"badge bg-danger\">".$email_error."</small>"; } ?>
|
||||||
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$auth_header_enable || ($auth_header_allow_direct_login && (!$external_account || !$auth_header_hide_password_field))){ ?>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label><?= __("Password"); ?></label>
|
<label><?= __("Password"); ?></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -79,6 +102,7 @@
|
|||||||
} else if (!isset($user_add)) { ?>
|
} else if (!isset($user_add)) { ?>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -98,7 +122,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<?php } else {
|
<?php } else {
|
||||||
$l = $this->config->item('auth_level');
|
$l = $this->config->item('auth_level');
|
||||||
echo $l[$user_type];
|
echo "<br><b>" . $l[$user_type] . "</b>";
|
||||||
}?>
|
}?>
|
||||||
<?php if ($clubstation) { ?>
|
<?php if ($clubstation) { ?>
|
||||||
<input type="hidden" name="clubstation" value="1" />
|
<input type="hidden" name="clubstation" value="1" />
|
||||||
@@ -114,17 +138,29 @@
|
|||||||
<div class="card-header"><?php if ($clubstation) { echo __("Callsign Owner"); } else { echo __("Personal");} ?></div>
|
<div class="card-header"><?php if ($clubstation) { echo __("Callsign Owner"); } else { echo __("Personal");} ?></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label><?= __("First Name"); ?></label>
|
<label>
|
||||||
<input class="form-control" type="text" name="user_firstname" value="<?php if(isset($user_firstname)) { echo $user_firstname; } ?>" />
|
<?= __("First Name"); ?>
|
||||||
<?php if(isset($firstname_error)) { echo "<small class=\"badge bg-danger\">".$firstname_error."</small>"; } else { ?>
|
<?php if ($idp_locked('user_firstname')) { echo $idp_badge; } ?>
|
||||||
<?php } ?>
|
</label>
|
||||||
|
<?php if ($idp_locked('user_firstname')) { ?>
|
||||||
|
<input class="form-control-plaintext fw-bold" name="user_firstname" value="<?= htmlspecialchars($user_firstname ?? '') ?>" readonly/>
|
||||||
|
<?php } else { ?>
|
||||||
|
<input class="form-control" type="text" name="user_firstname" value="<?php if(isset($user_firstname)) { echo $user_firstname; } ?>" />
|
||||||
|
<?php if(isset($firstname_error)) { echo "<small class=\"badge bg-danger\">".$firstname_error."</small>"; } ?>
|
||||||
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label><?= __("Last Name"); ?></label>
|
<label>
|
||||||
<input class="form-control" type="text" name="user_lastname" value="<?php if(isset($user_lastname)) { echo $user_lastname; } ?>" />
|
<?= __("Last Name"); ?>
|
||||||
<?php if(isset($lastname_error)) { echo "<small class=\"badge bg-danger\">".$lastname_error."</small>"; } else { ?>
|
<?php if ($idp_locked('user_lastname')) { echo $idp_badge; } ?>
|
||||||
<?php } ?>
|
</label>
|
||||||
|
<?php if ($idp_locked('user_lastname')) { ?>
|
||||||
|
<input class="form-control-plaintext fw-bold" name="user_lastname" value="<?= htmlspecialchars($user_lastname ?? '') ?>" readonly/>
|
||||||
|
<?php } else { ?>
|
||||||
|
<input class="form-control" type="text" name="user_lastname" value="<?php if(isset($user_lastname)) { echo $user_lastname; } ?>" />
|
||||||
|
<?php if(isset($lastname_error)) { echo "<small class=\"badge bg-danger\">".$lastname_error."</small>"; } ?>
|
||||||
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,17 +171,29 @@
|
|||||||
<div class="card-header"><?= __("Ham Radio"); ?></div>
|
<div class="card-header"><?= __("Ham Radio"); ?></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label><?php if ($clubstation) { echo __("Special/Club Callsign"); } else { echo __("Callsign"); } ?></label>
|
<label>
|
||||||
<input class="form-control uppercase" type="text" name="user_callsign" pattern="^\S+$" value="<?php if(isset($user_callsign)) { echo $user_callsign; } ?>" />
|
<?php if ($clubstation) { echo __("Special/Club Callsign"); } else { echo __("Callsign"); } ?>
|
||||||
<?php if(isset($callsign_error)) { echo "<small class=\"badge bg-danger\">".$callsign_error."</small>"; } else { ?>
|
<?php if ($idp_locked('user_callsign')) { echo $idp_badge; } ?>
|
||||||
<?php } ?>
|
</label>
|
||||||
|
<?php if ($idp_locked('user_callsign')) { ?>
|
||||||
|
<input class="form-control-plaintext fw-bold uppercase" name="user_callsign" value="<?= htmlspecialchars($user_callsign ?? '') ?>" readonly/>
|
||||||
|
<?php } else { ?>
|
||||||
|
<input class="form-control uppercase" type="text" name="user_callsign" pattern="^\S+$" value="<?php if(isset($user_callsign)) { echo $user_callsign; } ?>" />
|
||||||
|
<?php if(isset($callsign_error)) { echo "<small class=\"badge bg-danger\">".$callsign_error."</small>"; } ?>
|
||||||
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label><?= __("Gridsquare"); ?></label>
|
<label>
|
||||||
<input class="form-control uppercase" type="text" name="user_locator" value="<?php if(isset($user_locator)) { echo $user_locator; } ?>" />
|
<?= __("Gridsquare"); ?>
|
||||||
<?php if(isset($locator_error)) { echo "<small class=\"badge bg-danger\">".$locator_error."</small>"; } else { ?>
|
<?php if ($idp_locked('user_locator')) { echo $idp_badge; } ?>
|
||||||
<?php } ?>
|
</label>
|
||||||
|
<?php if ($idp_locked('user_locator')) { ?>
|
||||||
|
<input class="form-control-plaintext fw-bold uppercase" name="user_locator" value="<?= htmlspecialchars($user_locator ?? '') ?>" readonly/>
|
||||||
|
<?php } else { ?>
|
||||||
|
<input class="form-control uppercase" type="text" name="user_locator" value="<?php if(isset($user_locator)) { echo $user_locator; } ?>" />
|
||||||
|
<?php if(isset($locator_error)) { echo "<small class=\"badge bg-danger\">".$locator_error."</small>"; } ?>
|
||||||
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -109,37 +109,18 @@ $config['auth_mode'] = '3';
|
|||||||
$config['auth_level'][3] = 'Operator';
|
$config['auth_level'][3] = 'Operator';
|
||||||
$config['auth_level'][99] = 'Administrator';
|
$config['auth_level'][99] = 'Administrator';
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Third-Party Authentication (SSO)
|
| Third-Party Authentication (SSO)
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This section defines settings for third-party authentication over an external identity provider.
|
| Enable SSO support via a trusted HTTP header containing a JWT access token.
|
||||||
| When enabled, Wavelog will look for a specific HTTP header containing an access token, which can be used to authenticate users.
|
| When enabled, a sso.php config file is required (see sso.sample.php).
|
||||||
| This is particularly useful in environments where users are already authenticated through another system, such as a corporate SSO or an identity provider.
|
|
||||||
| Please refer to the documentation of Wavelog for more details on how to set up and use this feature.
|
|
||||||
| Link to documentation: https://docs.wavelog.org/admin-guide/configuration/third-party-authentication
|
|
||||||
|
|
|
|
||||||
| 'auth_header_enable' = Enable or disable third-party authentication via HTTP headers.
|
| Documentation: https://docs.wavelog.org/admin-guide/configuration/third-party-authentication
|
||||||
| 'auth_header_create' = Automatically create a local user account if a valid access token is provided and no corresponding user exists.
|
|
||||||
| 'auth_allow_direct_login' = Allow users to log in directly through the Wavelog login form even when third-party authentication is enabled. This can be useful for administrators or in cases where some users do not have access to the SSO system.
|
|
||||||
| 'auth_headers_accesstoken' = The name of the HTTP header that contains the access token. This should match the header sent by your SSO or authentication system.
|
|
||||||
| 'auth_headers_callsign_claim' = The JWT claim that contains the user's Callsign.
|
|
||||||
| 'auth_header_text' = The text displayed on the login button for third-party authentication.
|
|
||||||
| 'auth_header_club_id' = Optional club ID to assign to users authenticated via the header. This can be used to group users or assign specific permissions based on their club affiliation.
|
|
||||||
| 'auth_url_logout' = The URL to redirect users to when they log out. If OAuth2 Proxy and Keycloak is used, must hit OAuth2 Proxy first then Keycloak. whitelist_domains for OAuth2 Proxy must also be set to the IdP domain.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$config['auth_header_enable'] = false;
|
||||||
// $config['auth_header_enable'] = false;
|
|
||||||
// $config['auth_header_create'] = true;
|
|
||||||
// $config['auth_allow_direct_login'] = true;
|
|
||||||
// $config['auth_headers_accesstoken'] = "HTTP_X_FORWARDED_ACCESS_TOKEN";
|
|
||||||
// $config['auth_headers_callsign_claim'] = "callsign";
|
|
||||||
// $config['auth_header_text'] = "Login with SSO";
|
|
||||||
// $config['auth_header_club_id'] = "";
|
|
||||||
// $config['auth_url_logout'] = 'https://log.example.org/oauth2/sign_out?rd=https://auth.example.org/realms/example/protocol/openid-connect/logout';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user