'invalid_request', 'error_description' => 'POST required.']); exit(); } function respond_with_error(string $error, string $description, int $status = 400): void { http_response_code($status); echo json_encode(['error' => $error, 'error_description' => $description], JSON_UNESCAPED_SLASHES); exit(); } function extract_client_credentials(): array { $clientId = null; $clientSecret = null; if (!empty($_SERVER['HTTP_AUTHORIZATION']) && stripos($_SERVER['HTTP_AUTHORIZATION'], 'basic ') === 0) { $encoded = substr($_SERVER['HTTP_AUTHORIZATION'], 6); $decoded = base64_decode($encoded, true); if ($decoded !== false && strpos($decoded, ':') !== false) { [$clientId, $clientSecret] = explode(':', $decoded, 2); } } if ($clientId === null && isset($_POST['client_id'])) { $clientId = $_POST['client_id']; $clientSecret = $_POST['client_secret'] ?? ''; } return [trim((string) $clientId), (string) $clientSecret]; } [$clientId, $clientSecret] = extract_client_credentials(); $grantType = $_POST['grant_type'] ?? ''; if ($clientId === '' || $grantType === '') { respond_with_error('invalid_request', 'client_id and grant_type are required.'); } $oauthService = new OAuthService($pdo); $client = $oauthService->getClient($clientId); if (!$client) { respond_with_error('unauthorized_client', 'Unknown or revoked client.', 401); } $requiresSecret = (int) $client['is_confidential'] === 1; if ($requiresSecret && $clientSecret === '') { respond_with_error('invalid_client', 'Client credentials required.', 401); } if ($requiresSecret && !$oauthService->verifyClientSecret($client, $clientSecret)) { respond_with_error('invalid_client', 'Client authentication failed.', 401); } switch ($grantType) { case 'authorization_code': $code = $_POST['code'] ?? ''; $redirectUri = $_POST['redirect_uri'] ?? ''; if ($code === '') { respond_with_error('invalid_request', 'Authorization code is required.'); } $authRecord = $oauthService->consumeAuthorizationCode($code, $clientId); if (!$authRecord) { respond_with_error('invalid_grant', 'Authorization code is invalid or expired.'); } if ($redirectUri !== '' && !hash_equals($authRecord['redirect_uri'], $redirectUri)) { respond_with_error('invalid_grant', 'redirect_uri mismatch.'); } $tokens = $oauthService->issueTokens( $clientId, (int) $authRecord['person_id'], $authRecord['scope'] ?? null, true ); break; case 'refresh_token': $refreshToken = $_POST['refresh_token'] ?? ''; if ($refreshToken === '') { respond_with_error('invalid_request', 'refresh_token is required.'); } $tokens = $oauthService->exchangeRefreshToken($clientId, $refreshToken); if (!$tokens) { respond_with_error('invalid_grant', 'Refresh token is invalid or expired.'); } break; default: respond_with_error('unsupported_grant_type', 'The grant_type is not supported.'); } $response = [ 'access_token' => $tokens['access_token'], 'token_type' => $tokens['token_type'], 'expires_in' => max(0, $tokens['access_expires_at'] - time()), ]; if (!empty($tokens['scope'])) { $response['scope'] = $tokens['scope']; } if (!empty($tokens['refresh_token'])) { $response['refresh_token'] = $tokens['refresh_token']; if (!empty($tokens['refresh_expires_at'])) { $response['refresh_expires_in'] = max(0, $tokens['refresh_expires_at'] - time()); } } echo json_encode($response, JSON_UNESCAPED_SLASHES);