Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions AuthorizationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ public static function get_and_process_access_token( $authentication_request_bod
}

/**
* Decodes and validates an id_token value returned
* Decodes an id_token value returned
*
* @param array $authentication_request_body The body to use in the Authentication Request.
* @param string $id_token The JWT token
* @param \AADSSO_Settings $settings The settings to use.
*
* @return mixed The decoded authorization result.
*/
public static function validate_id_token( $id_token, $settings, $antiforgery_id ) {
private static function decode_token( $id_token, $settings) {

$jwt = null;
$last_exception = null;
Expand Down Expand Up @@ -132,13 +132,88 @@ public static function validate_id_token( $id_token, $settings, $antiforgery_id
. "-----END CERTIFICATE-----\n";

// This throws an exception if the id_token cannot be validated.

$jwt = \AADSSO\Firebase\JWT\JWT::decode( $id_token, $key_pem, self::$allowed_algorithms );
break;
} catch ( Exception $e ) {
$last_exception = $e;
}
}

if ( null == $jwt ) {
throw $last_exception;
}

return $jwt;
}

/**
* Validates claims of bearer_token
*
* @param string $jwt The JWT token
* @param \AADSSO_Settings $settings The settings to use.
*
*/
private static function validate_token_claims( $jwt, $settings){
$audience = $jwt->aud;
if ($audience != $settings->resource_id) {
throw new \AADSSO\Firebase\JWT\SignatureInvalidException('Invalid Audience');
}

// The Issuer Identifier for the OpenID Provider MUST exactly match the value of the iss (issuer) Claim.
$iss_token = $jwt->iss;
$iss_metadata = $settings->issuer;

if ($iss_token != $iss_metadata) {
throw new \AADSSO\Firebase\JWT\SignatureInvalidException('Signature invalid');
}

}

/**
* Validates signature and claims of a bearer_token value returned
*
* @param string $bearer_token The bearer token
* @param \AADSSO_Settings $settings The settings to use.
*
* @return mixed The decoded authorization result.
*/
public static function validate_bearer_token( $bearer_token, $settings) {

$jwt = null;
$last_exception = null;

try{
$jwt = self::decode_token( $bearer_token, $settings);
self::validate_token_claims( $jwt, $settings);
}
catch ( Exception $e ) {
throw $e;
}

return $jwt;
}

/**
* Decodes and validates an id_token value returned
*
* @param string $id_token The token
* @param \AADSSO_Settings $settings The settings to use.
*
* @return mixed The decoded authorization result.
*/
public static function validate_id_token( $id_token, $settings, $antiforgery_id) {

$jwt = null;
$last_exception = null;

try{
$jwt = self::decode_token( $id_token, $settings);
}
catch ( Exception $e ) {
$last_exception = $e;
}

if ( null == $jwt ) {
throw $last_exception;
}
Expand Down
16 changes: 11 additions & 5 deletions Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class AADSSO_Settings {
*/
public $client_id = '';

/**
* @var string The resource ID obtained after registering an application in AAD.
*/
public $resource_id = '';

/**
* @var string The client secret key, which is generated on the app configuration page in AAD.
*/
Expand All @@ -38,6 +43,11 @@ class AADSSO_Settings {
*/
public $org_display_name = '';

/**
* @var string The issuer obtained from metadata endpoints.
*/
public $issuer = '';

/**
* The value of the domain_hint is a registered domain for the tenant. If the tenant is federated
* to an on-premises directory, AAD redirects to the specified tenant federation server.
Expand Down Expand Up @@ -192,12 +202,9 @@ public static function get_instance() {
* @return \AADSSO_Settings The (only) configured instance of this class.
*/
public static function init() {

$instance = self::get_instance();

// First, set the settings stored in the WordPress database.
$instance->set_settings( get_option( 'aadsso_settings' ) );

/*
* Then, add the settings stored in the OpenID Connect configuration endpoint.
* We're using transient as a cache, to prevent from making a request on every WP page load.
Expand All @@ -213,7 +220,6 @@ public static function init() {
set_transient( 'aadsso_openid_configuration', $openid_configuration, 3600 );
}
$instance->set_settings( $openid_configuration );

return $instance;
}

Expand Down Expand Up @@ -273,4 +279,4 @@ function set_settings( $settings ) {
}
return $this;
}
}
}
20 changes: 20 additions & 0 deletions SettingsPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ public function register_settings() {
'aadsso_settings_general' // section
);

add_settings_field(
'resource_id', // id
__( 'Resource ID', 'aad-sso-wordpress' ), // title
array( $this, 'resource_id_callback' ), // callback
'aadsso_settings_page', // page
'aadsso_settings_general' // section
);

add_settings_field(
'client_secret', // id
__( 'Client secret', 'aad-sso-wordpress' ), // title
Expand Down Expand Up @@ -342,6 +350,7 @@ public function sanitize_settings( $input ) {
'org_display_name',
'org_domain_hint',
'client_id',
'resource_id',
'client_secret',
'redirect_uri',
'logout_redirect_uri',
Expand Down Expand Up @@ -486,6 +495,17 @@ public function client_id_callback() {
);
}

/**
* Renders the `resource_id` form control
*/
public function resource_id_callback() {
$this->render_text_field( 'resource_id' );
printf(
'<p class="description">%s</p>',
__( 'The resource ID of the Azure AD application representing this blog.', 'aad-sso-wordpress' )
);
}

/**
* Renders the `client_secret` form control
**/
Expand Down
110 changes: 107 additions & 3 deletions aad-sso-wordpress.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
require_once AADSSO_PLUGIN_DIR . '/lib/php-jwt/src/SignatureInvalidException.php';

class AADSSO {

private $jwt_error = null;

static $instance = FALSE;

Expand Down Expand Up @@ -88,6 +90,108 @@ public function __construct( $settings ) {

// Register the textdomain for localization after all plugins are loaded
add_action( 'plugins_loaded', array( $this, 'load_textdomain' ) );





add_filter( 'determine_current_user', array( $this, 'determine_current_user' ), 20 );

add_filter( 'wp_rest_server_class', array($this, 'wp_rest_server_class' ) );

add_filter( 'rest_pre_dispatch', array($this, 'rest_pre_dispatch' ), 10, 2 );
}


/**
* Get the bearer token from the request header
*
* @return string|bool
*/
private function get_bearer(){
$headers = getallheaders();
if(isset($headers['Authorization'])){
if(substr( $headers['Authorization'], 0, 7 ) === 'Bearer '){
return substr( $headers['Authorization'], 7);
}
}
return false;
}

/**
* Filter to hook the rest_pre_dispatch, if the is an error in the request
* send it, if there is no error just continue with the current request.
*
* @param $request
*/
public function rest_pre_dispatch($request)
{
if (is_wp_error($this->jwt_error)) {
return $this->jwt_error;
}
return $request;
}

/**
* Loosely Based on https://github.com/WP-API/Basic-Auth/blob/master/basic-auth.php
*
* @param $input_user
*
* @return WP_User|int
*/
public function determine_current_user( $input_user ){
// Don't authenticate twice
if ( ! empty( $input_user ) ) {
return $input_user;
}

if(($bearer = $this->get_bearer()) !== false){
try{
$jwt = AADSSO_AuthorizationHelper::validate_bearer_token($bearer, $this->settings);
$user = $this->get_wp_user_from_aad_user( $jwt );
if ( is_a( $user, 'WP_User' ) ) {
return $user->ID;
}
}
catch ( Exception $e ) {
$this->jwt_error = new WP_Error(
'aad-sso-wordpress-validation',
$e->getMessage(),
array(
'status' => 401,
)
);
}

}

// If it wasn't a user what got returned, just pass on what we had received originally.
return $input_user;
}

/**
* Prevent caching of unauthenticated status. See comment below.
*
* We don't actually care about the `wp_rest_server_class` filter, it just
* happens right after the constant we do care about is defined.
*
*/
public static function wp_rest_server_class( $class ) {
global $current_user;
if ( defined( 'REST_REQUEST' )
&& REST_REQUEST
&& $current_user instanceof WP_User
&& 0 === $current_user->ID ) {
/*
* For our authentication to work, we need to remove the cached lack
* of a current user, so the next time it checks, we can detect that
* this is a rest api request and allow our override to happen. This
* is because the constant is defined later than the first get current
* user call may run.
*/
$current_user = null;
}
return $class;
}

/**
Expand Down Expand Up @@ -234,7 +338,7 @@ function authenticate( $user, $username, $password ) {
/* If 'code' is present, this is the Authorization Response from Azure AD, and 'code' has
* the Authorization Code, which will be exchanged for an ID Token and an Access Token.
*/
if ( isset( $_GET['code'] ) ) {
if ( isset( $_GET['code'] ) ) {

$antiforgery_id = $_SESSION['aadsso_antiforgery-id'];
$state_is_missing = ! isset( $_GET['state'] );
Expand Down Expand Up @@ -278,7 +382,7 @@ function authenticate( $user, $username, $password ) {
// exists in WordPress (either because it already existed, or we created it
// on-the-fly). All that's left is to set the roles based on group membership.
if ( true === $this->settings->enable_aad_group_to_wp_role ) {
$user = $this->update_wp_user_roles( $user, $jwt->upn, $jwt->tid );
$user = $this->update_wp_user_roles( $user, $jwt->oid, $jwt->tid );
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, good catch! (This was only working before because Azure AD Graph API lets you use the UPN as the object ID for most requests, but this wouldn't hold up for external users.)

}
}

Expand Down Expand Up @@ -364,7 +468,7 @@ function get_wp_user_from_aad_user( $jwt ) {
'user_not_registered',
sprintf(
__( 'ERROR: The authenticated user %s is not a registered user in this blog.', 'aad-sso-wordpress' ),
$jwt->upn
$unique_name
)
);
}
Expand Down