Skip to content

Instantly share code, notes, and snippets.

@Glideh
Created September 12, 2022 06:28
Show Gist options
  • Save Glideh/acd2ee681799e55c574661ad14a00c54 to your computer and use it in GitHub Desktop.
Save Glideh/acd2ee681799e55c574661ad14a00c54 to your computer and use it in GitHub Desktop.
Azure authentication guard for Symfony based on thenetworg/oauth2-azure
<?php
namespace App\Security;
use Firebase\JWT\JWT;
use Firebase\JWT\SignatureInvalidException;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Contracts\Cache\ItemInterface;
use TheNetworg\OAuth2\Client\Provider\Azure;
class AzureAuthenticator extends AbstractGuardAuthenticator
{
// Update with your own Application ID URI
private $audience = 'api://xxxxx-xxxx-xxxxxxx-xxxxx-xxxxxxx';
private $tokenHeader = 'Authorization';
/** @var Azure $provider */
private $provider;
/** @var AbstractAdapter $cache */
private $cache;
private $refreshDelayStandard = 3600 * 24; // One day
private $refreshDelayShort = 60 * 5; // 5 minutes
public function __construct()
{
$this->provider = new Azure();
$this->cache = new ApcuAdapter('unique-string-for-app');
}
// One day cached keys
private function getKeys($force = false)
{
return $this->cache->get('microsoft-keys', function (ItemInterface $item) {
$item->expiresAfter($this->refreshDelayStandard);
return $this->provider->getJwtVerificationKeys();
}, $force ? INF : 1.0);
}
// 5 minutes cached keys
private function getKeysShort()
{
return $this->cache->get('microsoft-keys-short', function (ItemInterface $item) {
$item->expiresAfter($this->refreshDelayShort);
// Forces the one day cache refresh
return $this->getKeys(true);
});
}
public function getCredentials(Request $request)
{
$accessToken = explode(' ', $request->headers->get($this->tokenHeader))[1];
// 2 attempts
foreach ([0, 1] as $try) {
$firstTry = $try === 0;
try {
// First tries to use the "one day" cached keys, then falls back on the "5 minutes" cache
// (if signature check failed) which forces the "one day" cache refresh
$keys = $firstTry ? $this->getKeys() : $this->getKeysShort();
return (array)JWT::decode($accessToken, $keys);
} catch (SignatureInvalidException $e) {
if ($firstTry) continue;
throw new HttpException(403, $e->getMessage(), $e);
} catch (\UnexpectedValueException | \InvalidArgumentException | \DomainException $e) {
throw new HttpException(403, $e->getMessage(), $e);
}
}
}
public function start(Request $request, AuthenticationException $authException = null)
{
return new Response('Auth header required', 401);
}
public function supports(Request $request)
{
return $request->headers->has($this->tokenHeader);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// If we are using upn as the username
return $userProvider->loadUserByUsername($credentials['upn']);
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($credentials['aud'] !== $this->audience) throw new AuthenticationException('Wrong audience');
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
}
public function supportsRememberMe()
{
return false;
}
}
security:
providers:
# ...
login:
entity:
class: App\Entity\User
property: login
firewalls:
# ...
api:
pattern: ^/
provider: login
stateless: true
guard:
authenticators:
- App\Security\AzureAuthenticator
# ...
@Glideh
Copy link
Author

Glideh commented Sep 12, 2022

This is for Symfony >2 <6 and can be easily adapted for Symfony 6
Requires thenetworg/oauth2-azure

@T3chW1zard
Copy link

Do you maybe have an updated version of this for Symfony 6/7?

@Glideh
Copy link
Author

Glideh commented Oct 10, 2023

Sorry I don't have that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment