Skip to content

Commit

Permalink
Merge pull request #5 from jamesmorrison/feature/1.0.3
Browse files Browse the repository at this point in the history
1.0.3 Release
  • Loading branch information
jamesmorrison authored Jan 26, 2024
2 parents d13d067 + 63e42f2 commit 9e92d26
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 40 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented in this file, per [the Keep a Changelog standard](http://keepachangelog.com/), and will adhere to [Semantic Versioning](http://semver.org/).

## [1.0.3] - 2024-01-26
- Added constant `(bool)` `CF_ACCESS_CREATE_ACCOUNT`: whether an account should be created when authenticated through Cloudflare
- Added constant `(string)` `CF_ACCESS_NEW_USER_ROLE`: the new user role; defaults to subscriber
- Fixed composer dependencies issue; plugin returns error if the plugin needs `composer install` run 🎉
- Added `cloudflare_access_sso_plugin_pre_init` hook (prior to plugin loading) 🎉
- Corrected minor PHPCS compatibility issues (PHP 8.2) and added coding standards (WordPress-Extra) 🎉

## [1.0.2] - 2023-06-23
- Corrected version number throughout to 1.0.2. 🎉

Expand Down
2 changes: 1 addition & 1 deletion CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ Thank you to all the people who have already contributed to this repository via
## Libraries

The following software libraries are utilized in this repository:
- N/A
- [PHP JWT](https://github.com/firebase/php-jwt)
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ define( 'CF_ACCESS_AUD',
);
```

Optionally, two additional constants can also be set:
Optionally, four additional constants can also be set:

`CF_ACCESS_ATTEMPTS` The number of attempts to login via Cloudflare Access.

Expand All @@ -56,7 +56,16 @@ Default: (int) `3` if not set.

Default (int) `60` if not set.

> **Note:** Where the application is not configured correctly (authorisation header is not set, or the team name / AUD are incorrect), SSO is silently disabled. You can check the cookies section of inspector tools to confirm whether the cookie has been set.
`CF_ACCESS_CREATE_ACCOUNT` Whether an account should be created for a (Cloudflare) authenticated user if it doesn't exist
Note: This is dependent on the settings for your Cloudflare Access application; if you only allow "internal" users, "external" users won't be able to access the site at all.

Default: (bool) `false` if not set.

`CF_ACCESS_NEW_USER_ROLE` The role for user accounts created. Requires `CF_ACCESS_CREATE_ACCOUNT` to be true (is otherwise ignored).

Default: (string) `subscriber`

> **Note:** Where the application is not configured correctly (authorisation header is not set, or the team name / AUD are incorrect), SSO is **silently disabled**. You can check the cookies section of inspector tools to confirm whether the cookie has been set.
### Disclaimer
This plugin is not affiliated with nor developed by Cloudflare. All trademarks, service marks and company names are the property of their respective owners.
41 changes: 29 additions & 12 deletions cloudflare-access-sso.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* Plugin Name: Cloudflare Access SSO
* Description: Facilitates automatic login to WordPress when domain is protected with Cloudflare Access
* Version: 1.0.2
* Version: 1.0.3
* Plugin URI: https://github.com/jamesmorrison/cloudflare-access-sso
* Author: James Morrison
* Author URI: https://jamesmorrison.uk/
Expand All @@ -35,13 +35,15 @@

// The Cloudflare Team Name is required
if ( ! defined( 'CF_ACCESS_TEAM_NAME' ) ) {
error_log( 'Cloudflare Access SSO Error: CF_ACCESS_TEAM_NAME is not defined.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'Cloudflare Access SSO Error: Required constant: CF_ACCESS_TEAM_NAME is not defined.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( sprintf( 'Review the documentation at %s for setup instructions.', CF_ACCESS_GITHUB_URL ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return;
}

// The Cloudflare Application ID is required
if ( ! defined( 'CF_ACCESS_AUD' ) ) {
error_log( 'Cloudflare Access SSO Error: CF_ACCESS_AUD is not defined.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'Cloudflare Access SSO Error: Required constant: CF_ACCESS_AUD is not defined.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( sprintf( 'Review the documentation at %s for setup instructions.', CF_ACCESS_GITHUB_URL ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return;
}

Expand All @@ -55,19 +57,34 @@
define( 'CF_ACCESS_LEEWAY', 60 );
}

// Default to not creating accounts
if ( ! defined( 'CF_ACCESS_CREATE_ACCOUNT' ) ) {
define( 'CF_ACCESS_CREATE_ACCOUNT', false );
}

// Default to subscriber role for new accounts
if ( ! defined( 'CF_ACCESS_NEW_USER_ROLE' ) ) {
define( 'CF_ACCESS_NEW_USER_ROLE', 'subscriber' );
}

// Useful global constants
define( 'CLOUDFLARE_ACCESS_SSO_PLUGIN_VERSION', '1.0.2' );
define( 'CLOUDFLARE_ACCESS_SSO_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'CLOUDFLARE_ACCESS_SSO_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
define( 'CLOUDFLARE_ACCESS_SSO_PLUGIN_INC', CLOUDFLARE_ACCESS_SSO_PLUGIN_PATH . 'includes/' );
define( 'CF_ACCESS_SSO_PLUGIN_VERSION', '1.0.3' );
define( 'CF_ACCESS_SSO_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'CF_ACCESS_SSO_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
define( 'CF_ACCESS_SSO_PLUGIN_INC', CF_ACCESS_SSO_PLUGIN_PATH . 'includes/' );
define( 'CF_ACCESS_GITHUB_URL', 'https://github.com/jamesmorrison/cloudflare-access-sso' );

// Require Composer autoloader if it exists
if ( file_exists( CLOUDFLARE_ACCESS_SSO_PLUGIN_PATH . 'vendor/autoload.php' ) ) {
require_once CLOUDFLARE_ACCESS_SSO_PLUGIN_PATH . 'vendor/autoload.php';
// Load plugin classes
if ( ! file_exists( CF_ACCESS_SSO_PLUGIN_PATH . 'vendor/autoload.php' ) ) {
error_log( 'Cloudflare Access SSO Error: Composer dependencies are missing. Please run `composer install` in the plugin directory.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return;
}

// Include files
require_once CLOUDFLARE_ACCESS_SSO_PLUGIN_INC . '/core.php';
// Load composer dependencies
require_once CF_ACCESS_SSO_PLUGIN_PATH . 'vendor/autoload.php';

// Load plugin files
require_once CF_ACCESS_SSO_PLUGIN_INC . '/core.php';

// Activation / Deactivation
register_activation_hook( __FILE__, '\CloudflareAccessSSO\Core\activate' );
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jamesmorrison/cloudflare-access-sso",
"description": "Facilitates SSO login to WordPress via Cloudflare Access.",
"version": "1.0.2",
"version": "1.0.3",
"type": "wordpress-plugin",
"homepage": "https://james.morrison.uk/plugins/cloudflare-access-sso/",
"readme": "./readme.md",
Expand All @@ -27,4 +27,4 @@
"CloudflareAccessSSO\\": "includes/classes/"
}
}
}
}
16 changes: 8 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 84 additions & 13 deletions includes/classes/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ public static function get_instance() {
*/
public function setup() {
// Process SSO login on the 1st hook available on the login page.
add_action( 'login_head', array( $this, 'process_login' ) );
add_action( 'login_head', [ $this, 'process_login' ] );

// Logout from Cloudflare Access once WP logout is complete.
add_filter( 'logout_redirect', array( $this, 'set_cloudflare_access_logout_url' ), 10, 3 );
add_filter( 'logout_redirect', [ $this, 'set_cloudflare_access_logout_url' ], 10, 3 );
}

/**
Expand All @@ -78,7 +78,6 @@ public function process_login() {
$certificates = $this->get_cloudflare_certificates();
$login_attempts = 0;
$user = false;
$user_id = 0;

// On cache error, force update the certificates
if ( is_wp_error( $certificates ) ) {
Expand All @@ -92,13 +91,45 @@ public function process_login() {
while ( $login_attempts < CF_ACCESS_ATTEMPTS ) {
try {
JWT::$leeway = CF_ACCESS_LEEWAY;
$jwt_decoded = JWT::decode( $authorisation_header, JWK::parseKeySet( $certificates ), array( 'RS256' ) );

if ( isset( $jwt_decoded->email ) && isset( $jwt_decoded->aud ) && isset( $jwt_decoded->aud[0] ) && $this->verify_aud( $jwt_decoded->aud[0] ) ) {
$user = get_user_by( 'email', $jwt_decoded->email );
$jwt = JWT::decode( $authorisation_header, JWK::parseKeySet( $certificates ) );

if ( $this->validate_jwt( $jwt ) ) {
$user = get_user_by( 'email', $jwt->email );

// If a matching user is not found and create an account
if ( ! is_a( $user, '\WP_User' ) && CF_ACCESS_CREATE_ACCOUNT ?? false ) {
$user_id = wp_insert_user(
[
'user_login' => $jwt->email,
'user_email' => $jwt->email,
'user_pass' => wp_generate_password( 128, true, true ),
'role' => $this->validate_new_user_role( CF_ACCESS_NEW_USER_ROLE ) ?? 'subscriber',
]
);

if ( ! is_wp_error( $user_id ) ) {
$user = get_user_by( 'id', $user_id );

// Add user meta to indicate that the user was created by Cloudflare Access SSO.
update_user_meta( $user_id, 'cf_access_sso_created', true );
update_user_meta( $user_id, 'cf_access_sso_created_at', time() );
}
}

// If a matching user is found, facilitate log in.
if ( is_a( $user, '\WP_User' ) ) {
// If there is no meta for cf_access_sso_enabled, then this is the first login.
// Add user meta to identify when Clouflare Access SSO was enabled.
// This may be used in a future release to prevent typical username / password access.
if ( ! get_user_meta( $user->ID, 'cf_access_sso_enabled', true ) ) {
update_user_meta( $user->ID, 'cf_access_sso_enabled', true );
update_user_meta( $user->ID, 'cf_access_sso_enabled_at', time() );
}

// Set the last login time.
update_user_meta( $user_id, 'cf_access_sso_last_login', time() );

wp_set_auth_cookie( $user->ID );
wp_set_current_user( $user->ID );
do_action( 'wp_login', $user->name, $user );
Expand All @@ -111,20 +142,20 @@ public function process_login() {
$certificates = $this->get_cloudflare_certificates( true );
}

$login_attempts ++;
++$login_attempts;
}
}

/**
* Set Cloudflare Access Logout URL
*
* @param string $redirect_to The redirect destination URL.
* @param string $requested_redirect_to The requested redirect destination URL passed as a parameter.
* @param WP_User $user The WP_User object for the user that's logging out.
* @param string $requested_redirect_to The requested redirect destination URL (passed as a query parameter).
* @param WP_User $user The WP_User object for the user who has logged out.
*
* @return string Logout URL.
*/
public function set_cloudflare_access_logout_url( $redirect_to, $requested_redirect_to, $user ) {
public function set_cloudflare_access_logout_url( $redirect_to, $requested_redirect_to, $user ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
// Set the redirect URL to logout from Cloudflare Access - but only where the authorisation header exists.
if ( $this->get_authorisation_header() ) {
return esc_url( home_url( '/cdn-cgi/access/logout' ) );
Expand Down Expand Up @@ -158,14 +189,14 @@ protected function get_authorisation_header() {
* @return array|\WP_Error
*/
protected function get_cloudflare_certificates( $force = false ) {
$certificates = wp_cache_get( 'cf_access_sso_certficates', self::$cache_group );
$certificates = wp_cache_get( 'cf_access_certficates', self::$cache_group );

if ( ! $certificates || $force ) {
try {
$response = wp_remote_get( esc_url( self::$cloudflare_api_url ) );
$certificates = json_decode( wp_remote_retrieve_body( $response ), true );
wp_cache_set( 'cf_access_sso_certficates', $certificates, self::$cache_group, 7 * DAY_IN_SECONDS );
wp_cache_set( 'cf_access_sso_certficates_last_updated', time(), self::$cache_group, 30 * DAY_IN_SECONDS );
wp_cache_set( 'cf_access_certficates', $certificates, self::$cache_group, 7 * DAY_IN_SECONDS );
wp_cache_set( 'cf_access_certficates_last_updated', time(), self::$cache_group, 30 * DAY_IN_SECONDS );
} catch ( \Exception $e ) {
return new WP_Error( 'cf_access_sso_certificates_error', $e->getMessage(), self::$cloudflare_api_url );
}
Expand All @@ -174,6 +205,34 @@ protected function get_cloudflare_certificates( $force = false ) {
return $certificates;
}

/**
* Get Cloudflare Certificates Last Updated
*
* @return int
*/
protected function get_cloudflare_certificates_last_updated() {
return wp_cache_get( 'cf_access_certficates_last_updated', self::$cache_group );
}

/**
* Get Cloudflare Certificates Next Update
*
* @return int
*/
protected function get_cloudflare_certificates_next_update() {
return $this->get_cloudflare_certificates_last_updated() + ( 7 * DAY_IN_SECONDS );
}

/**
* Validate JWT
*
* @param object $jwt The JWT to validate.
* @return bool
*/
protected function validate_jwt( $jwt ) {
return isset( $jwt->email ) && isset( $jwt->aud ) && isset( $jwt->aud[0] ) && $this->verify_aud( $jwt->aud[0] );
}

/**
* Verify AUD
*
Expand All @@ -188,4 +247,16 @@ protected function verify_aud( $aud ) {
return false;
}

/**
* Validate New User Role
*
* @param string $role The role to validate.
* @return string
*/
protected function validate_new_user_role( $role ) {
if ( in_array( $role, get_editable_roles(), true ) ) {
return $role;
}
return false;
}
}
6 changes: 4 additions & 2 deletions includes/core.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function setup() {
function i18n() {
$locale = apply_filters( 'plugin_locale', get_locale(), 'cloudflare-access-sso' );
load_textdomain( 'cloudflare-access-sso', WP_LANG_DIR . '/cloudflare-access-sso/cloudflare-access-sso-' . $locale . '.mo' );
load_plugin_textdomain( 'cloudflare-access-sso', false, plugin_basename( CLOUDFLARE_ACCESS_SSO_PLUGIN_PATH ) . '/languages/' );
load_plugin_textdomain( 'cloudflare-access-sso', false, plugin_basename( CF_ACCESS_SSO_PLUGIN_PATH ) . '/languages/' );
}

/**
Expand All @@ -41,6 +41,8 @@ function i18n() {
*/
function init() {

do_action( 'cloudflare_access_sso_plugin_pre_init' );

if ( class_exists( '\CloudflareAccessSSO\Plugin' ) ) {
\CloudflareAccessSSO\Plugin::get_instance()->setup();
}
Expand All @@ -64,6 +66,6 @@ function activate() {
* @return void
*/
function deactivate() {
// Flush cache to remove Cloudflare Certificates
// Flushing the cache removes stored Cloudflare Certificates
wp_cache_flush();
}
5 changes: 5 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0"?>
<ruleset name="WordPress Extra Rules">
<description>WordPress Extra Rules</description>
<rule ref="WordPress-Extra"/>
</ruleset>

0 comments on commit 9e92d26

Please sign in to comment.