Skip to content

Commit

Permalink
Possibility to register wordpress authenticate filter.
Browse files Browse the repository at this point in the history
  • Loading branch information
gassan committed Sep 24, 2020
1 parent 7560701 commit 0f8e767
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 180 deletions.
234 changes: 147 additions & 87 deletions includes/openid-connect-generic-client-wrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ static public function register( OpenID_Connect_Generic_Client $client, OpenID_C
add_action( 'parse_request', array( $client_wrapper, 'alternate_redirect_uri_parse_request' ) );
}

if ( $settings->register_authenticate_filter ) {
add_filter( 'authenticate', array( $client_wrapper, 'authenticate_filter' ), (int) $settings->authenticate_filter_priority, 3 );
}

// Verify token for any logged in user.
if ( is_user_logged_in() ) {
add_action( 'wp_loaded', array( $client_wrapper, 'ensure_tokens_still_fresh' ) );
Expand Down Expand Up @@ -361,96 +365,12 @@ function authentication_request_callback() {
// Get the decoded response from the authentication request result.
$token_response = $client->get_token_response( $token_result );

// Allow for other plugins to alter data before validation.
$token_response = apply_filters( 'openid-connect-modify-token-response-before-validation', $token_response );

if ( is_wp_error( $token_response ) ) {
$this->error_redirect( $token_response );
}

// Ensure the that response contains required information.
$valid = $client->validate_token_response( $token_response );

if ( is_wp_error( $valid ) ) {
$this->error_redirect( $valid );
}

/**
* The id_token is used to identify the authenticated user, e.g. for SSO.
* The access_token must be used to prove access rights to protected
* resources e.g. for the userinfo endpoint
*/
$id_token_claim = $client->get_id_token_claim( $token_response );

// Allow for other plugins to alter data before validation.
$id_token_claim = apply_filters( 'openid-connect-modify-id-token-claim-before-validation', $id_token_claim );

if ( is_wp_error( $id_token_claim ) ) {
$this->error_redirect( $id_token_claim );
}

// Validate our id_token has required values.
$valid = $client->validate_id_token_claim( $id_token_claim );

if ( is_wp_error( $valid ) ) {
$this->error_redirect( $valid );
}

// If userinfo endpoint is set, exchange the token_response for a user_claim.
if ( ! empty( $this->settings->endpoint_userinfo ) && isset( $token_response['access_token'] ) ) {
$user_claim = $client->get_user_claim( $token_response );
} else {
$user_claim = $id_token_claim;
}

if ( is_wp_error( $user_claim ) ) {
$this->error_redirect( $user_claim );
}

// Validate our user_claim has required values.
$valid = $client->validate_user_claim( $user_claim, $id_token_claim );
$user = $this->validate( $token_response );

if ( is_wp_error( $valid ) ) {
$this->error_redirect( $valid );
if ( is_wp_error( $user ) ) {
$this->error_redirect( $user );
}

/**
* End authorization
* -
* Request is authenticated and authorized - start user handling
*/
$subject_identity = $client->get_subject_identity( $id_token_claim );
$user = $this->get_user_by_identity( $subject_identity );

if ( ! $user ) {
if ( $this->settings->create_if_does_not_exist ) {
$user = $this->create_new_user( $subject_identity, $user_claim );
if ( is_wp_error( $user ) ) {
$this->error_redirect( $user );
}
} else {
$this->error_redirect( new WP_Error( 'identity-not-map-existing-user', __( 'User identity is not linked to an existing WordPress user.', 'daggerhart-openid-connect-generic' ), $user_claim ) );
}
} else {
// Allow plugins / themes to take action using current claims on existing user (e.g. update role).
do_action( 'openid-connect-generic-update-user-using-current-claim', $user, $user_claim );
}

// Validate the found / created user.
$valid = $this->validate_user( $user );

if ( is_wp_error( $valid ) ) {
$this->error_redirect( $valid );
}

// Login the found / created user.
$this->login_user( $user, $token_response, $id_token_claim, $user_claim, $subject_identity );

do_action( 'openid-connect-generic-user-logged-in', $user );

// Log our success.
$this->logger->log( "Successful login for: {$user->user_login} ({$user->ID})", 'login-success' );

// Redirect back to the origin page if enabled.
$redirect_url = isset( $_COOKIE[ $this->cookie_redirect_key ] ) ? esc_url_raw( $_COOKIE[ $this->cookie_redirect_key ] ) : false;

Expand Down Expand Up @@ -570,6 +490,109 @@ function get_user_by_identity( $subject_identity ) {
return false;
}

/**
* Validate User. Moved from authentication_request_callback to use also with authenticate filter.
*
* @param array<mixed> $token_response Response from login endpoint.
*
* @return WP_User|WP_Error
*/
private function validate( $token_response ) {
$client = $this->client;

// Allow for other plugins to alter data before validation.
$token_response = apply_filters( 'openid-connect-modify-token-response-before-validation', $token_response );

if ( is_wp_error( $token_response ) ) {
return $token_response;
}

// Ensure the that response contains required information.
$valid = $client->validate_token_response( $token_response );

if ( is_wp_error( $valid ) ) {
return $valid;
}

/**
* The id_token is used to identify the authenticated user, e.g. for SSO.
* The access_token must be used to prove access rights to protected
* resources e.g. for the userinfo endpoint
*/
$id_token_claim = $client->get_id_token_claim( $token_response );

// Allow for other plugins to alter data before validation.
$id_token_claim = apply_filters( 'openid-connect-modify-id-token-claim-before-validation', $id_token_claim );

if ( is_wp_error( $id_token_claim ) ) {
return $id_token_claim;
}

// Validate our id_token has required values.
$valid = $client->validate_id_token_claim( $id_token_claim );

if ( is_wp_error( $valid ) ) {
return $valid;
}

// If userinfo endpoint is set, exchange the token_response for a user_claim.
if ( ! empty( $this->settings->endpoint_userinfo ) && isset( $token_response['access_token'] ) ) {
$user_claim = $client->get_user_claim( $token_response );
} else {
$user_claim = $id_token_claim;
}

if ( is_wp_error( $user_claim ) ) {
return $user_claim;
}

// Validate our user_claim has required values.
$valid = $client->validate_user_claim( $user_claim, $id_token_claim );

if ( is_wp_error( $valid ) ) {
return $valid;
}

/**
* End authorization
* -
* Request is authenticated and authorized - start user handling
*/
$subject_identity = $client->get_subject_identity( $id_token_claim );
$user = $this->get_user_by_identity( $subject_identity );

if ( ! $user ) {
if ( $this->settings->create_if_does_not_exist ) {
$user = $this->create_new_user( $subject_identity, $user_claim );
if ( is_wp_error( $user ) ) {
return $user;
}
} else {
return new WP_Error( 'identity-not-map-existing-user', __( 'User identity is not linked to an existing WordPress user.', 'daggerhart-openid-connect-generic' ), $user_claim );
}
} else {
// Allow plugins / themes to take action using current claims on existing user (e.g. update role).
do_action( 'openid-connect-generic-update-user-using-current-claim', $user, $user_claim );
}

// Validate the found / created user.
$valid = $this->validate_user( $user );

if ( is_wp_error( $valid ) ) {
return $valid;
}

// Login the found / created user.
$this->login_user( $user, $token_response, $id_token_claim, $user_claim, $subject_identity );

do_action( 'openid-connect-generic-user-logged-in', $user );

// Log our success.
$this->logger->log( "Successful login for: {$user->user_login} ({$user->ID})", 'login-success' );

return $user;
}

/**
* Avoid user_login collisions by incrementing.
*
Expand Down Expand Up @@ -877,4 +900,41 @@ function update_existing_user( $uid, $subject_identity ) {
// Return our updated user.
return get_user_by( 'id', $uid );
}

/**
* Authenticate filter.
*
* @param WP_User|null|WP_Error $user User (if authenticated with previous filters).
* @param string $username Username given in login form.
* @param string $password Password given in login form.
*
* @return WP_User|null|WP_Error
*/
function authenticate_filter( $user, $username, $password ) {
if ( $user instanceof WP_User ) {
return $user;
}

if ( is_null( $user ) || is_wp_error( $user ) && $user->get_error_code() === 'invalid_username' ) {
if ( ! empty( $username ) && ! empty( $password ) ) {
$token_result = $this->client->request_authentication_token_by_username_and_password( $username, $password );

if ( is_wp_error( $token_result ) ) {
return $token_result;
}

// get the decoded response from the authentication request result.
$token_response = $this->client->get_token_response( $token_result );

$user = $this->validate( $token_response );

if ( $user instanceof WP_User ) {
// Log the results.
$this->logger->log( "User authenticated by openid server: {$user->user_login}", 'success' );
}
}
}

return $user;
}
}
34 changes: 34 additions & 0 deletions includes/openid-connect-generic-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,40 @@ function request_authentication_token( $code ) {
return $response;
}

/**
* Using username and password from WP login form (authenticate filter).
*
* @param string $username Username given in login form.
* @param string $password Password given in login form.
*
* @return array<mixed>|WP_Error
*/
function request_authentication_token_by_username_and_password( $username, $password ) {
$request = array(
'body' => array(
'grant_type' => 'password',
'client_id' => $this->client_id,
'client_secret' => $this->client_secret,
'username' => $username,
'password' => $password,
'scope' => $this->scope,
),
);

// Allow modifications to the request.
$request = apply_filters( 'openid-connect-generic-alter-request', $request, 'get-authentication-token-by-username-and-password' );

// Call the server and ask for new tokens.
$this->logger->log( $this->endpoint_token, 'request_new_tokens_by_username_and_password' );
$response = wp_remote_post( $this->endpoint_token, $request );

if ( is_wp_error( $response ) ) {
$response->add( 'request_authentication_token', __( 'Request for authentication token failed.', 'daggerhart-openid-connect-generic' ) );
}

return $response;
}

/**
* Using the refresh token, request new tokens from the idp
*
Expand Down
20 changes: 11 additions & 9 deletions includes/openid-connect-generic-option-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,17 @@
*
* Plugin Settings:
*
* @property bool $enforce_privacy The flag to indicates whether a user us required to be authenticated to access the site.
* @property bool $alternate_redirect_uri The flag to indicate whether to use the alternative redirect URI.
* @property bool $token_refresh_enable The flag whether to support refresh tokens by IDPs.
* @property bool $link_existing_users The flag to indicate whether to link to existing WordPress-only accounts or greturn an error.
* @property bool $create_if_does_not_exist The flag to indicate whether to create new users or not.
* @property bool $redirect_user_back The flag to indicate whether to redirect the user back to the page on which they started.
* @property bool $redirect_on_logout The flag to indicate whether to redirect to the login screen on session expiration.
* @property bool $enable_logging The flag to enable/disable logging.
* @property int $log_limit The maximum number of log entries to keep.
* @property bool $enforce_privacy The flag to indicates whether a user us required to be authenticated to access the site.
* @property bool $alternate_redirect_uri The flag to indicate whether to use the alternative redirect URI.
* @property bool $register_authenticate_filter The flag to add authenticate filter. grant_type 'password' should be allowed by user provider.
* @property int $authenticate_filter_priority Authenticate filter priority. Previous option must be checked.
* @property bool $token_refresh_enable The flag whether to support refresh tokens by IDPs.
* @property bool $link_existing_users The flag to indicate whether to link to existing WordPress-only accounts or greturn an error.
* @property bool $create_if_does_not_exist The flag to indicate whether to create new users or not.
* @property bool $redirect_user_back The flag to indicate whether to redirect the user back to the page on which they started.
* @property bool $redirect_on_logout The flag to indicate whether to redirect to the login screen on session expiration.
* @property bool $enable_logging The flag to enable/disable logging.
* @property int $log_limit The maximum number of log entries to keep.
*/
class OpenID_Connect_Generic_Option_Settings {

Expand Down
12 changes: 12 additions & 0 deletions includes/openid-connect-generic-settings-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,18 @@ private function get_settings_fields() {
'type' => 'checkbox',
'section' => 'authorization_settings',
),
'register_authenticate_filter' => array(
'title' => __( 'Register Authenticate Filter', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Enable login by entering username and password on wordpress site without forwarding end user to Identity provider server. The grant_type "password" must be allowed on server.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'section' => 'authorization_settings',
),
'authenticate_filter_priority' => array(
'title' => __( 'Authenticate Filter Priority', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Previous options must be checked. The default wordpress authenticate filter has priority 20. The default value here is 15.', 'daggerhart-openid-connect-generic' ),
'type' => 'number',
'section' => 'authorization_settings',
),
'nickname_key' => array(
'title' => __( 'Nickname Key', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Where in the user claim array to find the user\'s nickname. Possible standard values: preferred_username, name, or sub.', 'daggerhart-openid-connect-generic' ),
Expand Down
Loading

0 comments on commit 0f8e767

Please sign in to comment.