WeatherKit REST API Authentication

Hi there! Please could you publish some of the specifics for authentication for the new WeatherKit REST API? I am attempting to use something cobbled together from MapKit, and unfortunately WeatherKit returns a 500 error in response.

Broken code sample below. If you find a working solution or can identify an error, please comment with the solution. Thank you!

require 'http'
require 'jwt'
require 'openssl'
require 'logger'

p8 = OpenSSL::PKey::EC.new(File.read("/PATH/TO/DOWNLOADED/P8/FILE"))

token = JWT.encode({
  iss: 'KEY_NAME_FROM_CIP',
  iat: Time.now.to_i,
  exp: Time.now.to_i + 3600,
  aud: 'weatherkit',
}, p8, 'ES256', {
  kid: 'KEY_ID_FROM_CIP',
  typ: 'JWT'
})

res = HTTP.use(logging: {
  logger: Logger.new(STDOUT)
}).headers(
  "Authorization" => "Bearer #{token}"
).get "https://weatherkit.apple.com/api/v1/weather/en/LONG/LAT?dataSets=currentWeather&timezone=Europe/London"

puts "code: #{res.code}"
puts res.body

For those of you who have this working on your end - how did you set up your key and ID in the Apple Developer account? I'm noticing a conflict between the documentation here and the information in this thread. Essentially, in this thread, the instruction is to create a key and an "App ID" - but in the Apple documentation for the WeatherKit API the instruction is to create a key and a "Service ID" - also there seems to be no consensus about whether, when creating an App ID, we should check the WeatherKit box for either Capabilities or App Services, or both.

My JWT is following the examples here which follow the Apple documentation. When I make the request, {"reason": "NOT_ENABLED"} is returned. The only thing I can figure is that somehow my Key and or ID are not set up properly to enable the service - but I've followed the documentation so now I'm stuck.

I'm trying to use the REST API from my dev box to no avail. I keep getting a 403 error (which is funny because the docs say possible responses are 200, 400 and 401).

I've tried setting up an App ID checking WeatherKit under both Capabilities and App Services, and also setting up a Service ID.

I've also created 2 different keys with WeatherKit access but neither works.

I'm using PHP 7 with Firebase\JWT\JWT.

Maybe this only works when called from the actual domain we set as Service ID?

{
  timestamp: "2023-01-17T19:25:37Z",
  status: 403,
  error:  "Forbidden",
  message: "Access Denied",
  path: "/api/v1/availability/-34.60/-58.60"
}

if anyone still need PHP implementation, here is a working code:

$getPrivateKey = openssl_pkey_get_private(file_get_contents('../config/weather_key.pem'));
openssl_pkey_export($getPrivateKey, $privateKey);

$date   = new DateTimeImmutable();
$expire_at     = $date->modify('+3 minutes')->getTimestamp();     
$teamID = "*******";
$encryptionType = 'ES256';
$keyID = '*****'; //key identifier
$serviceID   = "net.Yourysite.myWeather";  
$request_data = [
    'iat'  => $date->getTimestamp(),       
    'iss'  => $teamID,                  
    'exp'  => $expire_at,               
    'sub' => $serviceID,                
];

$mytoken = JWT::encode(
    $request_data,
    $privateKey,
    $encryptionType,
    $keyID
);

// use this valid $mytoken in curl function;

I'm stumped. I had everything working Sunday night, pulling weather data from my dev and staging environments. Went to sleep, and now I can't get data in my dev or staging environments. The response received is 401 Unauthorized, { reason: "NOT_ENABLED" }. After attempting several variations with no success, I decided to start over: remake the AppID, ServiceID, and Key (I believe the AppID isn't required).

Here is my current code:

import { sign } from "jsonwebtoken"
import { readFileSync } from "fs"

const generateJWT = async () => {
     const secret = readFileSync("./AuthKey_**********.p8")
     const signedToken = await signToken(secret)
     console.log("Signed Token = ", signedToken) /* Logs token correctly, but the logged token doesn't work in dev or in Postman requests */
     return signedToken
}

const signToken = (secret: Buffer) => {
     return new Promise((resolve, reject) => {
          sign(
               {
                    sub: [ServicesID-Identifier], 
               },
                    secret,
               {    
                    jwtid: [TeamID.ServicesID-Identifier],
                    issuer: [TeamID],
                    expiresIn: "1h",
                    keyid: [KeyID],
                    algorithm: "ES256",
                    header: {
                         id: [TeamID.ServiceID-Identifier],
                    },
               },
               function (error, token) {
                    error ? reject(error) : resolve(token)
               }
          )
     })
}

export default generateJWT
  • All bracketed information is the respective string value.

Fetch code, slightly abbreviated:

const getLocationWeather = async (location) => {
     const token = await generateJWT()

     const fetchURL = `https://weatherkit.apple.com/api/v1/weather/en-US/${location.lat}/${location.lon}?dataSets=forecastHourly`

     const res = await fetch(fetchUrl, {
          headers: {
               Authorization: `Bearer ${token}`
          }
     })

console.log("res = ", res.status) /* logs 401 */
const data = await res.json()
console.log("data = ", data) /* logs { reason: "NOT_ENABLED"}
}

Hitting the /availability end-point using Postman and the token's logged above results in the same 401 / NOT_ENABLED.

Outstanding questions requiring confirmation/clarification:

  1. Is ServiceID the correct ID to use, and NOT AppID?
  2. Since this implementation uses 'jsonwebtoken' library, there is no need to convert the .p8 private key into .pem, correct?
  3. I'm new to working with json web tokens. Using jwt.io to debug the generated tokens, I'm uncertain as to what to use for the Public Key box under "verify signature" once ES256 is selected from the drop-down for "Algorithm." This may be the key to troubleshooting why my tokens are failing to authenticate.

Future considerations: Reading from .p8 is the simplest way to confirm everything is working. Once this is working again, I would like to convert the .p8 private key into an environment variable. This was working in dev/staging by converting the private key into a string, then reading the string as a buffer into the jsonwebtoken.sign() secret value. I'm wondering if this has any implications for why it worked temporarily, but now doesn't.

Is there a public key to utilize at jwt.io in order to sign my token with my WeatherKit private key (so that I can then test it in Postman agnostic of any other code - since one cannot avoid 'Invalid Signature' after otherwise using their private key on jwt.io in attempt to sign it)?

WeatherKit REST API Authentication
 
 
Q