diff --git a/app/Controllers/Auth/LoginController.php b/app/Controllers/Auth/LoginController.php index dd9014b3..496d581a 100644 --- a/app/Controllers/Auth/LoginController.php +++ b/app/Controllers/Auth/LoginController.php @@ -51,8 +51,21 @@ public function login(Request $request, Response $response): Response $password = param($request, 'password'); $user = $this->database->query('SELECT `id`, `email`, `username`, `password`,`is_admin`, `active`, `current_disk_quota`, `max_disk_quota`, `ldap`, `copy_raw` FROM `users` WHERE `username` = ? OR `email` = ? LIMIT 1', [$username, $username])->fetch(); - if ($this->config['ldap']['enabled'] && (!$user || $user->ldap ?? true)) { - $user = $this->ldapLogin($request, $username, param($request, 'password'), $user); + // Enforce that external users cannot log in with a password + if ($user && $user->external) { + $this->logger->info("Login attempt with password for external user '{$username}' blocked."); + // Handling the error with session alert + $this->session->alert(lang('auth.external_user_password_blocked', [$user->username]), 'danger'); + return redirect($response, route('login')); + } else { + // Proceed with LDAP login or password verification for non-external users + if ($this->config['ldap']['enabled'] && (!$user || $user->ldap ?? true)) { + $user = $this->ldapLogin($request, $username, $password, $user); + } + + if (!$user || !password_verify($password, $user->password)) { + $validator->alert('bad_login'); + } } $validator @@ -111,6 +124,132 @@ public function logout(Request $request, Response $response): Response return redirect($response, route('login.show')); } + /** + * Configure Oauth provider + */ + private function getOAuthProvider(): Azure + { + return new Azure([ + 'clientId' => $this->config['oauth']['clientId'], + 'clientSecret' => $this->config['oauth']['clientSecret'], + 'redirectUri' => $this->config['oauth']['redirectUri'], + 'urlAuthorize' => $this->config['oauth']['urlAuthorize'], + 'urlAccessToken' => $this->config['oauth']['urlAccessToken'], + 'urlResourceOwnerDetails' => $this->config['oauth']['urlResourceOwnerDetails'], + 'scopes' => $this->config['oauth']['scopes'], + 'defaultEndPointVersion' => $this->config['oauth']['defaultEndPointVersion'], + 'tenant' => $this->config['oauth']['tenant_id'] + ]); + } + + /** + * Redirect to Azure AD for authentication + */ + public function initiateOAuthLogin(Response $response): Response + { + $provider = $this->getOAuthProvider(); + + $authorizationUrl = $provider->getAuthorizationUrl(); + $_SESSION['OAuth2.state'] = $provider->getState(); + + return $response->withHeader('Location', $authorizationUrl)->withStatus(302); + } + + /** + * Handle the callback from Azure AD after authentication + */ + public function handleOAuthCallback(Request $request, Response $response): Response + { + $provider = $this->getOAuthProvider(); + + if (empty($request->getQueryParams()['state']) || ($request->getQueryParams()['state'] !== $_SESSION['OAuth2.state'])) { + unset($_SESSION['OAuth2.state']); + $response->getBody()->write(lang('auth.invalid_oauth_state')); + return $response->withStatus(400); + } + + try { + $token = $provider->getAccessToken('authorization_code', [ + 'code' => $request->getQueryParams()['code'] + ]); + + $resourceOwner = $provider->getResourceOwner($token); + $user = $resourceOwner->toArray(); + + $existingUser = $this->database->query('SELECT * FROM users WHERE email = ? LIMIT 1', [$user['email']])->fetch(); + + if ($existingUser) { + $this->database->query('UPDATE users SET username = ?, external = 1 WHERE id = ?', [ + $user['preferred_username'], + $existingUser->id + ]); + $this->setupUserSession($existingUser); + session_write_close(); + return redirect($response, route('home'))->withStatus(302); + } else { + $userCode = $this->generateUserCode(); + $this->database->query('INSERT INTO users (username, email, external, user_code) VALUES (?, ?, 1, ?)', [ + $user['preferred_username'], + $user['email'], + $userCode + ]); + $newUsername = $user['preferred_username']; + + $response->getBody()->write(" + + + + + +

" . sprintf(lang('auth.new_user_signed_up'), $newUsername) . "

+ + + "); + session_write_close(); + return $response->withStatus(200)->withHeader('Content-Type', 'text/html'); + } + + } catch (\Exception $e) { + $response->getBody()->write(sprintf(lang('auth.failed_to_get_access_token'), $e->getMessage())); + return $response->withStatus(400); + } + + // Always return a response interface + return $response; + } + + /** + * Set up session data for the authenticated user + * + * @param $user + */ + private function setupUserSession($user) + { + $this->session->set('logged', true) + ->set('user_id', $user->id) + ->set('username', $user->username) + ->set('admin', $user->is_admin) + ->set('copy_raw', $user->copy_raw); + + $this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota); + + $this->session->alert(lang('welcome', [$user->username]), 'info'); + $this->logger->info("User {$user->username} logged in."); + } + + /** + * Generate a unique 5-character code for the user + */ + private function generateUserCode(): string + { + do { + $code = substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 5); + $existingCode = $this->database->query('SELECT id FROM users WHERE user_code = ? LIMIT 1', [$code])->fetch(); + } while ($existingCode); + + return $code; + } + /** * @param Request $request * @param string $username diff --git a/app/Controllers/Auth/PasswordRecoveryController.php b/app/Controllers/Auth/PasswordRecoveryController.php index e0abbb4a..93ae77d3 100644 --- a/app/Controllers/Auth/PasswordRecoveryController.php +++ b/app/Controllers/Auth/PasswordRecoveryController.php @@ -42,7 +42,7 @@ public function recoverMail(Request $request, Response $response): Response return redirect($response, route('recover')); } - $user = $this->database->query('SELECT `id`, `username` FROM `users` WHERE `email` = ? AND NOT `ldap` LIMIT 1', param($request, 'email'))->fetch(); + $user = $this->database->query('SELECT `id`, `username` FROM `users` WHERE `email` = ? AND (`ldap` = 0 AND `external` = 0) LIMIT 1', param($request, 'email'))->fetch(); if (!isset($user->id)) { $this->session->alert(lang('recover_email_sent'), 'success'); diff --git a/app/routes.php b/app/routes.php index 348e6e32..958a0a06 100755 --- a/app/routes.php +++ b/app/routes.php @@ -91,3 +91,6 @@ $app->post('/{userCode}/{mediaCode}/delete/{token}', [MediaController::class, 'deleteByToken'])->setName('public.delete')->add(CheckForMaintenanceMiddleware::class); $app->get('/{userCode}/{mediaCode}/raw[.{ext}]', [MediaController::class, 'getRaw'])->setName('public.raw'); $app->get('/{userCode}/{mediaCode}/download', [MediaController::class, 'download'])->setName('public.download'); + +$app->get('/login/oauth', [LoginController::class, 'initiateOAuthLogin'])->setName('login.oauth'); +$app->get('/login/oauth/callback', [LoginController::class, 'handleOAuthCallback'])->setName('login.oauth.callback'); \ No newline at end of file diff --git a/composer.json b/composer.json index 000af6b9..221b691d 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "slim/slim": "^4.0", "spatie/flysystem-dropbox": "^1.0", "superbalist/flysystem-google-storage": "^7.2", - "twig/twig": "^2.14" + "twig/twig": "^2.14", + "thenetworg/oauth2-azure": "^2.2" }, "config": { "optimize-autoloader": true, diff --git a/config.example.php b/config.example.php index f79cbe4c..6873f92f 100644 --- a/config.example.php +++ b/config.example.php @@ -12,4 +12,18 @@ 'driver' => 'local', 'path' => realpath(__DIR__).'/storage', ], + // Sample config for Azure AD oauth + // 'oauth' => [ + // 'title' => "Login with Azure AD", + // 'enabled' => true, // Set to true to enable OAuth login + // 'clientId' => '[client_id]', // Azure AD Application (client) ID + // 'clientSecret' => '[client_secret]', // Azure AD Client Secret + // 'redirectUri' => 'http://localhost:8080/login/oauth/callback', // Redirect URI configured in Azure + // 'urlAuthorize' => 'https://login.microsoftonline.com/[tenant_id]/oauth2/v2.0/authorize', + // 'urlAccessToken' => 'https://login.microsoftonline.com/[tenant_id]/oauth2/v2.0/token', + // 'urlResourceOwnerDetails' => '', + // 'scopes' => ['openid', 'profile', 'email', 'User.Read'], // Adjust scopes as needed + // 'defaultEndPointVersion' => '2.0', // Use v2.0 endpoint + // 'tenant_id' => '[tenant_id]', // Azure AD tenant_id + // ], ]; diff --git a/resources/lang/en.lang.php b/resources/lang/en.lang.php index 32c1a544..7b54e6a8 100755 --- a/resources/lang/en.lang.php +++ b/resources/lang/en.lang.php @@ -161,5 +161,9 @@ 'zip_ext_not_loaded' => 'The required "zip" extension is not loaded', 'changelog' => 'Changelog', 'show_changelog' => 'Show changelog', - 'image_embeds' => 'Embed images' + 'image_embeds' => 'Embed images', + 'auth.external_user_password_blocked' => 'Login attempt with password for external user %s is blocked. Try Login with OAuth.', + 'auth.invalid_oauth_state' => 'Invalid OAuth state.', + 'auth.new_user_signed_up' => 'New user %s signed up. You will be redirected to login with OAuth in 3 seconds...', + 'auth.failed_to_get_access_token' => 'Failed to get access token: %s.' ]; diff --git a/resources/schemas/mysql/mysql.8.sql b/resources/schemas/mysql/mysql.8.sql new file mode 100644 index 00000000..5d93a55a --- /dev/null +++ b/resources/schemas/mysql/mysql.8.sql @@ -0,0 +1,2 @@ +ALTER TABLE `users` ADD COLUMN `external` BOOLEAN NOT NULL DEFAULT 0; +ALTER TABLE users MODIFY password VARCHAR(255) NULL; \ No newline at end of file diff --git a/resources/schemas/sqlite/sqlite.8.sql b/resources/schemas/sqlite/sqlite.8.sql new file mode 100644 index 00000000..5d93a55a --- /dev/null +++ b/resources/schemas/sqlite/sqlite.8.sql @@ -0,0 +1,2 @@ +ALTER TABLE `users` ADD COLUMN `external` BOOLEAN NOT NULL DEFAULT 0; +ALTER TABLE users MODIFY password VARCHAR(255) NULL; \ No newline at end of file diff --git a/resources/templates/auth/login.twig b/resources/templates/auth/login.twig index c79db438..f270de9d 100644 --- a/resources/templates/auth/login.twig +++ b/resources/templates/auth/login.twig @@ -62,6 +62,17 @@ + {% if oauth_enabled %} +
+
+ +
+
+ {% endif %} {% endblock %} diff --git a/resources/templates/user/edit.twig b/resources/templates/user/edit.twig index 4a56f1a9..4ee1619a 100644 --- a/resources/templates/user/edit.twig +++ b/resources/templates/user/edit.twig @@ -17,7 +17,10 @@
- + + {% if user.external %} + + {% endif %}
@@ -33,7 +36,7 @@
- +
@@ -106,6 +109,14 @@
{% endif %} + {% if config.oauth.enabled %} +
+ +
+ +
+
+ {% endif %} {% if quota_enabled == 'on' %}