Skip to content

Commit

Permalink
Implement OIDC
Browse files Browse the repository at this point in the history
  • Loading branch information
SamuelWei authored and pizkaz committed Jul 25, 2024
1 parent 5cc4c5d commit 12bbe99
Show file tree
Hide file tree
Showing 24 changed files with 562 additions and 12 deletions.
11 changes: 11 additions & 0 deletions app/Auth/OIDC/InvalidConfiguration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace App\Auth\OIDC;

class InvalidConfiguration extends \Exception
{
public function __construct()
{
parent::__construct('OIDC configuration could not be retrieved: invalid response from discovery endpoint');
}
}
13 changes: 13 additions & 0 deletions app/Auth/OIDC/NetworkIssue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Auth\OIDC;

use Exception;

class NetworkIssue extends \Exception
{
public function __construct(Exception $e)
{
parent::__construct('OIDC configuration could not be retrieved:'.$e->getMessage());
}
}
107 changes: 107 additions & 0 deletions app/Auth/OIDC/OIDCController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

namespace App\Auth\OIDC;

use App\Auth\MissingAttributeException;
use App\Http\Controllers\Controller;
use App\Models\SessionData;
use Auth;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\InvalidStateException;

class OIDCController extends Controller
{
public function __construct()
{
$this->middleware('guest');
}

public function redirect(Request $request)
{
if ($request->get('redirect')) {
$request->session()->put('redirect_url', $request->input('redirect'));
}

try {
return Socialite::driver('oidc')->redirect();
} catch (NetworkIssue $e) {
report($e);

return redirect('/external_login?error=network_issue');
} catch (InvalidConfiguration $e) {
report($e);

return redirect('/external_login?error=invalid_configuration');
}
}

public function logout(Request $request)
{
if (isset($_REQUEST['logout_token'])) {
$logout_token = $_REQUEST['logout_token'];

$claims = Socialite::driver('oidc')->getLogoutTokenClaims($logout_token);

$lookupSessions = SessionData::where('key', 'oidc_sub')->where('value', $claims->sub)->get();
foreach ($lookupSessions as $lookupSession) {
$lookupSession->session()->delete();
}
}
}

public function callback(Request $request)
{
try {
$oidc_raw_user = Socialite::driver('oidc')->user();
} catch (NetworkIssue $e) {
report($e);

return redirect('/external_login?error=network_issue');
} catch (InvalidConfiguration $e) {
report($e);

return redirect('/external_login?error=invalid_configuration');
} catch (ClientException $e) {
report($e);

return redirect('/external_login?error=invalid_configuration');
} catch (InvalidStateException $e) {
report($e);

return redirect('/external_login?error=invalid_state');
}

// Create new open-id connect user
try {
$oidc_user = new OIDCUser($oidc_raw_user);
} catch (MissingAttributeException $e) {
return redirect('/external_login?error=missing_attributes');
}

// Get eloquent user (existing or new)
$user = $oidc_user->createOrFindEloquentModel('oidc');

// Sync attributes
try {
$oidc_user->syncWithEloquentModel($user, config('services.oidc.mapping')->roles);
} catch (MissingAttributeException $e) {
return redirect('/external_login?error=missing_attributes');
}

Auth::login($user);

session(['session_data' => [
['key' => 'oidc_sub', 'value' => $oidc_user->getRawAttributes()['sub']],
]]);

session()->put('oidc_id_token', $oidc_raw_user->accessTokenResponseBody['id_token']);

\Log::info('External user {user} has been successfully authenticated.', ['user' => $user->getLogLabel(), 'type' => 'oidc']);

$url = '/external_login';

return redirect($request->session()->has('redirect_url') ? ($url.'?redirect='.urlencode($request->session()->get('redirect_url'))) : $url);
}
}
13 changes: 13 additions & 0 deletions app/Auth/OIDC/OIDCExtendSocialite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Auth\OIDC;

use SocialiteProviders\Manager\SocialiteWasCalled;

class OIDCExtendSocialite
{
public function handle(SocialiteWasCalled $socialiteWasCalled)
{
$socialiteWasCalled->extendSocialite('oidc', OIDCProvider::class);
}
}
216 changes: 216 additions & 0 deletions app/Auth/OIDC/OIDCProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<?php

namespace App\Auth\OIDC;

use Cache;
use Exception;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use GuzzleHttp\RequestOptions;
use Http;
use Illuminate\Http\Request;
use SocialiteProviders\Manager\OAuth2\AbstractProvider;
use SocialiteProviders\Manager\OAuth2\User;

class OIDCProvider extends AbstractProvider
{
public const IDENTIFIER = 'OIDC';

protected $scopeSeparator = ' ';

protected $scopes = ['openid'];

public function __construct(Request $request, $clientId, $clientSecret, $redirectUrl, $guzzle = [])
{
parent::__construct($request, $clientId, $clientSecret, $redirectUrl, $guzzle);
}

/**
* {@inheritdoc}
*/
public static function additionalConfigKeys()
{
return ['issuer', 'ttl', 'scopes'];
}

protected function getOIDCConfig($key)
{
$url = rtrim($this->getConfig('issuer'), '/').'/.well-known/openid-configuration';

$cacheKey = 'oidc.config.'.md5($url);
$config = Cache::get($cacheKey);

if (! $config) {
try {
$response = Http::get($url);
} catch (Exception $e) {
throw new NetworkIssue($e);
}

if ($response->successful()) {
$config = $response->json();
Cache::put($cacheKey, $config, $seconds = $this->getConfig('ttl'));
} else {
throw new InvalidConfiguration();
}
}

$this->redirectUrl = url($this->getConfig('redirect'));

return $config[$key] ?? null;
}

/**
* Get public keys
*
* @return array
*/
private function getJWTKeys()
{
$response = Http::get($this->getOIDCConfig('jwks_uri'));

return $response->json();
}

public function getLogoutTokenClaims($logoutToken)
{
try {
$claims = JWT::decode($logoutToken, JWK::parseKeySet($this->getJWTKeys(), 'RS256'));

$payload = explode('.', $logoutToken);
$headerRaw = JWT::urlsafeB64Decode($payload[0]);
$header = JWT::jsonDecode($headerRaw);

// Get the supported algorithms, fallback to RS256
$supportedAlgs = $this->getOIDCConfig('id_token_signing_alg_values_supported') ?? ['RS256'];
// Get the alg from the header
$alg = $header->alg;

// Validate the alg (algorithm) Header Parameter
// https://openid.net/specs/openid-connect-backchannel-1_0.html
if (! in_array($alg, $supportedAlgs)) {
throw new Exception('Unsupported alg '.$alg.' header, supported algorithms are '.implode(', ', $supportedAlgs));
}
if ($alg === 'none') {
throw new Exception('Unsupported alg none');
}

if ($this->verifyLogoutTokenClaims($claims)) {
return $claims;
}
} catch (Exception $ex) {
return false;
}

return false;
}

private function verifyLogoutTokenClaims($claims)
{
// Verify that the Logout Token doesn't contain a nonce Claim.
if (isset($claims->nonce)) {
return false;
}

// Verify that the logout token contains the sub
if (! isset($claims->sub)) {
return false;
}

// Verify that the Logout Token contains an events Claim whose
// value is a JSON object containing the member name
// https://openid.net/specs/openid-connect-backchannel-1_0.html
if (isset($claims->events)) {
$events = (array) $claims->events;
if (! isset($events['http://schemas.openid.net/event/backchannel-logout']) ||
! is_object($events['http://schemas.openid.net/event/backchannel-logout'])) {
return false;
}
} else {
return false;
}

// Validate the iss
if (strcmp($claims->iss, $this->getOIDCConfig('issuer'))) {
return false;
}

// Validate the aud
$auds = $claims->aud;
$auds = is_array($auds) ? $auds : [$auds];
if (! in_array($this->config['client_id'], $auds, true)) {
return false;
}

return true;
}

/**
* {@inheritdoc}
*/
protected function getAuthUrl($state)
{
$this->setScopes(array_merge($this->scopes, $this->getConfig('scopes')));

return $this->buildAuthUrlFromBase($this->getOIDCConfig('authorization_endpoint'), $state);
}

/**
* {@inheritdoc}
*/
protected function getTokenUrl()
{
return $this->getOIDCConfig('token_endpoint');
}

/**
* {@inheritdoc}
*/
protected function getUserByToken($token)
{
$response = $this->getHttpClient()->get($this->getOIDCConfig('userinfo_endpoint'), [
RequestOptions::HEADERS => [
'Authorization' => 'Bearer '.$token,
],
]);

return json_decode((string) $response->getBody(), true);
}

/**
* {@inheritdoc}
*/
protected function mapUserToObject(array $user)
{
return (new User())->setRaw($user);
}

/**
* {@inheritdoc}
*/
protected function getTokenFields($code)
{
return array_merge(parent::getTokenFields($code), [
'grant_type' => 'authorization_code',
]);
}

public function logout($idToken, $redirect)
{
$signout_endpoint = $this->getOIDCConfig('end_session_endpoint');

if (! $signout_endpoint) {
return false;
}

$signout_params = [
'client_id' => $this->config['client_id'],
'id_token_hint' => $idToken,
'post_logout_redirect_uri' => $redirect,
];

$signout_endpoint .= (strpos($signout_endpoint, '?') === false ? '?' : '&').http_build_query($signout_params, '', '&');

return $signout_endpoint;
}
}
Loading

0 comments on commit 12bbe99

Please sign in to comment.