-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
562 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.