Last active
November 1, 2022 12:00
-
-
Save rcosgrave/ec92938181096fd8847a38c9cc6a37d0 to your computer and use it in GitHub Desktop.
A quick and dirty implementation of Azure's Active Directory B2C OAuth2 Service using Authorization Code Grant (external libraries required)
This file contains hidden or 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
<?php | |
/** This is a simple class to allow for fast implementation of Azure's Active Direct B2C OAuth Service via Authorization Code scope | |
** It requires the use of the following repos | |
** https://github.com/firebase/php-jwt | |
** https://github.com/phpseclib/phpseclib/tree/master/phpseclib (Please note to get this to work I had to move the Math directory inside the Crypt directory) | |
** Sample Configuration https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=b2c_1_sign_in | |
** Sample Key Location https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_sign_in | |
*/ | |
use \Firebase\JWT\JWT; | |
class AzureOAuth { | |
public $client_id; | |
public $state; | |
public $logout_url; | |
private $client_secret; | |
private $tenant; | |
private $redirect_uri; | |
private $policy; | |
private $policy_qs = ""; | |
private $key_url; | |
private $authorize_url; | |
private $token_url; | |
private $RSA; | |
private $JWT; | |
/** | |
* AzureOAuth constructor. | |
* @param string $client_id The client_id (Application ID) of the Application | |
* @param string $client_secret The client_secret (key) of the Application | |
* @param string $tenant $tenant.onmicrosoft.com | |
* @param string $redirect_uri Authorized Redirect URI of the Application | |
* @param string $policy Which Sign In/Sign Up policy to implement | |
*/ | |
function __construct($client_id="", $client_secret="", $tenant="", $redirect_uri="", $policy="") { | |
// Be sure to supply your own path to these libraries | |
require_once("azure_rsa/Crypt/RSA.php"); | |
require_once("php-jwt-master/src/JWT.php"); | |
$this->client_id = $client_id; | |
$this->client_secret = $client_secret; | |
$this->tenant = $tenant; | |
$this->redirect_uri = $redirect_uri; | |
$this->policy = $policy; | |
$this->RSA = new Crypt_RSA(); | |
$this->JWT = new JWT(); | |
$this->policy = $policy; | |
if (!empty($this->policy)) { | |
$this->policy_qs = "?p=" . $this->policy; | |
} | |
$this->key_url = "https://login.microsoftonline.com/" . $this->tenant . ".onmicrosoft.com/discovery/v2.0/keys" . $this->policy_qs; | |
$this->authorize_url = "https://login.microsoftonline.com/" . $this->tenant . ".onmicrosoft.com/oauth2/v2.0/authorize"; | |
$this->token_url = "https://login.microsoftonline.com/" . $this->tenant . ".onmicrosoft.com/oauth2/v2.0/token" . $this->policy_qs; | |
$this->logout_url = "https://login.microsoftonline.com/" . $this->tenant . ".onmicrosoft.com/oauth2/v2.0/logout"; | |
} | |
/** | |
* @param string $scope | |
* @param string $response_type | |
* @param string $state can be used to prevent cross site forgery OR store the URL the client wanted to goto | |
* $param string $response_mode how we want to receive the data (form_post, query, or fragment) | |
* @return string returns the fully qualified URL to begin 3-legged OAuth | |
*/ | |
public function get_authorize_url($scope = "openid", $response_type = "code", $state="", $response_mode="query") { | |
if (strlen($state) > 0) { | |
$this->state = $state; | |
} | |
else { | |
$this->state = uniqid(); | |
} | |
$authorize_params = array( | |
"scope" => $scope, // `openid` for id_token and profile `$this->client_id offline_access` to additionally get a refresh_token | |
"response_type" => $response_type, | |
"client_id" => $this->client_id, | |
"state" => base64_encode($this->state), | |
"redirect_uri" => $this->redirect_uri, | |
"response_mode" => $response_mode | |
); | |
if ($this->policy) { | |
$authorize_params['p'] = $this->policy; | |
} | |
$authorize_querystring = http_build_query($authorize_params); | |
$authorize_url = $this->authorize_url . "?" . $authorize_querystring; | |
return $authorize_url; | |
} | |
/** | |
* @param string $authorization_code | |
* @return array|bool returns Token payload (including id_token and refresh_token (if correct scope) | |
* SAMPLE RESPONSE: | |
* object(stdClass)#15 (5) { | |
* ["id_token"]=> string() "THIS.IS.FAKE" | |
* ["token_type"]=> string(6) "Bearer" | |
* ["not_before"]=> int(1493691845) | |
* ["id_token_expires_in"]=> int(3600) | |
* ["profile_info"]=> string(171) "ALSOFAKE" | |
* } | |
*/ | |
public function get_token($authorization_code="") { | |
$token_params = array( | |
"grant_type" => "authorization_code", | |
"client_id" => $this->client_id, | |
"client_secret" => $this->client_secret, | |
"code" => $authorization_code | |
); | |
$response = $this->send_curl($this->token_url, $token_params); | |
if ($response['success']) { | |
return json_decode($response['payload']); | |
} | |
return false; | |
} | |
/** | |
* @param $id_token | |
* @return array | |
*/ | |
public function validate_id_token($id_token) { | |
$used_key = $this->get_used_key($id_token); | |
$modulus = $this->convert_base64url_to_base64($used_key->n); // Alter to correct format | |
$exponent = $this->convert_base64url_to_base64($used_key->e); // Alter to correct format | |
$this->RSA->setPublicKey('<RSAKeyValue> | |
<Modulus>' . $modulus . '</Modulus> | |
<Exponent>' . $exponent . '</Exponent> | |
</RSAKeyValue>'); | |
$public_key = $this->RSA->getPublicKey(); | |
try { | |
$decoded = $this->JWT->decode($id_token, $public_key, array('RS256')); | |
} | |
catch (Exception $e) { | |
return array("success" => false, "error" => "Unable to valid id_token with message: " .$e->getMessage()); | |
} | |
return array("success" => true, "payload" => $decoded); | |
} | |
/** | |
* @param $redirect_to string | |
* @return string Fully qualified Logout URL with redirect URL | |
*/ | |
public function get_logout_url($redirect_to=false) { | |
if (empty($redirect_to)) { | |
return false; | |
} | |
$logout_params = array( | |
"post_logout_redirect_uri" => $redirect_to | |
); | |
if ($this->policy) { | |
$logout_params["p"] = $this->policy; | |
} | |
$logout_params_qs = "?" . http_build_query($logout_params); | |
return $this->logout_url . $logout_params_qs; | |
} | |
/** | |
* Using the kid of the $id_token to match against available keys | |
* @param $id_token | |
* @return mixed | |
*/ | |
private function get_used_key($id_token) { | |
$token_parts = $this->get_id_token_parts($id_token); | |
$header = json_decode(base64_decode($token_parts['header'])); | |
$available_keys = $this->get_available_keys(); | |
foreach ($available_keys as $available_key) { | |
if ($available_key->kid == $header->kid) { | |
return $available_key; | |
} | |
} | |
return false; | |
} | |
/** | |
* @param $id_token | |
* @return array | |
*/ | |
private function get_id_token_parts($id_token) { | |
$token_parts = explode(".", $id_token); | |
$return['header'] = $token_parts[0]; | |
$return['payload'] = $token_parts[1]; | |
$return['signature'] = $token_parts[2]; | |
return $return; | |
} | |
/** | |
* @return object | |
*/ | |
private function get_available_keys() { | |
$azure_keys = json_decode(file_get_contents($this->key_url)); | |
return $azure_keys->keys; | |
} | |
/** | |
* @param string $url | |
* @param array $params | |
* @param string $method | |
* @return array | |
*/ | |
private function send_curl($url="", $params=array(), $method="POST") { | |
$curl = curl_init(); | |
curl_setopt_array($curl, | |
array( | |
CURLOPT_URL => $url, | |
CURLOPT_RETURNTRANSFER => true, | |
CURLOPT_ENCODING => "", | |
CURLOPT_MAXREDIRS => 10, | |
CURLOPT_TIMEOUT => 30, | |
//CURLOPT_SSL_VERIFYPEER=> 0, | |
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, | |
CURLOPT_CUSTOMREQUEST => $method, | |
CURLOPT_POSTFIELDS =>http_build_query($params), | |
CURLOPT_HTTPHEADER => array( | |
"cache-control: no-cache", | |
"content-type: application/x-www-form-urlencoded", | |
), | |
) | |
); | |
$response = curl_exec($curl); | |
$err = curl_error($curl); | |
curl_close($curl); | |
if ($err) { | |
$return = array("success" => false, "payload" => $err); | |
} else { | |
$return = array("success" => true, "payload" => $response); | |
} | |
return $return; | |
} | |
/** | |
* @param string $input | |
* @return string | |
*/ | |
private function convert_base64url_to_base64($input="") { | |
$padding = strlen($input) % 4; | |
if ($padding > 0) { | |
$input .= str_repeat("=", 4 - $padding); | |
} | |
return strtr($input, '-_', '+/'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thx!