165 lines
4.4 KiB
PHP
165 lines
4.4 KiB
PHP
<?php
|
|
/**
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Auth;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
final class OAuthProxyProtocol {
|
|
|
|
public const VERSION = '2';
|
|
public const MAX_CLOCK_SKEW = 300;
|
|
public const REPLAY_TTL = 660;
|
|
|
|
private const REQUEST_DIRECTION = 'request';
|
|
private const CALLBACK_DIRECTION = 'callback';
|
|
|
|
private const REQUEST_FIELDS = [
|
|
'v',
|
|
'request_id',
|
|
'provider',
|
|
'action',
|
|
'agent_back_url',
|
|
'ts',
|
|
];
|
|
|
|
private const CALLBACK_FIELDS = [
|
|
'v',
|
|
'request_id',
|
|
'jti',
|
|
'provider',
|
|
'action',
|
|
'issuer',
|
|
'provider_id',
|
|
'username',
|
|
'name',
|
|
'email',
|
|
'email_verified',
|
|
'avatar_url',
|
|
'url',
|
|
'bio',
|
|
'locale',
|
|
'error',
|
|
'ts',
|
|
];
|
|
|
|
public static function request_fields(array $source): array {
|
|
return self::fields($source, self::REQUEST_FIELDS);
|
|
}
|
|
|
|
public static function callback_fields(array $source): array {
|
|
return self::fields($source, self::CALLBACK_FIELDS);
|
|
}
|
|
|
|
public static function sign_request(array $fields, string $secret): string {
|
|
return self::sign(self::request_fields($fields), $secret, self::REQUEST_DIRECTION);
|
|
}
|
|
|
|
public static function verify_request(array $fields, string $secret, string $signature): bool {
|
|
return self::verify(self::request_fields($fields), $secret, self::REQUEST_DIRECTION, $signature);
|
|
}
|
|
|
|
public static function sign_callback(array $fields, string $secret): string {
|
|
return self::sign(self::callback_fields($fields), $secret, self::CALLBACK_DIRECTION);
|
|
}
|
|
|
|
public static function verify_callback(array $fields, string $secret, string $signature): bool {
|
|
return self::verify(self::callback_fields($fields), $secret, self::CALLBACK_DIRECTION, $signature);
|
|
}
|
|
|
|
public static function timestamp_valid(string $timestamp): bool {
|
|
if (preg_match('/^[0-9]{10}$/', $timestamp) !== 1) {
|
|
return false;
|
|
}
|
|
return abs(time() - (int) $timestamp) <= self::MAX_CLOCK_SKEW;
|
|
}
|
|
|
|
public static function token_valid(string $token): bool {
|
|
return preg_match('/^[A-Za-z0-9_-]{32,128}$/', $token) === 1;
|
|
}
|
|
|
|
public static function callback_url(string $url): string {
|
|
$url = OAuthUrlPolicy::public_https($url);
|
|
if ($url === '') {
|
|
return '';
|
|
}
|
|
$parts = wp_parse_url($url);
|
|
$path = rtrim((string) ($parts['path'] ?? ''), '/');
|
|
if (!is_array($parts) || !str_ends_with($path, '/oauth/proxy/callback') || isset($parts['query'])) {
|
|
return '';
|
|
}
|
|
$authority = self::authority($parts);
|
|
return $authority !== '' ? 'https://' . $authority . $path . '/' : '';
|
|
}
|
|
|
|
public static function server_base_url(string $url): string {
|
|
$url = OAuthUrlPolicy::public_https($url);
|
|
if ($url === '') {
|
|
return '';
|
|
}
|
|
$parts = wp_parse_url($url);
|
|
if (!is_array($parts) || isset($parts['query'])) {
|
|
return '';
|
|
}
|
|
$authority = self::authority($parts);
|
|
if ($authority === '') {
|
|
return '';
|
|
}
|
|
$path = rtrim((string) ($parts['path'] ?? ''), '/');
|
|
return 'https://' . $authority . $path;
|
|
}
|
|
|
|
public static function issuer(string $url): string {
|
|
$url = OAuthUrlPolicy::public_https($url);
|
|
$parts = $url !== '' ? wp_parse_url($url) : false;
|
|
if (!is_array($parts)) {
|
|
return '';
|
|
}
|
|
$authority = self::authority($parts);
|
|
return $authority !== '' ? 'https://' . $authority : '';
|
|
}
|
|
|
|
private static function authority(array $parts): string {
|
|
$host = strtolower((string) ($parts['host'] ?? ''));
|
|
if ($host === '') {
|
|
return '';
|
|
}
|
|
if (str_contains($host, ':') && $host[0] !== '[') {
|
|
$host = '[' . $host . ']';
|
|
}
|
|
$port = isset($parts['port']) ? ':' . (int) $parts['port'] : '';
|
|
return $host . $port;
|
|
}
|
|
|
|
private static function fields(array $source, array $names): array {
|
|
$fields = [];
|
|
foreach ($names as $name) {
|
|
$value = $source[$name] ?? '';
|
|
if (!is_scalar($value) && $value !== null) {
|
|
$value = '';
|
|
}
|
|
$fields[$name] = (string) wp_unslash((string) $value);
|
|
}
|
|
return $fields;
|
|
}
|
|
|
|
private static function sign(array $fields, string $secret, string $direction): string {
|
|
if ($secret === '') {
|
|
return '';
|
|
}
|
|
ksort($fields, SORT_STRING);
|
|
$payload = http_build_query($fields, '', '&', PHP_QUERY_RFC3986);
|
|
$key = hash_hmac('sha256', 'aksrt-oauth-proxy-v2/' . $direction, $secret, true);
|
|
return hash_hmac('sha256', $payload, $key);
|
|
}
|
|
|
|
private static function verify(array $fields, string $secret, string $direction, string $signature): bool {
|
|
if ($secret === '' || preg_match('/^[a-f0-9]{64}$/i', $signature) !== 1) {
|
|
return false;
|
|
}
|
|
return hash_equals(self::sign($fields, $secret, $direction), strtolower($signature));
|
|
}
|
|
}
|