mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 02:14:13 +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/database.php
|
||||
/application/config/sso.php
|
||||
/application/config/redis.php
|
||||
/application/config/memcached.php
|
||||
/application/config/**/config.php
|
||||
/application/config/**/database.php
|
||||
/application/config/**/sso.php
|
||||
/application/config/**/redis.php
|
||||
/application/config/**/memcached.php
|
||||
/application/logs/*
|
||||
|
||||
@@ -109,37 +109,18 @@ $config['auth_mode'] = '3';
|
||||
$config['auth_level'][3] = 'Operator';
|
||||
$config['auth_level'][99] = 'Administrator';
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third-Party Authentication (SSO)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This section defines settings for third-party authentication over an external identity provider.
|
||||
| When enabled, Wavelog will look for a specific HTTP header containing an access token, which can be used to authenticate users.
|
||||
| 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
|
||||
| Enable SSO support via a trusted HTTP header containing a JWT access token.
|
||||
| When enabled, a sso.php config file is required (see sso.sample.php).
|
||||
|
|
||||
| 'auth_header_enable' = Enable or disable third-party authentication via HTTP headers.
|
||||
| '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.
|
||||
| Documentation: https://docs.wavelog.org/admin-guide/configuration/third-party-authentication
|
||||
*/
|
||||
|
||||
|
||||
// $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';
|
||||
$config['auth_header_enable'] = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -936,7 +917,6 @@ $config['enable_dxcluster_file_cache_worked'] = false;
|
||||
*/
|
||||
$config['dxcluster_refresh_time'] = 30;
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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');
|
||||
|
||||
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
|
||||
@@ -9,19 +16,28 @@ class Header_auth extends CI_Controller {
|
||||
|
||||
public function __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() {
|
||||
// Guard: feature must be enabled
|
||||
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
|
||||
$accesstoken_path = $this->config->item('auth_headers_accesstoken') ?? false;
|
||||
$accesstoken_path = $this->config->item('auth_header_accesstoken', 'sso') ?? false;
|
||||
if (!$accesstoken_path) {
|
||||
log_message('error', 'SSO Authentication: Access Token Path not configured in config.php.');
|
||||
$this->_sso_error();
|
||||
@@ -32,39 +48,43 @@ class Header_auth extends CI_Controller {
|
||||
$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)) {
|
||||
log_message('error', 'SSO Authentication: Invalid access token format. Failed to decode JWT token.');
|
||||
$this->_sso_error();
|
||||
$this->_sso_error("SSO Authentication failed. Invalid token.");
|
||||
}
|
||||
|
||||
if (!$this->_verify_jwtdata($claims)) {
|
||||
log_message('error', 'SSO Authentication: Token validation failed.');
|
||||
$this->_sso_error();
|
||||
if ($this->config->item('auth_header_debug_jwt', 'sso')) {
|
||||
log_message('debug', 'Decoded and validated JWT: ' . json_encode($claims, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
$callsign_claim = $this->config->item('auth_headers_callsign_claim') ?? 'callsign';
|
||||
$claim_map = $this->config->item('auth_headers_claim_config', 'sso');
|
||||
|
||||
$username = $claims['preferred_username'] ?? '';
|
||||
$email = $claims['email'] ?? '';
|
||||
$callsign = $claims[$callsign_claim] ?? '';
|
||||
$firstname = $claims['given_name'] ?? '';
|
||||
$lastname = $claims['family_name'] ?? '';
|
||||
|
||||
if (empty($username)) {
|
||||
log_message('error', 'SSO Authentication: Missing username claim in access token.');
|
||||
$this->_sso_error();
|
||||
// Extract all mapped claims dynamically — supports custom fields added by the admin
|
||||
$mapped = [];
|
||||
foreach ($claim_map as $db_field => $cfg) {
|
||||
$mapped[$db_field] = $claims[$cfg['claim']] ?? null;
|
||||
}
|
||||
|
||||
// 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');
|
||||
$query = $this->user_model->get($username);
|
||||
$query = $this->user_model->get_by_external_account($external_identifier);
|
||||
|
||||
if (!$query || $query->num_rows() !== 1) {
|
||||
// Config check if create user
|
||||
if ($this->config->item('auth_header_create')) {
|
||||
$this->_create_user($username, $email, $callsign, $firstname, $lastname);
|
||||
$query = $this->user_model->get($username);
|
||||
if ($this->config->item('auth_header_create', 'sso')) {
|
||||
$this->_create_user($mapped, $external_identifier);
|
||||
$query = $this->user_model->get_by_external_account($external_identifier);
|
||||
} else {
|
||||
$this->_sso_error(__("User not found."));
|
||||
return;
|
||||
@@ -72,13 +92,16 @@ class Header_auth extends CI_Controller {
|
||||
}
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
$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)
|
||||
if (!empty($user->clubstation) && $user->clubstation == 1) {
|
||||
$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
|
||||
*
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function _decode_jwt_payload(string $token): ?array {
|
||||
$parts = explode('.', $token);
|
||||
if (count($parts) !== 3) {
|
||||
private function _verify_jwt(string $token): ?array {
|
||||
$jwksUri = $this->config->item('auth_header_jwks_uri', 'sso');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$decode = 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 = $decode($parts[0]);
|
||||
$payload = $decode($parts[1]);
|
||||
if ($payload === null) {
|
||||
if (isset($claims['typ']) && $claims['typ'] !== 'Bearer') {
|
||||
log_message('error', 'SSO Authentication: JWT Token is no Bearer Token.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Merge alg from header into payload so _verify_jwtdata can check it
|
||||
if (isset($header['alg'])) {
|
||||
$payload['alg'] = $header['alg'];
|
||||
}
|
||||
|
||||
return $payload;
|
||||
return $claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to verify some long hangig fruits. We are not verifying the JWT token against the issuer at this point.
|
||||
* 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.
|
||||
*
|
||||
* Additonal verificarion steps can be added at a later point.
|
||||
*
|
||||
* @param array claim data
|
||||
*
|
||||
* @return bool
|
||||
* Update user fields from JWT claims where override_on_update is enabled.
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param array $claim_map
|
||||
* @param array $values Associative array of field => value from the JWT
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function _verify_jwtdata(?array $claims = null): bool {
|
||||
// No claim, no verificiation
|
||||
if (!$claims) {
|
||||
log_message('error', 'JWT Verification: No claim data received.');
|
||||
return false;
|
||||
private function _update_user_from_claims(int $user_id, array $mapped): void {
|
||||
$updates = [];
|
||||
$claim_map = $this->config->item('auth_headers_claim_config', 'sso');
|
||||
foreach ($claim_map as $db_field => $cfg) {
|
||||
if (!empty($cfg['override_on_update']) && $mapped[$db_field] !== null) {
|
||||
$updates[$db_field] = $mapped[$db_field];
|
||||
}
|
||||
}
|
||||
|
||||
// Check expire date
|
||||
if (($claims['exp'] ?? 0) < time()) {
|
||||
log_message('error', 'JWT Verification: JWT Token is expired.');
|
||||
return false;
|
||||
if (!empty($updates)) {
|
||||
$this->user_model->update_sso_claims($user_id, $updates);
|
||||
}
|
||||
|
||||
// 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.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $email
|
||||
* @param string $callsign
|
||||
* @param string $firstname
|
||||
* @param string $lastname
|
||||
*
|
||||
* @param array $mapped All DB field => value pairs extracted from JWT claims
|
||||
* @param string $external_identifier Composite key JSON {iss, sub} — stored once, never updated
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function _create_user($username, $email, $callsign, $firstname, $lastname) {
|
||||
if (empty($email) || empty($callsign)) {
|
||||
private function _create_user(array $mapped, string $external_identifier) {
|
||||
if (empty($mapped['user_email']) || empty($mapped['user_callsign'])) {
|
||||
log_message('error', 'SSO Authentication: Missing email or callsign claim in access token.');
|
||||
$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');
|
||||
$result = $this->user_model->add(
|
||||
$username,
|
||||
bin2hex(random_bytes(64)), // password
|
||||
$email,
|
||||
3, // $data['user_type'], we don't create admins for security reasons
|
||||
$firstname,
|
||||
$lastname,
|
||||
$callsign,
|
||||
"", // locator
|
||||
24, // user_timezone is default UTC
|
||||
"M", // measurement
|
||||
"Y", // dashboard_map
|
||||
"Y-m-d", // user_date_format
|
||||
'darkly', // user_stylesheet
|
||||
'0', // user_qth_lookup
|
||||
'0', // user_sota_lookup
|
||||
'0', // user_wwff_lookup
|
||||
'0', // user_pota_lookup
|
||||
1, // user_show_notes
|
||||
'Mode', // user_column1
|
||||
'RSTS', // user_column2
|
||||
'RSTR', // user_column3
|
||||
'Band', // user_column4
|
||||
'Country', // user_column5
|
||||
'0', // user_show_profile_image
|
||||
'0', // user_previous_qsl_type
|
||||
'0', // user_amsat_status_upload
|
||||
'', // user_mastodon_url
|
||||
'ALL', // user_default_band
|
||||
'QL', // user_default_confirmation
|
||||
'0', // user_qso_end_times
|
||||
"Y", // user_qso_db_search_priority
|
||||
'0', // user_quicklog
|
||||
'0', // user_quicklog_enter
|
||||
"english", // user_language
|
||||
'', // user_hamsat_key
|
||||
'', // user_hamsat_workable_only
|
||||
'', // user_iota_to_qso_tab
|
||||
'', // user_sota_to_qso_tab
|
||||
'', // user_wwff_to_qso_tab
|
||||
'', // user_pota_to_qso_tab
|
||||
'', // user_sig_to_qso_tab
|
||||
'', // user_dok_to_qso_tab
|
||||
0, // user_station_to_qso_tab
|
||||
'', // user_lotw_name
|
||||
'', // user_lotw_password
|
||||
'', // user_eqsl_name
|
||||
'', // user_eqsl_password
|
||||
'', // user_clublog_name
|
||||
'', // user_clublog_password
|
||||
'0', // 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
|
||||
"", // oqrs_grouped_search_show_station_name
|
||||
"", // oqrs_auto_matching
|
||||
"", // oqrs_direct_auto_matching
|
||||
"", // user_dxwaterfall_enable
|
||||
"", // user_qso_show_map
|
||||
0, // clubstation
|
||||
true, // external_account
|
||||
$mapped['user_name'] ?? '',
|
||||
bin2hex(random_bytes(64)), // password is always random
|
||||
$mapped['user_email'] ?? '',
|
||||
3, // user_type: never admin via SSO
|
||||
$mapped['user_firstname'] ?? '',
|
||||
$mapped['user_lastname'] ?? '',
|
||||
$mapped['user_callsign'] ?? '',
|
||||
$mapped['user_locator'] ?? '',
|
||||
$mapped['user_timezone'] ?? 24,
|
||||
$mapped['user_measurement_base'] ?? 'M',
|
||||
$mapped['dashboard_map'] ?? 'Y',
|
||||
$mapped['user_date_format'] ?? 'Y-m-d',
|
||||
$mapped['user_stylesheet'] ?? 'darkly',
|
||||
$mapped['user_qth_lookup'] ?? '0',
|
||||
$mapped['user_sota_lookup'] ?? '0',
|
||||
$mapped['user_wwff_lookup'] ?? '0',
|
||||
$mapped['user_pota_lookup'] ?? '0',
|
||||
$mapped['user_show_notes'] ?? 1,
|
||||
$mapped['user_column1'] ?? 'Mode',
|
||||
$mapped['user_column2'] ?? 'RSTS',
|
||||
$mapped['user_column3'] ?? 'RSTR',
|
||||
$mapped['user_column4'] ?? 'Band',
|
||||
$mapped['user_column5'] ?? 'Country',
|
||||
$mapped['user_show_profile_image'] ?? '0',
|
||||
$mapped['user_previous_qsl_type'] ?? '0',
|
||||
$mapped['user_amsat_status_upload'] ?? '0',
|
||||
$mapped['user_mastodon_url'] ?? '',
|
||||
$mapped['user_default_band'] ?? 'ALL',
|
||||
$mapped['user_default_confirmation'] ?? 'QL',
|
||||
$mapped['user_qso_end_times'] ?? '0',
|
||||
$mapped['user_qso_db_search_priority'] ?? 'Y',
|
||||
$mapped['user_quicklog'] ?? '0',
|
||||
$mapped['user_quicklog_enter'] ?? '0',
|
||||
$mapped['user_language'] ?? 'english',
|
||||
$mapped['user_hamsat_key'] ?? '',
|
||||
$mapped['user_hamsat_workable_only'] ?? '',
|
||||
$mapped['user_iota_to_qso_tab'] ?? '',
|
||||
$mapped['user_sota_to_qso_tab'] ?? '',
|
||||
$mapped['user_wwff_to_qso_tab'] ?? '',
|
||||
$mapped['user_pota_to_qso_tab'] ?? '',
|
||||
$mapped['user_sig_to_qso_tab'] ?? '',
|
||||
$mapped['user_dok_to_qso_tab'] ?? '',
|
||||
$mapped['user_station_to_qso_tab'] ?? 0,
|
||||
$mapped['user_lotw_name'] ?? '',
|
||||
$mapped['user_lotw_password'] ?? '',
|
||||
$mapped['user_eqsl_name'] ?? '',
|
||||
$mapped['user_eqsl_password'] ?? '',
|
||||
$mapped['user_clublog_name'] ?? '',
|
||||
$mapped['user_clublog_password'] ?? '',
|
||||
$mapped['user_winkey'] ?? '0',
|
||||
$mapped['on_air_widget_enabled'] ?? '',
|
||||
$mapped['on_air_widget_display_last_seen'] ?? '',
|
||||
$mapped['on_air_widget_show_only_most_recent_radio'] ?? '',
|
||||
$mapped['qso_widget_display_qso_time'] ?? '',
|
||||
$mapped['dashboard_banner'] ?? '',
|
||||
$mapped['dashboard_solar'] ?? '',
|
||||
$mapped['global_oqrs_text'] ?? '',
|
||||
$mapped['oqrs_grouped_search'] ?? '',
|
||||
$mapped['oqrs_grouped_search_show_station_name'] ?? '',
|
||||
$mapped['oqrs_auto_matching'] ?? '',
|
||||
$mapped['oqrs_direct_auto_matching'] ?? '',
|
||||
$mapped['user_dxwaterfall_enable'] ?? '',
|
||||
$mapped['user_qso_show_map'] ?? '',
|
||||
0, // clubstation
|
||||
$external_identifier, // external_account
|
||||
);
|
||||
|
||||
switch ($result) {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
class User extends CI_Controller {
|
||||
|
||||
private $pwd_placeholder = '**********';
|
||||
|
||||
public function index()
|
||||
{
|
||||
$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));
|
||||
|
||||
$data['existing_languages'] = $this->config->item('languages');
|
||||
$pwd_placeholder = '**********';
|
||||
|
||||
$this->load->model('bands');
|
||||
$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
|
||||
$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");
|
||||
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
@@ -465,7 +477,7 @@ class User extends CI_Controller {
|
||||
$data['user_password'] = $this->input->post('user_password',true);
|
||||
} else {
|
||||
if ($q->user_password !== '' && $q->user_password !== null) {
|
||||
$data['user_password'] = $pwd_placeholder;
|
||||
$data['user_password'] = $this->pwd_placeholder;
|
||||
} else {
|
||||
$data['user_password'] = '';
|
||||
}
|
||||
@@ -535,7 +547,7 @@ class User extends CI_Controller {
|
||||
$data['user_clublog_password'] = $this->input->post('user_clublog_password', true);
|
||||
} else {
|
||||
if ($q->user_clublog_password !== '' && $q->user_clublog_password !== null) {
|
||||
$data['user_clublog_password'] = $pwd_placeholder;
|
||||
$data['user_clublog_password'] = $this->pwd_placeholder;
|
||||
} else {
|
||||
$data['user_clublog_password'] = '';
|
||||
}
|
||||
@@ -545,7 +557,7 @@ class User extends CI_Controller {
|
||||
$data['user_lotw_password'] = $this->input->post('user_lotw_password', true);
|
||||
} else {
|
||||
if ($q->user_lotw_password !== '' && $q->user_lotw_password !== null) {
|
||||
$data['user_lotw_password'] = $pwd_placeholder;
|
||||
$data['user_lotw_password'] = $this->pwd_placeholder;
|
||||
} else {
|
||||
$data['user_lotw_password'] = '';
|
||||
}
|
||||
@@ -561,7 +573,7 @@ class User extends CI_Controller {
|
||||
$data['user_eqsl_password'] = $this->input->post('user_eqsl_password', true);
|
||||
} else {
|
||||
if ($q->user_eqsl_password !== '' && $q->user_eqsl_password !== null) {
|
||||
$data['user_eqsl_password'] = $pwd_placeholder;
|
||||
$data['user_eqsl_password'] = $this->pwd_placeholder;
|
||||
} else {
|
||||
$data['user_eqsl_password'] = '';
|
||||
}
|
||||
@@ -953,7 +965,17 @@ class User extends CI_Controller {
|
||||
}
|
||||
|
||||
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
|
||||
case EUSERNAMEEXISTS:
|
||||
$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) {
|
||||
$data['page_title'] = __("Login");
|
||||
$data['https_check'] = $this->https_check();
|
||||
|
||||
$data['auth_header_enable'] = $this->config->item('auth_header_enable') ?? false;
|
||||
$data['auth_header_text'] = $this->config->item('auth_header_text') ?: '';
|
||||
$data['hide_login_form'] = ($data['auth_header_enable'] && !($this->config->item('auth_allow_direct_login') ?? true));
|
||||
if ($data['auth_header_enable']) {
|
||||
// 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('user/login');
|
||||
$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, '');
|
||||
}
|
||||
|
||||
$logout = $this->config->item('auth_url_logout');
|
||||
if ($this->config->item('auth_header_enable') && $logout !== null && $logout !== '') {
|
||||
redirect($logout);
|
||||
if ($this->config->item('auth_header_enable')) {
|
||||
$this->config->load('sso', true, true);
|
||||
$logout = $this->config->item('auth_header_url_logout', 'sso') ?: null;
|
||||
if ($logout !== null) {
|
||||
redirect($logout);
|
||||
}
|
||||
}
|
||||
|
||||
redirect('user/login');
|
||||
|
||||
@@ -4,7 +4,7 @@ defined('BASEPATH') or exit('No direct script access allowed');
|
||||
class Migration_external_account extends CI_Migration {
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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_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,
|
||||
$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
|
||||
if(!$this->exists($username)) {
|
||||
$data = array(
|
||||
@@ -540,7 +540,7 @@ class User_Model extends CI_Model {
|
||||
// This is really just a wrapper around User_Model::authenticate
|
||||
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.');
|
||||
redirect('user/login');
|
||||
}
|
||||
@@ -753,6 +753,18 @@ class User_Model extends CI_Model {
|
||||
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_last_seen($user_id) {
|
||||
$data = array(
|
||||
|
||||
@@ -53,18 +53,41 @@
|
||||
<div class="card">
|
||||
<div class="card-header"><?= __("Account"); ?></div>
|
||||
<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">
|
||||
<label><?= __("Username"); ?></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'; } ?> />
|
||||
<?php if(isset($username_error)) { echo "<small class=\"badge bg-danger\">".$username_error."</small>"; } ?>
|
||||
<label>
|
||||
<?= __("Username"); ?>
|
||||
<?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 class="mb-3">
|
||||
<label><?= __("Email Address"); ?></label>
|
||||
<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>"; } ?>
|
||||
<label>
|
||||
<?= __("Email Address"); ?>
|
||||
<?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>
|
||||
|
||||
<?php if (!$auth_header_enable || ($auth_header_allow_direct_login && (!$external_account || !$auth_header_hide_password_field))){ ?>
|
||||
<div class="mb-3">
|
||||
<label><?= __("Password"); ?></label>
|
||||
<div class="input-group">
|
||||
@@ -79,6 +102,7 @@
|
||||
} else if (!isset($user_add)) { ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<hr/>
|
||||
<div class="mb-3">
|
||||
@@ -98,7 +122,7 @@
|
||||
</select>
|
||||
<?php } else {
|
||||
$l = $this->config->item('auth_level');
|
||||
echo $l[$user_type];
|
||||
echo "<br><b>" . $l[$user_type] . "</b>";
|
||||
}?>
|
||||
<?php if ($clubstation) { ?>
|
||||
<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-body">
|
||||
<div class="mb-3">
|
||||
<label><?= __("First Name"); ?></label>
|
||||
<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>"; } else { ?>
|
||||
<?php } ?>
|
||||
<label>
|
||||
<?= __("First Name"); ?>
|
||||
<?php if ($idp_locked('user_firstname')) { echo $idp_badge; } ?>
|
||||
</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 class="mb-3">
|
||||
<label><?= __("Last Name"); ?></label>
|
||||
<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>"; } else { ?>
|
||||
<?php } ?>
|
||||
<label>
|
||||
<?= __("Last Name"); ?>
|
||||
<?php if ($idp_locked('user_lastname')) { echo $idp_badge; } ?>
|
||||
</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>
|
||||
@@ -135,17 +171,29 @@
|
||||
<div class="card-header"><?= __("Ham Radio"); ?></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label><?php if ($clubstation) { echo __("Special/Club Callsign"); } else { echo __("Callsign"); } ?></label>
|
||||
<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>"; } else { ?>
|
||||
<?php } ?>
|
||||
<label>
|
||||
<?php if ($clubstation) { echo __("Special/Club Callsign"); } else { echo __("Callsign"); } ?>
|
||||
<?php if ($idp_locked('user_callsign')) { echo $idp_badge; } ?>
|
||||
</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 class="mb-3">
|
||||
<label><?= __("Gridsquare"); ?></label>
|
||||
<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>"; } else { ?>
|
||||
<?php } ?>
|
||||
<label>
|
||||
<?= __("Gridsquare"); ?>
|
||||
<?php if ($idp_locked('user_locator')) { echo $idp_badge; } ?>
|
||||
</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>
|
||||
|
||||
@@ -109,37 +109,18 @@ $config['auth_mode'] = '3';
|
||||
$config['auth_level'][3] = 'Operator';
|
||||
$config['auth_level'][99] = 'Administrator';
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third-Party Authentication (SSO)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This section defines settings for third-party authentication over an external identity provider.
|
||||
| When enabled, Wavelog will look for a specific HTTP header containing an access token, which can be used to authenticate users.
|
||||
| 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
|
||||
| Enable SSO support via a trusted HTTP header containing a JWT access token.
|
||||
| When enabled, a sso.php config file is required (see sso.sample.php).
|
||||
|
|
||||
| 'auth_header_enable' = Enable or disable third-party authentication via HTTP headers.
|
||||
| '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.
|
||||
| Documentation: https://docs.wavelog.org/admin-guide/configuration/third-party-authentication
|
||||
*/
|
||||
|
||||
|
||||
// $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';
|
||||
$config['auth_header_enable'] = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user