Skip to content

Instantly share code, notes, and snippets.

@rcosgrave
Last active November 1, 2022 12:00
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rcosgrave/ec92938181096fd8847a38c9cc6a37d0 to your computer and use it in GitHub Desktop.
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)
<?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, '-_', '+/');
}
}
@patrickjaja
Copy link

Thx!

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