Skip to content

Commit

Permalink
Add support for Azure Oauth
Browse files Browse the repository at this point in the history
  • Loading branch information
nitin-bhadauria committed Aug 27, 2024
1 parent a00fd5a commit da82f30
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 7 deletions.
143 changes: 141 additions & 2 deletions app/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("
<html>
<head>
<meta http-equiv='refresh' content='3;url=" . route('login.oauth') . "'>
</head>
<body>
<p>" . sprintf(lang('auth.new_user_signed_up'), $newUsername) . "</p>
</body>
</html>
");
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
Expand Down
2 changes: 1 addition & 1 deletion app/Controllers/Auth/PasswordRecoveryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
3 changes: 3 additions & 0 deletions app/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions config.example.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ],
];
6 changes: 5 additions & 1 deletion resources/lang/en.lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
];
2 changes: 2 additions & 0 deletions resources/schemas/mysql/mysql.8.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `users` ADD COLUMN `external` BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE users MODIFY password VARCHAR(255) NULL;
2 changes: 2 additions & 0 deletions resources/schemas/sqlite/sqlite.8.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `users` ADD COLUMN `external` BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE users MODIFY password VARCHAR(255) NULL;
11 changes: 11 additions & 0 deletions resources/templates/auth/login.twig
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@
</div>
</div>
</form>
{% if oauth_enabled %}
<div class="row mt-3">
<div class="col-md-12 text-center">
<div class="login-options">
<a href="{{ route('login.oauth') }}" class="btn btn-primary">
{{config.oauth.title}}
</a>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

Expand Down
15 changes: 13 additions & 2 deletions resources/templates/user/edit.twig
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
<div class="form-group row">
<label for="email" class="col-sm-3 col-form-label">Email</label>
<div class="col-sm-9">
<input type="email" class="form-control" id="email" placeholder="email@example.com" name="email" value="{{ user.email }}" autocomplete="off" required>
<input type="email" class="form-control" id="email" placeholder="email@example.com" name="email" value="{{ user.email }}" autocomplete="off" {{ user.external ? ' disabled' : '' }} required>
{% if user.external %}
<input type="hidden" name="email" value="{{ user.email }}">
{% endif %}
</div>
</div>
<div class="form-group row">
Expand All @@ -33,7 +36,7 @@
<div class="form-group row">
<label for="password" class="col-sm-3 col-form-label">{{ lang('password') }}</label>
<div class="col-sm-9">
<input type="password" class="form-control" id="password" placeholder="{{ lang('password') }}" name="password" autocomplete="off"{{ user.ldap ? ' disabled' }}>
<input type="password" class="form-control" id="password" placeholder="{{ lang('password') }}" name="password" autocomplete="off"{{ user.ldap or user.external ? ' disabled' }}>
</div>
</div>
<div class="form-group row">
Expand Down Expand Up @@ -106,6 +109,14 @@
</div>
</div>
{% endif %}
{% if config.oauth.enabled %}
<div class="form-group row">
<label for="external" class="col-sm-3 col-form-label">OAuth User</label>
<div class="col-sm-9">
<input type="checkbox" name="external" data-toggle="toggle" data-off="{{ lang('no') }}" data-on="{{ lang('yes') }}" {{ user.external ? 'checked' }} disabled>
</div>
</div>
{% endif %}
{% if quota_enabled == 'on' %}
<div class="form-group row">
<label for="max_user_quota" class="col-sm-3 col-form-label">{{ lang('max_user_quota') }}</label>
Expand Down

0 comments on commit da82f30

Please sign in to comment.