Internet Fridge: The Outside World

In Part I of this series, I stuck a cheap Android tablet to my fridge with fridge magnets. In Part II, I talked a little bit about Blazor and what makes a progressive web application. In Part III, I’m going to add an API that will let the fridge talk to the Internet safely.

The Internet Fridge is all about living in the future. The future comes in different sizes. Kitchen computers have been the dream for fifty years, but when I get up in the morning, the future I’m interested in is, “Is it going to rain today?”

This is a big step up from the Honeywell kitchen computer that inspired this project. It didn’t have an internet connection, mainly because the internet only had four nodes in 1969. None of them were fridges.

The Blazor sample app we started with comes with a tiny nonsense weather page. We can improve on that with some real data from https://openweathermap.org/

There is a bit of a wrinkle here. Because the Blazor app runs in the web browser, it’s not safe to store credentials in the app. The Open Weather API requires an app ID, which I don’t want to be visible on the client, so I’m going to create a second project to run on the server.

The Internet Fridge application won’t talk to the Open Weather API. It will talk to my server layer, which will talk to Open Weather. So we need to think about two security-related things – application secrets and CORS (cross-origin request sharing).

But first, let’s model the data we get back from the Open Weather API.

Quick and Dirty Models

Here’s a quick and dirty way to do 80% of the model-building work.

Copy the example response from https://openweathermap.org/weather-data and then paste the JSON as classes in Visual Studio.

If you copy the example JSON from that page and then go to an empty class file in a Visual Studio project you can go to the Edit menu, go down to Paste Special, and select ‘Paste JSON As Classes’. Visual Studio will look at the JSON on the clipboard and create classes that match the structure. There’ll be a root object and each nested object will be created as a new class. If you want to move each class to its own file, you can do that from the Quick Actions menu (Ctrl+.).

This will work fine as is or you can rename properties to make their purpose clearer. Decorate renamed properties with the JsonPropertyName attribute so that the JSON deserialiser knows what to do. In some cases you may need to alter the types as well.

Here’s what the Paste Special action generated:

    public class Coord
    {
        public float lat { get; set; }
        public float lon { get; set; }
    }

And here’s what I changed it to:

    public class Coordinates
    {
        [JsonPropertyName("lat")]
        public decimal Latitude { get; set; }
        [JsonPropertyName("lon")]
        public decimal Longitude { get; set; }

    }

Getting the Current Weather

The core of this component is the OpenWeatherService class. It does two things – gets the current weather and gets a forecast from the Open Weather API. I’m going to display both in my app. In both cases the request needs a city ID and an app ID, a GUID API key unique to you.

You can get your favourite location’s city ID in the Find page and apply for your API key on the How to Start page. This demo will only use data available to free accounts.

This is the service’s interface:

    public interface IOpenWeatherService
    {
        Task<CurrentWeather> GetCurrentWeatherAsync(int cityId, Guid appId);
        Task<Forecast> GetForecastAsync(int cityId, Guid appId);
    }

The service’s constructor takes an HttpClient and sets its base address.

        private readonly HttpClient _client;
        private const string OpenWeatherApiBaseUrl = "https://api.openweathermap.org/data/2.5/";

        public OpenWeatherService(HttpClient client)
        {
            client.BaseAddress = new Uri(OpenWeatherApiBaseUrl);
            _client = client;
        }

The methods to get the data are simple thanks to the new GetFromJsonAsync extension method.

        public async Task<CurrentWeather> GetCurrentWeatherAsync(int cityId, Guid appId)
        {
            var url = $"weather?id={cityId}&units=metric&appid={appId:N}";
            return await _client.GetFromJsonAsync<CurrentWeather>(url);
        }

Install the System.Net.Http.Json package and you get this all-in-one extension method that will make the HTTP request, parse the JSON response, and deserialise it into a C# object.

Get the Code

The models and the service class are available as source code or as a NuGet package.

GitHub: https://github.com/darnton/OpenWeather

NuGet: https://www.nuget.org/packages/Darnton.OpenWeather/

Proxy Architecture

I’m going to create an API proxy that will make the calls to the Open Weather API without the client application needing to know the secret API key. It’ll be a .NET web API project  and will

  • have two controllers, for current weather and weather forecast
  • use the OpenWeather NuGet package discussed above
  • have a way of getting the API key
  • make itself available to the client app with its cross-origin policy

I’ve created a project called InternetFridge.API in the same solution as the Blazor WebAssembly project I created in the last post.

InfoSec 101: Not Punching Yourself in the Face

The Open Weather API key should be kept secret. It shouldn’t be known to the client application because then anyone or anything with access to the browser’s JavaScript sandbox could see it.

It also mustn’t be saved in the source code because then it will end up on GitHub. If you take this one simple step of not punching yourself in the face, you’ll have better security than half the US Government. The 2020 SolarWinds breach, where Russian government hackers got into the US Treasury and Department of Homeland Security, got a head start when a SolarWinds employee saved the password for the company’s FTP server in his personal GitHub account.

For development purposes, I’m going to use the ASP.NET Secret Manager. This is not terribly secure as it stores secrets in an unencrypted JSON file, but they’re in your user profile rather than the project folder, so nothing sensitive will end up on GitHub.

To enable Secret Manager for your project, open the command line in your project directory and run

dotnet user-secrets init

This will add a user secrets ID to your project and create a secrets.json file in your user profile, under the same ID.

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>6b1cd195-9bd0-47bc-8f38-603405ae6f05</UserSecretsId>
  </PropertyGroup>

To add a secret to Secret Manager, copy the API key you got from Open Weather and run

dotnet user-secrets set “OpenWeather:ApiKey” “{your-api-key}”

If you have a look in your user profile, you should see the secrets file and you can open it up to check that it’s right. Like I said, unencrypted. Anyone with access to your machine will be able to see the keys, but you’ll be safe from sophisticated state-level hackers copying stuff off GitHub.

When an ASP.NET Core application starts up, secrets are read into the application configuration. All we need to do is inject an IConfiguration into a controller’s constructors and we can see the keys we need.

        public CurrentWeatherController(IOpenWeatherService openWeatherService, IConfiguration config, ILogger<CurrentWeatherController> logger)
        {
            _openWeatherService = openWeatherService;
            _config = config;
            _logger = logger;
        }
        ...
        [HttpGet]
        public async Task<CurrentWeather> Get(int cityId)
        {
            return await _openWeatherService.GetCurrentWeatherAsync(cityId, new Guid(_config["OpenWeather:ApiKey"]));
        }

The key is stored as a string and the OpenWeatherService is expecting a GUID, but otherwise it’s completely straightforward.

I’ll talk about what to do with the keys in production when we get to deployment, in a later piece.

Cross-origin Requests

CORS (cross-origin request sharing) is one of the fishhooks in developing Blazor applications. Requests to third-party APIs that work fine in a server-side app may not work in client-side app because web browsers enforce a “same-origin policy”. I’ve got more detail on CORS in my post How to Build a CORS Proxy for Client-side Blazor.

In production you might serve your client app and have the API endpoints on the same origin (protocol, host, and port). In that case, CORS won’t be needed.

In development, it’s easiest to set up your solution to run multiple startup projects. They’ll be served from two different ports on localhost. A different port means a different origin as far as the browser is concerned and the client won’t be able to talk to the API without CORS.

Setting up CORS in ASP.NET Core is pretty simple. You need to add a CORS policy and configure the request pipeline to use it.

Add the policy in Startup’s ConfigureServices method.

        services.AddCors(options => 
            options.AddPolicy("localDev", builder => 
                builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()
        ));

I’ve created a policy called “localDev” because I only intend to use it in development. If you’re going to use it in production, limit the allowed origins and headers to just what’s needed.

Add CORS to the request pipeline in Startup’s Configure method. I’m only adding it in development.

        if (env.IsDevelopment())
        {
            app.UseCors("localDev");
        }

Middleware order is important. The CORS setup must come after routing and before authorisation.

With the OpenWeather library imported, the user secrets kept safe, and cross-origin requests allowed, everything is in place for the server project.

In the next part, I’ll call the API and consume the weather data on the client.

Get the (Other) Code

As well as the OpenWeather package, you can get the Internet Fridge code on GitHub.

GitHub: https://github.com/darnton/InternetFridge

One thought on “Internet Fridge: The Outside World

  1. Thanks a lot for sharing this ! This is exactly what I’ve been looking for in order to resolve same-origin policy in my Blazor app. Now I managed to implement most my data-retrieval methods in purely client-side WASM app, without resorting to having special controllers and services in WebAPI project just in order to circumvent the CORS issue.

    I have an issue with WebDav requests though – using a WebDav client (https://github.com/stefh/WebDAV-Client) in order to retrieve files from a remote server. Can’t figure out a way to specify the route from WASM client in a way that WebAPI proxy rule picks that request up.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s