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

I'm the same here. Apparently, the APIs are still not available.

I was really hoping the session would be more informative, but they simply said "[you need to] create the header containing the fields and values described in the developer documentation", when the developer documentation literally has no mention that this API uses JWTs.

@leemartin reached out to Novall on Twitter to ask when the REST API will be functional, but hasn't received a response yet. Twitter isn't allowed to be linked, but the text of the link is permitted: https://twitter.com/leemartin/status/1534214645128564737

Really excited to get cracking with this, but looks like it's half-baked at the moment.

This video contains some details about WeatherKit Auth starting at 9:05 - https://developer.apple.com/videos/play/wwdc2022/10003/

The example code in the video omitted the "Bearer " prefix from the Authorization header

Seems like there was an update this night. Now I'm getting 401 instead of 500.

Anyone get this working yet?

I'm getting a 500 with a node app

Looks like there is a url issue? If I use the urls in the docs:

https://weatherkit.apple.com/api/v1/weather/en

then I get a 500.

If I use the url as in the video

https://weatherkit.apple.com/1/weather/

then I get a 401

I've opened a feedback request (10140066) with the following:

WeatherKit REST API documentation is missing authentication details

The API documentation for WeatherKit's REST API is missing the authentication details that were mentioned during the session video. Several other engineers have also reported this via the forums, without any official response. To reproduce, visit https://developer.apple.com/documentation/weatherkitrestapi/get_api_v1_weather_language_latitude_longitude and observe the lack of details regarding authentication. I expect to be able to understand how to authenticate with this API from its documentation, but the documentation is missing, however, any reference to authentication is missing. I am not using a version of Xcode to encounter this issue.

Would really appreciate a timeline from Apple for a resolution!

Additionally, @markdaws on Twitter has also followed up to another WeatherKit tweet expressing issue with authentication for this API. Tweet links as code are now banned, so here's a gigantic picture instead:

SUCCESS! Had a labs call with two Apple engineers and they set me on the right path.

This is what your JWT should look like:

var privateKey = fs.readFileSync("YOU KEY FILE FROM DEVELOPER CENTER.p8");
  var token = jwt.sign(
    {
      subject: "APP ID",
    },
    privateKey,
    {
      jwtid: "YOUR TEAM ID.YOUR APP ID",
      issuer: "TEAM ID",
      expiresIn: "1h",
      keyid: "KEY ID",
      algorithm: "ES256",
      header: {
        id: "YOUR TEAM ID.YOUR APP ID",
      },
    }
  );
  // id is teamid.serviceid

  const url =
    "https://weatherkit.apple.com/api/v1/weather/en/51.677612/-2.937941?dataSets=currentWeather&timezone=Europe/London";

  const config = {
    headers: { Authorization: `Bearer ${token}` },
  };

  const { data: weatherData } = await axios.get(url, config);

Getting the id on the header is the trick.

I have made a YouTube video and example app but it won't let me post the link, the youtube video id is: 7mg42_Fix9k

@tanakasan1734 you legend, thank you for staying up till 2am working on this!!

Thank you so much for putting this up as a YouTube video as well, really appreciate it! I've converted your JavaScript example into my native Ruby & included it below.

For those still having issues with this API: of significant importance is setting up an "app" in Certificates, Identifiers & Profiles in addition to setting up the "key", so you have an "app ID" registered for WeatherKit in addition to the key being assigned access to it. I would imagine this has something to do with the upcoming billing for WeatherKit.

Full annotated example from my lab notebook:

# WeatherKit REST API: access example
# With thanks to Simon of All The Code (Twitter: @allthecode_)

require 'openssl' # stdlib
require 'logger' # stdlib
require 'http' # gem install http
require 'jwt' # gem install jwt

## APPLE ID's (mine are examples)
TEAM_ID = "TVVX2ENCS3" # Your Apple Developer "team ID", see https://developer.apple.com/account/#!/membership -> "Team ID"
APP_ID = "net.rubynerd.weathervane" # Create an Identifier -> App ID -> App -> set Bundle ID -> App Services -> check WeatherKit -> Save, use Bundle ID here
KEY_ID = "HC2L4UY9UW" # Create a Key -> set Key Name -> check WeatherKit, use Key ID here
KEY_PATH = "/Users/rubynerd/Downloads/AuthKey_HC2L4UY9UW.p8" # Path to file downloaded from key creation above

## GEO VARS
# Charing Cross, nominal centre of London
LAT = "51.5081"
LONG = "0.1248"
TZ = "Europe/London"
DATASETS = %w{currentWeather}.join(",")

# Read the key path to create an ECDSA key
p8 = OpenSSL::PKey::EC.new(File.read(KEY_PATH))

# Create a JWT for accessing WeatherKit
jwt = JWT.encode({
  iss: TEAM_ID,
  iat: Time.now.to_i,
  exp: Time.now.to_i + 3600,
  sub: APP_ID,
  jti: "#{TEAM_ID}.#{APP_ID}",
}, p8, 'ES256', {
  kid: KEY_ID,
  id: "#{TEAM_ID}.#{APP_ID}",
})

# Use httprb (https://github.com/httprb/http) to make the request to WeatherKit
res = HTTP.use(logging: {
  logger: Logger.new(STDOUT)
}).headers(
  "Authorization" => "Bearer #{jwt}",
).get "https://weatherkit.apple.com/api/v1/weather/en/#{LAT}/#{LONG}?dataSets=#{DATASETS}&timezone=#{TZ}"

# Parse the response body from JSON to a Ruby Hash
body = res.parse

# Print the condition code
puts "conditionCode: #{body["currentWeather"]["conditionCode"]}"
# Print required attribution & link
puts "data provided by Apple WeatherKit (#{body["currentWeather"]["metadata"]["attributionURL"]})"

If you're translating the above from Ruby to another language, I've included an expired JWT you can paste into jwt.io's debugger:

eyJraWQiOiJIQzJMNFVZOVVXIiwiaWQiOiJUVlZYMkVOQ1MzLm5ldC5ydWJ5bmVyZC53ZWF0aGVydmFuZSIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJUVlZYMkVOQ1MzIiwiaWF0IjoxNjU0ODU0NTU5LCJleHAiOjE2NTQ4NTQ1NjQsInN1YiI6Im5ldC5ydWJ5bmVyZC53ZWF0aGVydmFuZSIsImp0aSI6IlRWVlgyRU5DUzMubmV0LnJ1YnluZXJkLndlYXRoZXJ2YW5lIn0.7mGJZvce6D2JmbligmurJc0H4sMa_CwCwHBB4a5yoQvh9n7AljVOpDp7vHblWRG-DPtSqSFzOflM92otKkgQSw

Between the JS and Ruby, this should be enough to close this. I'll leave the feedback request open to push for accurate documentation. Let me know if you have any issues with what's above, or DM me on Twitter if you need further assistance.

Apple have published docs on this topic now and also modified the API so that now your JWT subject has to be sub and not subject as I was able to get away with in my first code example.

I ended up integrating this on a new project already. 😅 https://www.3000degreez.app (Dev blog coming soon)

This setup had been working well for me until yesterday, and now I'm unable to authenticate. Has anyone else experienced this issue as well?

Same here... it worked in the betas, but stopped working now that I want to publish :( Weatherkit is enabled in both Capacities, and in the App Services...

I have decoded the JWT token and ensured it contains the exact same items as the sample token at https://developer.apple.com/documentation/weatherkitrestapi/request_authentication_for_weatherkit_rest_api

{
    "alg": "ES256",
    "kid": "3J4F34**24",
    "id": "KC697SNQ2X.com.caramba.instaweather"
}
{
    "iss": "KC697SNQ2X",
    "iat": 1437179036,
    "exp": 1493298100,
    "sub": "com.caramba.instaweather"
}

response is

{"timestamp":"2022-09-16T18:17:15Z","status":403,"error":"Forbidden","message":"Access Denied","path":"/api/v1/availability/37.323/122.032"}   

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