Skip to content

Commit

Permalink
refactor: moved EntraID related code into own class (#3202)
Browse files Browse the repository at this point in the history
  • Loading branch information
thorsten committed Nov 2, 2024
1 parent 1577aab commit 73f9ab7
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 135 deletions.
2 changes: 1 addition & 1 deletion docs/administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ Follow these steps to create an App Registration in Microsoft Azure:

1. In the "Name" field, provide a name for your App Registration, e.g. "phpMyFAQ".
2. Choose the supported account types that your application will authenticate: "Accounts in this organizational directory only"
3. In the "Redirect URI" section, specify the redirect URI where Entra ID will send authentication responses: `http://www.example.com/faq/services/azure/callback.php`
3. In the "Redirect URI" section, specify the redirect URI where Entra ID will send authentication responses: `http://www.example.com/faq/services/entra-id/callback.php`
4. Click the "Register" button to create the App Registration.

**Step 5: Configure Authentication**
Expand Down
2 changes: 1 addition & 1 deletion phpmyfaq/assets/templates/admin/login.twig
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
</a>
{% endif %}
{% if hasSignInWithMicrosoftActive %}
<a class="w-100 py-2 mb-2 btn btn-outline-warning rounded-3" href="../services/azure">
<a class="w-100 py-2 mb-2 btn btn-outline-warning rounded-3" href="../services/entra-id">
<i class="bi bi-windows" aria-hidden="true"></i>
{{ msgSignInWithMicrosoft }}
</a>
Expand Down
2 changes: 1 addition & 1 deletion phpmyfaq/assets/templates/default/login.twig
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
</a>
{% endif %}
{% if useSignInWithMicrosoft %}
<a class="w-100 py-2 mb-2 btn btn-outline-dark rounded-3" href="./services/azure">
<a class="w-100 py-2 mb-2 btn btn-outline-dark rounded-3" href="./services/entra-id">
<i class="bi bi-windows" aria-hidden="true"></i>
{{ 'msgSignInWithMicrosoft' | translate }}
</a>
Expand Down
2 changes: 1 addition & 1 deletion phpmyfaq/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@
}

if ($faqConfig->isSignInWithMicrosoftActive() && $user->getUserAuthSource() === 'azure') {
$redirect = new RedirectResponse($faqConfig->getDefaultUrl() . 'services/azure/logout.php');
$redirect = new RedirectResponse($faqConfig->getDefaultUrl() . 'services/entra-id/logout.php');
$redirect->send();
}

Expand Down
21 changes: 15 additions & 6 deletions phpmyfaq/services/azure/callback.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@
*/

use phpMyFAQ\Auth\AuthEntraId;
use phpMyFAQ\Auth\Azure\OAuth;
use phpMyFAQ\Auth\EntraId\OAuth;
use phpMyFAQ\Auth\EntraId\Session as EntraIdSession;
use phpMyFAQ\Configuration;
use phpMyFAQ\Enums\AuthenticationSourceType;
use phpMyFAQ\Filter;
use phpMyFAQ\User\CurrentUser;
use phpMyFAQ\User\UserSession;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage;

ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(-1);

if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
Expand All @@ -48,13 +54,16 @@
$code = Filter::filterInput(INPUT_GET, 'code', FILTER_SANITIZE_SPECIAL_CHARS);
$error = Filter::filterInput(INPUT_GET, 'error_description', FILTER_SANITIZE_SPECIAL_CHARS);

$session = new UserSession($faqConfig);
$oAuth = new OAuth($faqConfig, $session);
$session = new Session(new PhpBridgeSessionStorage());
$session->start();

$entraIdSession = new EntraIdSession($faqConfig, $session);
$oAuth = new OAuth($faqConfig, $entraIdSession);
$auth = new AuthEntraId($faqConfig, $oAuth);

$redirect = new RedirectResponse($faqConfig->getDefaultUrl());

if ($session->getCurrentSessionKey()) {
if ($entraIdSession->getCurrentSessionKey()) {
try {
$token = $oAuth->getOAuthToken($code);
$oAuth->setToken($token)->setAccessToken($token->access_token)->setRefreshToken($token->refresh_token);
Expand All @@ -81,7 +90,7 @@
$user->setTokenData([
'refresh_token' => $oAuth->getRefreshToken(),
'access_token' => $oAuth->getAccessToken(),
'code_verifier' => $session->get(UserSession::ENTRA_ID_OAUTH_VERIFIER),
'code_verifier' => $entraIdSession->get(EntraIdSession::ENTRA_ID_OAUTH_VERIFIER),
'jwt' => $oAuth->getToken()
]);
$user->setSuccess(true);
Expand Down
8 changes: 4 additions & 4 deletions phpmyfaq/services/azure/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
*/

use phpMyFAQ\Auth\AuthEntraId;
use phpMyFAQ\Auth\Azure\OAuth;
use phpMyFAQ\Auth\EntraId\OAuth;
use phpMyFAQ\Auth\EntraId\Session as EntraIdSession;
use phpMyFAQ\Configuration;
use phpMyFAQ\User\UserSession;

//
// Prepend and start the PHP session
Expand All @@ -34,8 +34,8 @@

$faqConfig = Configuration::getConfigurationInstance();

$session = new UserSession($faqConfig);
$oAuth = new OAuth($faqConfig, $session);
$enraIdSession = new EntraIdSession($faqConfig, $session);
$oAuth = new OAuth($faqConfig, $enraIdSession);
$auth = new AuthEntraId($faqConfig, $oAuth);

try {
Expand Down
8 changes: 4 additions & 4 deletions phpmyfaq/services/azure/logout.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
*/

use phpMyFAQ\Auth\AuthEntraId;
use phpMyFAQ\Auth\Azure\OAuth;
use phpMyFAQ\Auth\EntraId\OAuth;
use phpMyFAQ\Auth\EntraId\Session as EntraIdSession;
use phpMyFAQ\Configuration;
use phpMyFAQ\User\UserSession;

//
// Prepend and start the PHP session
Expand All @@ -34,8 +34,8 @@

$faqConfig = Configuration::getConfigurationInstance();

$session = new UserSession($faqConfig);
$oAuth = new OAuth($faqConfig, $session);
$enraIdSession = new EntraIdSession($faqConfig, $session);
$oAuth = new OAuth($faqConfig, $enraIdSession);
$auth = new AuthEntraId($faqConfig, $oAuth);

$auth->logout();
1 change: 1 addition & 0 deletions phpmyfaq/src/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
} else {
$ldap = null;
}

//
// Connect to Elasticsearch if enabled
//
Expand Down
21 changes: 10 additions & 11 deletions phpmyfaq/src/phpMyFAQ/Auth/AuthEntraId.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
namespace phpMyFAQ\Auth;

use phpMyFAQ\Auth;
use phpMyFAQ\Auth\Azure\OAuth;
use phpMyFAQ\Auth\EntraId\OAuth;
use phpMyFAQ\Auth\EntraId\Session;
use phpMyFAQ\Configuration;
use phpMyFAQ\Core\Exception;
use phpMyFAQ\Enums\AuthenticationSourceType;
use phpMyFAQ\User;
use phpMyFAQ\User\UserSession;
use SensitiveParameter;
use Symfony\Component\HttpFoundation\RedirectResponse;

Expand All @@ -34,8 +34,6 @@
*/
class AuthEntraId extends Auth implements AuthDriverInterface
{
private readonly UserSession $session;

private string $oAuthVerifier = '';

private string $oAuthChallenge;
Expand All @@ -49,10 +47,11 @@ class AuthEntraId extends Auth implements AuthDriverInterface
/**
* @inheritDoc
*/
public function __construct(Configuration $configuration, private readonly OAuth $oAuth)
{
public function __construct(
Configuration $configuration,
private readonly OAuth $oAuth
) {
$this->configuration = $configuration;
$this->session = new UserSession($configuration);

parent::__construct($configuration);
}
Expand Down Expand Up @@ -128,16 +127,16 @@ public function isValidLogin(string $login, ?array $optionalData = []): int
public function authorize(): void
{
$this->createOAuthChallenge();
$this->session->setCurrentSessionKey();
$this->session->set(UserSession::ENTRA_ID_OAUTH_VERIFIER, $this->oAuthVerifier);
$this->session->setCookie(UserSession::ENTRA_ID_OAUTH_VERIFIER, $this->oAuthVerifier, 7200, false);
$this->oAuth->getSession()->setCurrentSessionKey();
$this->oAuth->getSession()->set(Session::ENTRA_ID_OAUTH_VERIFIER, $this->oAuthVerifier);
$this->oAuth->getSession()->setCookie(Session::ENTRA_ID_OAUTH_VERIFIER, $this->oAuthVerifier, 7200, false);

$oAuthURL = sprintf(
'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize' .
'?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&code_challenge=%s&code_challenge_method=%s',
AAD_OAUTH_TENANTID,
AAD_OAUTH_CLIENTID,
urlencode($this->configuration->getDefaultUrl() . 'services/azure/callback.php'),
urlencode($this->configuration->getDefaultUrl() . 'services/entra-id/callback.php'),
AAD_OAUTH_SCOPE,
$this->oAuthChallenge,
self::ENTRAID_CHALLENGE_METHOD
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
* @since 2022-09-09
*/

namespace phpMyFAQ\Auth\Azure;
namespace phpMyFAQ\Auth\EntraId;

use phpMyFAQ\Configuration;
use phpMyFAQ\User\UserSession;
use stdClass;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
Expand All @@ -43,7 +42,7 @@ class OAuth
/**
* Constructor.
*/
public function __construct(private readonly Configuration $configuration, private readonly UserSession $session)
public function __construct(private readonly Configuration $configuration, private readonly Session $session)
{
$this->client = HttpClient::create();
}
Expand All @@ -66,17 +65,17 @@ public function getOAuthToken(string $code): stdClass
{
$url = 'https://login.microsoftonline.com/' . AAD_OAUTH_TENANTID . '/oauth2/v2.0/token';

if ($this->session->get(UserSession::ENTRA_ID_OAUTH_VERIFIER) !== '') {
$codeVerifier = $this->session->get(UserSession::ENTRA_ID_OAUTH_VERIFIER);
if ($this->session->get(Session::ENTRA_ID_OAUTH_VERIFIER) !== '') {
$codeVerifier = $this->session->get(Session::ENTRA_ID_OAUTH_VERIFIER);
} else {
$codeVerifier = $this->session->getCookie(UserSession::ENTRA_ID_OAUTH_VERIFIER);
$codeVerifier = $this->session->getCookie(Session::ENTRA_ID_OAUTH_VERIFIER);
}

$response = $this->client->request('POST', $url, [
'body' => [
'grant_type' => 'authorization_code',
'client_id' => AAD_OAUTH_CLIENTID,
'redirect_uri' => $this->configuration->getDefaultUrl() . 'services/azure/callback.php',
'redirect_uri' => $this->configuration->getDefaultUrl() . 'services/entra-id/callback.php',
'code' => $code,
'code_verifier' => $codeVerifier,
'client_secret' => AAD_OAUTH_SECRET
Expand Down Expand Up @@ -118,10 +117,15 @@ public function setToken(stdClass $token): OAuth
{
$idToken = base64_decode(explode('.', (string) $token->id_token)[1]);
$this->token = json_decode($idToken, null, 512, JSON_THROW_ON_ERROR);
$this->session->set(UserSession::ENTRA_ID_JWT, json_encode($this->token, JSON_THROW_ON_ERROR));
$this->session->set(Session::ENTRA_ID_JWT, json_encode($this->token, JSON_THROW_ON_ERROR));
return $this;
}

public function getSession(): Session
{
return $this->session;
}

public function getRefreshToken(): ?string
{
return $this->refreshToken;
Expand Down
120 changes: 120 additions & 0 deletions phpmyfaq/src/phpMyFAQ/Auth/EntraId/Session.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace phpMyFAQ\Auth\EntraId;

use Exception;
use phpMyFAQ\Configuration;
use phpMyFAQ\Session\AbstractSession;
use Random\RandomException;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session as SymfonySession;

class Session extends AbstractSession
{
/** @var string EntraID session key */
final public const ENTRA_ID_SESSION_KEY = 'pmf-entra-id-session-key';

/** @var string */
final public const ENTRA_ID_OAUTH_VERIFIER = 'pmf-entra-id-oauth-verifier';

/** @var string */
final public const ENTRA_ID_JWT = 'pmf-entra-id-jwt';

private ?string $currentSessionKey;

public function __construct(private readonly Configuration $configuration, private readonly SymfonySession $session)
{
parent::__construct($configuration, $session);

$this->createCurrentSessionKey();
}

/**
* Creates the current UUID session key
*/
public function createCurrentSessionKey(): void
{
$this->currentSessionKey = $this->uuid();
}

/**
* Returns the current UUID session key
*/
public function getCurrentSessionKey(): ?string
{
return $this->currentSessionKey ?? $this->session->get(self::ENTRA_ID_SESSION_KEY);
}

/**
* Sets the current UUID session key
*
* @throws Exception
*/
public function setCurrentSessionKey(): Session
{
if (!isset($this->currentSessionKey)) {
$this->createCurrentSessionKey();
}

$this->session->set(self::ENTRA_ID_SESSION_KEY, $this->currentSessionKey);

return $this;
}

/**
* Store the Session ID into a persistent cookie expiring
* 3600 seconds after the page request.
*
* @param string $name Cookie name
* @param int|string|null $sessionId Session ID
* @param int $timeout Cookie timeout
*/
public function setCookie(string $name, int|string|null $sessionId, int $timeout = 3600, bool $strict = true): void
{
$request = Request::createFromGlobals();

Cookie::create($name)
->withValue($sessionId ?? '')
->withExpires($request->server->get('REQUEST_TIME') + $timeout)
->withPath(dirname($request->server->get('SCRIPT_NAME')))
->withDomain(parse_url($this->configuration->getDefaultUrl(), PHP_URL_HOST))
->withSameSite($strict ? 'strict' : '')
->withSecure($request->isSecure())
->withHttpOnly();
}

/**
* Returns the value of a cookie.
*
* @param string $name Cookie name
*/
public function getCookie(string $name): string
{
$request = Request::createFromGlobals();
return $request->cookies->get($name, '');
}

/**
* Returns a UUID Version 4 compatible universally unique identifier.
*/
public function uuid(): string
{
try {
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0x0fff) | 0x4000,
random_int(0, 0x3fff) | 0x8000,
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0xffff)
);
} catch (RandomException $e) {
$this->configuration->getLogger()->error('Cannot generate UUID: ' . $e->getMessage());
return '';
}
}
}
Loading

0 comments on commit 73f9ab7

Please sign in to comment.