Add M365 Login plugin: Microsoft Entra ID sign-in for existing users
Adds a WordPress plugin that places a customisable "Sign in with Microsoft" button on wp-login.php and signs existing users in via the OpenID Connect authorization code flow with PKCE. Users are matched by e-mail address only; no accounts are created. Security: single-use state/nonce bound to an HttpOnly cookie, ID token signature verification against Microsoft's JWKS (RS256 only) with issuer/audience/tenant/expiry/nonce checks, optional tenant pinning, account binding to the Microsoft object ID, e-mail domain allow-list, client secret encrypted at rest (AES-256-GCM). Admin: settings screen with connection, button and security tabs, live button preview, colour presets, media-library icon picker, redirect URI copy button and tenant connectivity test. Packaging for WordPress.org: readme.txt with External services section, GPL-2.0 license, uninstall.php, POT + German translations, .distignore, build script, PHPCS config and CI running Plugin Check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJxAHYdMfKPoN4koRc4Ci2
This commit is contained in:
commit
3e3e87b399
35 changed files with 5413 additions and 0 deletions
85
includes/class-m365-login-crypto.php
Normal file
85
includes/class-m365-login-crypto.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
/**
|
||||
* Symmetric encryption for secrets at rest.
|
||||
*
|
||||
* @package M365_Login
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* AES-256-GCM helper keyed from the WordPress salts.
|
||||
*
|
||||
* The key is derived from AUTH_KEY / SECURE_AUTH_KEY (via wp_salt()), so the
|
||||
* stored client secret is useless without access to wp-config.php.
|
||||
*/
|
||||
final class M365_Login_Crypto {
|
||||
|
||||
const PREFIX = 'm365v1:';
|
||||
const CIPHER = 'aes-256-gcm';
|
||||
|
||||
/**
|
||||
* Derives the encryption key.
|
||||
*
|
||||
* @return string 32 raw bytes.
|
||||
*/
|
||||
private static function key() {
|
||||
$material = wp_salt( 'auth' ) . '|' . wp_salt( 'secure_auth' ) . '|m365-login';
|
||||
if ( function_exists( 'hash_hkdf' ) ) {
|
||||
return hash_hkdf( 'sha256', $material, 32, 'm365-login-client-secret' );
|
||||
}
|
||||
return hash( 'sha256', $material, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether encryption is available.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function available() {
|
||||
return function_exists( 'openssl_encrypt' ) && in_array( self::CIPHER, openssl_get_cipher_methods(), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a string.
|
||||
*
|
||||
* @param string $plain Plain text.
|
||||
* @return string|false
|
||||
*/
|
||||
public static function encrypt( $plain ) {
|
||||
if ( ! self::available() ) {
|
||||
return false;
|
||||
}
|
||||
$iv = random_bytes( 12 );
|
||||
$tag = '';
|
||||
$ct = openssl_encrypt( $plain, self::CIPHER, self::key(), OPENSSL_RAW_DATA, $iv, $tag, '', 16 );
|
||||
if ( false === $ct || '' === $tag ) {
|
||||
return false;
|
||||
}
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
return self::PREFIX . base64_encode( $iv . $tag . $ct );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a string produced by encrypt().
|
||||
*
|
||||
* @param string $stored Stored value.
|
||||
* @return string|false
|
||||
*/
|
||||
public static function decrypt( $stored ) {
|
||||
if ( ! is_string( $stored ) || 0 !== strpos( $stored, self::PREFIX ) || ! self::available() ) {
|
||||
return false;
|
||||
}
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$raw = base64_decode( substr( $stored, strlen( self::PREFIX ) ), true );
|
||||
if ( false === $raw || strlen( $raw ) < 28 ) {
|
||||
return false;
|
||||
}
|
||||
$iv = substr( $raw, 0, 12 );
|
||||
$tag = substr( $raw, 12, 16 );
|
||||
$ct = substr( $raw, 28 );
|
||||
|
||||
$plain = openssl_decrypt( $ct, self::CIPHER, self::key(), OPENSSL_RAW_DATA, $iv, $tag );
|
||||
return false === $plain ? false : $plain;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue