Skip to content

Instantly share code, notes, and snippets.

@gomasy
Last active August 10, 2026 19:46
Show Gist options
  • Select an option

  • Save gomasy/5f01c52b4bece379585969c42221b8b9 to your computer and use it in GitHub Desktop.

Select an option

Save gomasy/5f01c52b4bece379585969c42221b8b9 to your computer and use it in GitHub Desktop.
<?php
/**
* Plugin Name: Cloudflare Access SSO
* Description: Cloudflare Access の JWT を検証し、認証済みメールアドレスに対応する WordPress ユーザーへ自動ログインします。
* Version: 2.0.0
* Requires PHP: 7.2
*
* 設置場所: wp-content/mu-plugins/cf-access-sso.php
*
* --- 必須 ---
* define('CF_ACCESS_TEAM_DOMAIN', 'https://example.cloudflareaccess.com');
* define('CF_ACCESS_AUD', '4714c1358e65fe4b...'); // 文字列または配列
*
* --- 任意 ---
* define('CF_ACCESS_AUTO_CREATE', false); // 未登録メールのユーザーを自動作成
* define('CF_ACCESS_DEFAULT_ROLE', 'subscriber'); // 自動作成時のロール
* define('CF_ACCESS_ENFORCE_SESSION', true); // Access セッション消失時に WP も強制ログアウト
* define('CF_ACCESS_HEADER_ONLY', false); // JWT 検証を省きヘッダーのみ信頼(非推奨)
* define('CF_ACCESS_LOGOUT_URL', '...'); // 既定: <サイト>/cdn-cgi/access/logout
* define('CF_ACCESS_DEBUG', false); // error_log への診断出力(WP_DEBUG と併用)
*
*/
if (!defined('ABSPATH')) {
exit;
}
/* =========================================================================
* 共通ユーティリティ
* ====================================================================== */
final class CF_Access_Util {
/**
* base64url をデコードする。不正な入力は空文字を返す。
*
* @param string $input
* @return string
*/
public static function b64url_decode($input) {
if (!is_string($input) || $input === '') {
return '';
}
$decoded = base64_decode(strtr($input, '-_', '+/'), true);
return $decoded === false ? '' : $decoded;
}
/**
* 診断ログ。トークンや Cookie の値は決して渡さないこと。
*
* @param string $message
*/
public static function log($message) {
if (CF_Access_Config::debug()) {
error_log('[cf-access-sso] ' . $message);
}
}
}
/* =========================================================================
* 設定
* ====================================================================== */
final class CF_Access_Config {
/** @var array 正規化済みの値をリクエスト内で使い回す */
private static $cache = [];
/**
* チームドメイン。スキーム省略や http 指定は https に正規化する。
*
* @return string|null
*/
public static function team_domain() {
if (array_key_exists('team_domain', self::$cache)) {
return self::$cache['team_domain'];
}
$value = null;
if (defined('CF_ACCESS_TEAM_DOMAIN') && is_string(CF_ACCESS_TEAM_DOMAIN)) {
$domain = untrailingslashit(trim(CF_ACCESS_TEAM_DOMAIN));
if ($domain !== '') {
$value = 'https://' . preg_replace('#^https?://#i', '', $domain);
}
}
return self::$cache['team_domain'] = $value;
}
/**
* 許可する Application Audience (AUD) の一覧。
*
* @return string[]
*/
public static function audiences() {
if (array_key_exists('audiences', self::$cache)) {
return self::$cache['audiences'];
}
$out = [];
if (defined('CF_ACCESS_AUD')) {
$list = is_array(CF_ACCESS_AUD) ? CF_ACCESS_AUD : [CF_ACCESS_AUD];
foreach ($list as $aud) {
if (is_string($aud) && $aud !== '') {
$out[] = $aud;
}
}
}
return self::$cache['audiences'] = $out;
}
/**
* JWT 検証に必要な設定が揃っているか。
*
* @return bool
*/
public static function is_configured() {
return self::team_domain() !== null && self::audiences() !== [];
}
/** @return bool */
public static function header_only() {
return self::flag('CF_ACCESS_HEADER_ONLY', false);
}
/** @return bool */
public static function enforce_session() {
return self::flag('CF_ACCESS_ENFORCE_SESSION', true);
}
/** @return bool */
public static function auto_create() {
return self::flag('CF_ACCESS_AUTO_CREATE', false);
}
/** @return bool */
public static function debug() {
return self::flag('WP_DEBUG', false) && self::flag('CF_ACCESS_DEBUG', false);
}
/** @return string */
public static function default_role() {
if (defined('CF_ACCESS_DEFAULT_ROLE') && is_string(CF_ACCESS_DEFAULT_ROLE)
&& CF_ACCESS_DEFAULT_ROLE !== '') {
return CF_ACCESS_DEFAULT_ROLE;
}
return 'subscriber';
}
/**
* ログアウト時の遷移先。null なら WordPress 標準の挙動に委ねる。
*
* @return string|null
*/
public static function logout_url() {
if (!defined('CF_ACCESS_LOGOUT_URL')) {
// 既定値はサイト自身のホスト。ここでしかサイト側の Cookie は削除できない。
return home_url('/cdn-cgi/access/logout');
}
$url = CF_ACCESS_LOGOUT_URL;
if (!is_string($url) || $url === '') {
return null;
}
return esc_url_raw($url);
}
/**
* @param string $constant
* @param bool $default
* @return bool
*/
private static function flag($constant, $default) {
return defined($constant) ? (bool) constant($constant) : (bool) $default;
}
}
/* =========================================================================
* 公開鍵の取得
* ====================================================================== */
final class CF_Access_Keys {
/** 取得済みの鍵(transient) */
const CACHE_KEY = 'cf_access_sso_certs';
const CACHE_TTL = 3600;
/** 取得失敗時に使う、最後に成功した鍵(option) */
const BACKUP_KEY = 'cf_access_sso_certs_backup';
/** 外部リクエストの連打を防ぐロック(transient) */
const LOCK_KEY = 'cf_access_sso_certs_lock';
const LOCK_TTL = 300;
/**
* kid に対応する公開鍵 PEM を返す。未知の kid なら一度だけ再取得を試みる。
*
* @param string $kid
* @return string|null
*/
public static function pem_for($kid) {
$keys = self::all();
if (isset($keys[$kid])) {
return $keys[$kid];
}
// 鍵ローテーション直後の可能性。ロックがあるため連打にはならない。
$keys = self::all(true);
return isset($keys[$kid]) ? $keys[$kid] : null;
}
/**
* @param bool $force キャッシュを無視して取得を試みる
* @return array<string,string> kid => PEM
*/
private static function all($force = false) {
if (!$force) {
$cached = get_transient(self::CACHE_KEY);
if (is_array($cached) && $cached) {
return $cached;
}
}
// 存在しない kid を送り続けることで外部リクエストを誘発する攻撃を防ぐ
if (get_transient(self::LOCK_KEY)) {
return self::backup();
}
set_transient(self::LOCK_KEY, 1, self::LOCK_TTL);
$fetched = self::fetch();
if (!$fetched) {
// Cloudflare 側の一時障害で全員をログアウトさせないための保険
CF_Access_Util::log('key fetch failed; falling back to last known good keys');
return self::backup();
}
set_transient(self::CACHE_KEY, $fetched, self::CACHE_TTL);
if (self::backup() !== $fetched) {
update_option(self::BACKUP_KEY, $fetched, false);
}
return $fetched;
}
/**
* /cdn-cgi/access/certs から鍵を取得する。
* public_certs(PEM)を優先し、無ければ keys(JWKS)から組み立てる。
*
* @return array<string,string>
*/
private static function fetch() {
$domain = CF_Access_Config::team_domain();
if (!$domain) {
return [];
}
$res = wp_remote_get($domain . '/cdn-cgi/access/certs', ['timeout' => 5]);
if (is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200) {
return [];
}
$body = json_decode(wp_remote_retrieve_body($res), true);
if (!is_array($body)) {
return [];
}
$keys = [];
if (!empty($body['public_certs']) && is_array($body['public_certs'])) {
foreach ($body['public_certs'] as $entry) {
if (is_array($entry) && !empty($entry['kid']) && !empty($entry['cert'])
&& is_string($entry['kid']) && is_string($entry['cert'])) {
$keys[$entry['kid']] = $entry['cert'];
}
}
}
if (!$keys && !empty($body['keys']) && is_array($body['keys'])) {
foreach ($body['keys'] as $jwk) {
if (!is_array($jwk) || empty($jwk['kid']) || !is_string($jwk['kid'])) {
continue;
}
$pem = self::jwk_to_pem($jwk);
if ($pem) {
$keys[$jwk['kid']] = $pem;
}
}
}
return $keys;
}
/**
* @return array<string,string>
*/
private static function backup() {
$backup = get_option(self::BACKUP_KEY);
return is_array($backup) ? $backup : [];
}
/**
* RSA の JWK を PEM 形式の SubjectPublicKeyInfo に変換する。
*
* @param array $jwk
* @return string|null
*/
private static function jwk_to_pem(array $jwk) {
if (empty($jwk['kty']) || $jwk['kty'] !== 'RSA' || empty($jwk['n']) || empty($jwk['e'])) {
return null;
}
$modulus = CF_Access_Util::b64url_decode($jwk['n']);
$exponent = CF_Access_Util::b64url_decode($jwk['e']);
if ($modulus === '' || $exponent === '') {
return null;
}
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
$rsa_key = self::der(0x30, self::der_integer($modulus) . self::der_integer($exponent));
// AlgorithmIdentifier ::= SEQUENCE { OID rsaEncryption, NULL }
$algorithm = self::der(
0x30,
self::der(0x06, "\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01") . self::der(0x05, '')
);
// SubjectPublicKeyInfo ::= SEQUENCE { AlgorithmIdentifier, BIT STRING }
$spki = self::der(0x30, $algorithm . self::der(0x03, "\x00" . $rsa_key));
return "-----BEGIN PUBLIC KEY-----\n"
. chunk_split(base64_encode($spki), 64, "\n")
. "-----END PUBLIC KEY-----\n";
}
/**
* @param int $tag
* @param string $value
* @return string
*/
private static function der($tag, $value) {
return chr($tag) . self::der_length(strlen($value)) . $value;
}
/**
* @param int $length
* @return string
*/
private static function der_length($length) {
if ($length < 0x80) {
return chr($length);
}
$bytes = '';
while ($length > 0) {
$bytes = chr($length & 0xff) . $bytes;
$length >>= 8;
}
return chr(0x80 | strlen($bytes)) . $bytes;
}
/**
* DER の INTEGER は符号付きなので、最上位ビットが立つ場合は 0x00 を前置する。
*
* @param string $binary
* @return string
*/
private static function der_integer($binary) {
$binary = ltrim($binary, "\x00");
if ($binary === '') {
$binary = "\x00";
}
if (ord($binary[0]) & 0x80) {
$binary = "\x00" . $binary;
}
return self::der(0x02, $binary);
}
}
/* =========================================================================
* Access が認証した識別情報(値オブジェクト)
* ====================================================================== */
final class CF_Access_Identity {
/** @var string */
private $email;
/** @var int|null JWT の exp。ヘッダーのみのモードでは null */
private $expires_at;
public function __construct($email, $expires_at = null) {
$this->email = $email;
$this->expires_at = $expires_at;
}
/** @return string */
public function email() {
return $this->email;
}
/** @return int|null */
public function expires_at() {
return $this->expires_at;
}
}
/* =========================================================================
* トークンの取得と検証
* ====================================================================== */
final class CF_Access_Token {
/** 時刻ずれの許容秒数 */
const LEEWAY = 60;
/** @var CF_Access_Identity|false|null false = 該当なし、null = 未評価 */
private static $current = null;
/**
* このリクエストの識別情報。検証に失敗した場合は null。
*
* @return CF_Access_Identity|null
*/
public static function current() {
if (self::$current !== null) {
return self::$current ?: null;
}
self::$current = self::resolve() ?: false;
return self::$current ?: null;
}
/**
* @return CF_Access_Identity|null
*/
private static function resolve() {
if (CF_Access_Config::header_only()) {
return self::from_header();
}
if (!CF_Access_Config::is_configured()) {
CF_Access_Util::log('CF_ACCESS_TEAM_DOMAIN / CF_ACCESS_AUD is not configured');
return null;
}
$token = self::raw_token();
if (!$token) {
return null;
}
$payload = self::verify($token);
if (!$payload) {
return null;
}
// Service Token(非対話認証)は email を持たないためログインさせない
if (empty($payload['email']) || !is_string($payload['email'])) {
CF_Access_Util::log('token has no email claim (service token?)');
return null;
}
$email = sanitize_email($payload['email']);
if (!is_email($email)) {
return null;
}
return new CF_Access_Identity($email, isset($payload['exp']) ? (int) $payload['exp'] : null);
}
/**
* 非推奨モード。オリジンへの直接到達経路が存在しない場合のみ成立する。
*
* @return CF_Access_Identity|null
*/
private static function from_header() {
if (empty($_SERVER['HTTP_CF_ACCESS_AUTHENTICATED_USER_EMAIL'])) {
return null;
}
$email = sanitize_email(wp_unslash($_SERVER['HTTP_CF_ACCESS_AUTHENTICATED_USER_EMAIL']));
return is_email($email) ? new CF_Access_Identity($email) : null;
}
/**
* ヘッダーを優先し、無ければ Cookie から取る。
* ヘッダーは Access を通過したリクエストにしか付かないため、
* アプリのパス外(/wp-admin/ など)では Cookie が唯一の経路になる。
*
* @return string|null
*/
private static function raw_token() {
if (!empty($_SERVER['HTTP_CF_ACCESS_JWT_ASSERTION'])) {
return trim(wp_unslash($_SERVER['HTTP_CF_ACCESS_JWT_ASSERTION']));
}
if (!empty($_COOKIE['CF_Authorization'])) {
return trim(wp_unslash($_COOKIE['CF_Authorization']));
}
return null;
}
/**
* RS256 署名・iss・aud・exp/nbf を検証してペイロードを返す。
*
* @param string $token
* @return array|null
*/
private static function verify($token) {
$parts = explode('.', $token);
if (count($parts) !== 3) {
return null;
}
list($b64_header, $b64_payload, $b64_signature) = $parts;
$header = json_decode(CF_Access_Util::b64url_decode($b64_header), true);
$payload = json_decode(CF_Access_Util::b64url_decode($b64_payload), true);
$signature = CF_Access_Util::b64url_decode($b64_signature);
if (!is_array($header) || !is_array($payload) || $signature === '') {
return null;
}
if (!self::verify_signature($header, $b64_header . '.' . $b64_payload, $signature)) {
return null;
}
if (!self::verify_time($payload)) {
return null;
}
if (!self::verify_issuer($payload)) {
return null;
}
if (!self::verify_audience($payload)) {
return null;
}
return $payload;
}
/**
* @param array $header
* @param string $signed_input
* @param string $signature
* @return bool
*/
private static function verify_signature(array $header, $signed_input, $signature) {
// alg を固定する。none や HS256 への差し替えを許すと署名検証が無意味になる。
if (!isset($header['alg']) || $header['alg'] !== 'RS256') {
CF_Access_Util::log('unexpected alg');
return false;
}
if (empty($header['kid']) || !is_string($header['kid'])) {
return false;
}
$pem = CF_Access_Keys::pem_for($header['kid']);
if (!$pem) {
CF_Access_Util::log('no public key for kid');
return false;
}
$public_key = openssl_pkey_get_public($pem);
if ($public_key === false) {
CF_Access_Util::log('failed to parse public key');
return false;
}
if (openssl_verify($signed_input, $signature, $public_key, OPENSSL_ALGO_SHA256) !== 1) {
CF_Access_Util::log('signature verification failed');
return false;
}
return true;
}
/**
* @param array $payload
* @return bool
*/
private static function verify_time(array $payload) {
$now = time();
if (!isset($payload['exp']) || ($now - self::LEEWAY) >= (int) $payload['exp']) {
CF_Access_Util::log('token expired');
return false;
}
if (isset($payload['nbf']) && ($now + self::LEEWAY) < (int) $payload['nbf']) {
return false;
}
return true;
}
/**
* @param array $payload
* @return bool
*/
private static function verify_issuer(array $payload) {
if (empty($payload['iss']) || !is_string($payload['iss'])) {
return false;
}
if (!hash_equals(CF_Access_Config::team_domain(), untrailingslashit($payload['iss']))) {
CF_Access_Util::log('issuer mismatch');
return false;
}
return true;
}
/**
* @param array $payload
* @return bool
*/
private static function verify_audience(array $payload) {
$actual_list = isset($payload['aud']) ? (array) $payload['aud'] : [];
foreach ($actual_list as $actual) {
if (!is_string($actual)) {
continue;
}
foreach (CF_Access_Config::audiences() as $expected) {
if (hash_equals($expected, $actual)) {
return true;
}
}
}
CF_Access_Util::log('audience mismatch (check CF_ACCESS_AUD)');
return false;
}
}
/* =========================================================================
* WordPress 統合
* ====================================================================== */
final class CF_Access_SSO {
/** wp-login.php で素通しするアクション */
const PASSTHROUGH_ACTIONS = ['logout', 'postpass', 'confirmaction'];
/** @var int|false|null 解決済みユーザーID */
private static $user_id = null;
public static function boot() {
// 認証 Cookie の発行。auth_redirect() より前である必要があるため plugins_loaded。
add_action('plugins_loaded', [__CLASS__, 'establish_session'], 1);
// wp-login.php は reauth=1 で Cookie を消してから login_init を撃つので張り直す。
add_action('login_init', [__CLASS__, 'login_init'], 1);
// WordPress の Cookie 操作は $_COOKIE を更新しない。同一リクエスト内の
// auth_redirect() や nonce 生成と食い違わないよう、発行・削除の両方を反映させる。
add_action('set_auth_cookie', [__CLASS__, 'remember_auth_cookie'], 10, 5);
add_action('set_logged_in_cookie', [__CLASS__, 'remember_logged_in_cookie'], 10, 1);
add_action('clear_auth_cookie', [__CLASS__, 'forget_cookies'], 10, 0);
// WordPress のセッションが Access のセッションより長生きしないようにする。
add_filter('auth_cookie_expiration', [__CLASS__, 'auth_cookie_expiration'], 99, 3);
add_filter('logout_redirect', [__CLASS__, 'logout_redirect'], 10, 3);
add_filter('allowed_redirect_hosts', [__CLASS__, 'allowed_redirect_hosts'], 10, 1);
}
/* ---------------------------------------------------------------- */
/* セッション */
/* ---------------------------------------------------------------- */
/**
* Access の識別情報に合わせて WordPress のセッションを同期する。
*/
public static function establish_session() {
if (self::is_non_browser_context() || headers_sent()) {
return;
}
$user_id = self::user_id();
if ($user_id) {
self::issue_cookie($user_id);
return;
}
// Access のセッションが無い。WordPress 側も終了させる。
// wp_clear_auth_cookie() ではなく wp_logout() を使う。前者は Cookie を消すだけで
// サーバー側のセッショントークンが user_meta に残り、再利用できてしまう。
if (CF_Access_Config::enforce_session() && wp_validate_auth_cookie('', 'logged_in')) {
CF_Access_Util::log('no Access identity; terminating WordPress session');
wp_logout();
}
}
/**
* 必要であれば認証 Cookie を発行し、カレントユーザーを確定する。
*
* @param int $user_id
*/
private static function issue_cookie($user_id) {
$current = wp_validate_auth_cookie('', 'logged_in');
if ($current === $user_id) {
return; // 既に有効な Cookie がある
}
if ($current) {
wp_clear_auth_cookie(); // 別ユーザーの Cookie が残っている
}
wp_set_auth_cookie($user_id, false);
wp_set_current_user($user_id);
CF_Access_Util::log(sprintf('issued auth cookie for user #%d', $user_id));
}
/**
* ログイン画面。reauth=1 で消された Cookie を張り直し、目的地へ送る。
*/
public static function login_init() {
$action = isset($_REQUEST['action']) ? sanitize_key(wp_unslash($_REQUEST['action'])) : '';
// ログアウト・投稿パスワード・プライバシー確認リンクは WordPress 本来の処理に任せる
if (in_array($action, self::PASSTHROUGH_ACTIONS, true)) {
return;
}
$user_id = self::user_id();
if (!$user_id) {
wp_die(
esc_html__('このサイトへのログインは Cloudflare Access 経由でのみ可能です。', 'cf-access-sso'),
esc_html__('Forbidden', 'cf-access-sso'),
['response' => 403]
);
}
if (!headers_sent()) {
self::issue_cookie($user_id);
}
// セッション切れモーダル(iframe 内)はリダイレクトさせず、そのまま描画させる
if (isset($_REQUEST['interim-login'])) {
return;
}
wp_safe_redirect(self::post_login_redirect());
exit;
}
/**
* ログイン後の遷移先。wp-login.php 自身が指定された場合はループするため除外する。
*
* @return string
*/
private static function post_login_redirect() {
$fallback = admin_url();
if (empty($_REQUEST['redirect_to'])) {
return $fallback;
}
$target = wp_unslash($_REQUEST['redirect_to']);
if (!is_string($target) || $target === '') {
return $fallback;
}
$path = (string) wp_parse_url($target, PHP_URL_PATH);
return strpos($path, 'wp-login.php') !== false ? $fallback : $target;
}
/**
* WP-CLI・cron など、Cookie を扱う意味がない実行文脈かどうか。
*
* @return bool
*/
private static function is_non_browser_context() {
return (defined('WP_CLI') && WP_CLI)
|| (defined('DOING_CRON') && DOING_CRON)
|| (defined('WP_INSTALLING') && WP_INSTALLING);
}
/* ---------------------------------------------------------------- */
/* Cookie の整合性 */
/* ---------------------------------------------------------------- */
public static function remember_auth_cookie($cookie, $expire, $expiration, $user_id, $scheme) {
$name = ($scheme === 'secure_auth') ? SECURE_AUTH_COOKIE : AUTH_COOKIE;
$_COOKIE[$name] = $cookie;
}
public static function remember_logged_in_cookie($cookie) {
$_COOKIE[LOGGED_IN_COOKIE] = $cookie;
}
/**
* 削除を同一リクエストへ反映させる。これが無いと直後の auth_redirect() が
* 古い $_COOKIE で認証を通し、ログアウトが1リクエスト分遅れる。
* wp_destroy_current_session() はこのフックより前に走るため取り違えは起きない。
*/
public static function forget_cookies() {
foreach ([AUTH_COOKIE, SECURE_AUTH_COOKIE, LOGGED_IN_COOKIE] as $name) {
unset($_COOKIE[$name]);
}
}
/**
* WordPress の Cookie 有効期限を JWT の exp までに切り詰める。
*/
public static function auth_cookie_expiration($length, $user_id, $remember) {
$identity = CF_Access_Token::current();
if (!$identity || !$identity->expires_at()) {
return $length;
}
$remaining = $identity->expires_at() - time();
return ($remaining > CF_Access_Token::LEEWAY && $remaining < $length) ? $remaining : $length;
}
/* ---------------------------------------------------------------- */
/* ユーザー解決 */
/* ---------------------------------------------------------------- */
/**
* @return int|false
*/
private static function user_id() {
if (self::$user_id !== null) {
return self::$user_id;
}
$identity = CF_Access_Token::current();
self::$user_id = $identity ? self::resolve_user($identity->email()) : false;
return self::$user_id;
}
/**
* @param string $email
* @return int|false
*/
private static function resolve_user($email) {
$user = get_user_by('email', $email);
if ($user instanceof WP_User) {
return (int) $user->ID;
}
if (!CF_Access_Config::auto_create()) {
CF_Access_Util::log('authenticated but no matching WordPress user');
return false;
}
$login = sanitize_user($email, true);
if ($login === '' || username_exists($login)) {
$login .= '-' . wp_generate_password(4, false, false);
}
$user_id = wp_insert_user([
'user_login' => $login,
'user_email' => $email,
'user_pass' => wp_generate_password(64, true, true),
'role' => CF_Access_Config::default_role(),
]);
if (is_wp_error($user_id)) {
CF_Access_Util::log('user creation failed: ' . $user_id->get_error_code());
return false;
}
return (int) $user_id;
}
/* ---------------------------------------------------------------- */
/* ログアウト */
/* ---------------------------------------------------------------- */
public static function logout_redirect($redirect_to, $requested_redirect_to = '', $user = null) {
$url = CF_Access_Config::logout_url();
return $url ? $url : $redirect_to;
}
/**
* logout_redirect の戻り値は wp_safe_redirect() に渡されるため、
* 別ホスト(IdP のログアウト等)を経由する場合は許可リストへの追加が必須。
*/
public static function allowed_redirect_hosts($hosts) {
$url = CF_Access_Config::logout_url();
if (!$url) {
return $hosts;
}
$host = wp_parse_url($url, PHP_URL_HOST);
if ($host) {
$hosts[] = $host;
}
return $hosts;
}
}
CF_Access_SSO::boot();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment