Skip to content

Commit

Permalink
Merge pull request #3 from Rich2k/p8-support
Browse files Browse the repository at this point in the history
Allow dynamic generation of JWT tokens from p8 file
  • Loading branch information
Rich2k committed Dec 14, 2022
2 parents 9c98b16 + cb21a02 commit 7281b6d
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 19 deletions.
91 changes: 76 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,73 @@ To register a facade accessor, add the following to `config/app.php` `aliases` a

```php
'aliases' => [
'WeatherKit' => \Rich2k\LaravelWeatherKit\Facades\WeatherKit::class,
'WeatherKit' => Rich2k\LaravelWeatherKit\Facades\WeatherKit::class,
]
```

### Configuration

See [Authentication](#Authentication) section on how to use these environment variables.

| Variable name | Default | Description |
|----------------------------|---------------------------------|---------------------------------|
| `WEATHERKIT_AUTH_TYPE` | `jwt` | `jwt` or `p8` token generation |
|----------------------------|---------------------------------|---------------------------------|
| `WEATHERKIT_JWT_TOKEN` | | A pre-generated JWT token. |
|----------------------------|---------------------------------|---------------------------------|
| `WEATHERKIT_KEY_PATH` | | Path to the `.p8` key file |
| `WEATHERKIT_KEY_ID` | | Key ID for you `.p8` file |
| `WEATHERKIT_TEAM_ID` | | Your Apple Team ID |
| `WEATHERKIT_BUNDLE_ID` | | Bundle ID of your App |
| `WEATHERKIT_TOKEN_TTL` | `3600` | Expiry time of token in seconds |
|----------------------------|---------------------------------|---------------------------------|
| `WEATHERKIT_LANGUAGE_CODE` | `config('app.locale', 'en')` | Language code |
| `WEATHERKIT_TIMEZONE` | `config('app.timezone', 'UTC')` | Timezone for timestamps |

If you wish to change the default configuration, you can publish the configuration file to your project.

```bash
$ php artisan vendor:publish --provider=\Rich2k\LaravelWeatherKit\Providers\LaravelServiceProvider
```

## Auth Keys
## Authentication

There are two ways to authenticate with WeatherKit using this library. You'll need to generate the key file first for whichever method you choose.

### Generate Key File

If you wish to generate and manage your own JWT Token yourself then you'll need to first generate a JWT token to access WeatherKit APIs.

You'll need to be enrolled in the paid Apple Developer Program, and register a new App ID and create a key.

#### Create new App ID

Create an App Identifier on the [Identifiers](https://developer.apple.com/account/resources/identifiers/list) section of your account. Enter a short description and give your app a unique bundle ID (e.g. com.myapp.weather).

Make sure you check the WeatherKit option under *BOTH* the Capabilities and App Services tabs. Click on Continue.

#### Create a Key

Go to the [Keys](https://developer.apple.com/account/resources/authkeys/list) page in your developer account.

Give the key a name, e.g. WeatherKit, and make sure to enable WeatherKit. Then click the Continue button. Then you'll be taken to a page with a Register button.

You'll need to first generate a JWT token to access WeatherKit APIs.
Remember to download the key file you get at the end!

* Setup an App Identifier on the [Identifiers](https://developer.apple.com/account/resources/identifiers/list) page of your Apple paid developer account.
* Create a new `App ID` of type App, give it a `Bundle ID` in reverse-domain name style, so com.myapp.weather or similar, and then make sure you select WeatherKit from the App Services tab. This App Identifier can take about 30 minutes to propagate through Apple's systems.
* Go to the [Keys](https://developer.apple.com/account/resources/authkeys/list) page in your developer account and add a new key with WeatherKit selected. Remember to download the key file you get at the end!
#### Required Information

Now we need to generate your JWT token and public/private keys
Whichever authentication method you decide to use, we are going to need some additional information first.

First create your private key in a PEM format using `openssl`
* You Apple Team ID
* The App Bundle ID that you created earlier (reverse DNS).
* The Key ID of the key, that you created in the Create new key section, you can get this at any point after generation.
* The physical key file ending in `.p8` you downloaded.

### Manual JWT Token Generation

Once you've generated and downloaded your `.p8` key file above, we now need to generate your JWT token and public/private keys

Create your private key in a PEM format using `openssl`

`openssl pkcs8 -nocrypt -in AuthKey_ABC1235XS.p8 -out AuthKey_ABC1235XS.pem`

Expand Down Expand Up @@ -98,23 +142,40 @@ E.g.
"id": "DEV1234567.com.myapp.weather"
},
{
"iss": "DEV1234567",
"iat": 1670851291,
"exp": 1702385664,
"sub": "com.myapp.weather"
"iss": "DEV1234567",
"iat": 1670851291,
"exp": 1702385664,
"sub": "com.myapp.weather"
}
```

Copy and paste your private and public key into the signature verification, and the output is what you need to add to your configuration.
Copy and paste your private and public key into the signature verification, and the output is what you need to add to your configuration `WEATHERKIT_JWT_TOKEN`.

## Configuration
#### Configuration

Add the following line to the .env file:
Add the following lines to the .env file:

```sh
WEATHERKIT_AUTH_TYPE=jwt
WEATHERKIT_JWT_TOKEN=<your_weatherkit_jwt_token>
```

### Dynamic Token Generation

Starting with library version `>=1.2` you can dynamically generate your JWT token direct

#### Configuration

Add the following lines to the .env file:

```sh
WEATHERKIT_AUTH_TYPE=p8
WEATHERKIT_KEY_PATH=<Path To Key File>
WEATHERKIT_KEY_ID=<Key Id>
WEATHERKIT_TEAM_ID=<Team Id>
WEATHERKIT_BUNDLE_ID=<Bundle ID>
```


## Usage
For full details of response formats, visit: https://developer.apple.com/documentation/weatherkitrestapi/get_api_v1_weather_language_latitude_longitude
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@
],
"require": {
"php": ">=7.4.0",
"ext-json": "*",
"ext-openssl": "*",
"firebase/php-jwt": "^6.3",
"guzzlehttp/guzzle": "~7.0",
"illuminate/support": "~7.0|~8.0|~9.0",
"nesbot/carbon": "~1.0|~2.0",
"ext-json": "*"
"nesbot/carbon": "~1.0|~2.0"
},
"require-dev": {
"phpunit/phpunit" : "4.*"
Expand Down
12 changes: 11 additions & 1 deletion config/weatherkit.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@
'auth' => [
'config' => [
'jwt' => env('WEATHERKIT_JWT_TOKEN', ''),

'pathToKeyFile' => base_path(env('WEATHERKIT_KEY_PATH', '')),
'keyId' => env('WEATHERKIT_KEY_ID', ''),
'teamId' => env('WEATHERKIT_TEAM_ID'),
'bundleId' => env('WEATHERKIT_BUNDLE_ID'),
'tokenTTL' => env('WEATHERKIT_TOKEN_TTL', 3600)
],

'type' => 'jwt',
/**
* Can be either 'jwt' to use a pre-generated JWT Token
* or 'p8' to use your downloaded p8 file from Apple to dynamically generate a JWT Token at runtime
*/
'type' => env('WEATHERKIT_AUTH_TYPE', \Rich2k\LaravelWeatherKit\WeatherKit::AUTH_TYPE_TOKEN),
],

'languageCode' => env('WEATHERKIT_LANGUAGE_CODE', config('app.locale', 'en')),
Expand Down
11 changes: 11 additions & 0 deletions src/Exceptions/KeyDecodingException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
namespace Rich2k\LaravelWeatherKit\Exceptions;

/**
* KeyDecodingException.
*
* @package Rich2k\LaravelWeatherKit\Exceptions
*/
class KeyDecodingException extends LaravelWeatherKitException
{
}
11 changes: 11 additions & 0 deletions src/Exceptions/KeyFileMissingException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
namespace Rich2k\LaravelWeatherKit\Exceptions;

/**
* KeyNotFoundException.
*
* @package Rich2k\LaravelWeatherKit\Exceptions
*/
class KeyFileMissingException extends LaravelWeatherKitException
{
}
11 changes: 11 additions & 0 deletions src/Exceptions/TokenGenerationFailedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
namespace Rich2k\LaravelWeatherKit\Exceptions;

/**
* TokenGenerationFailedException.
*
* @package Rich2k\LaravelWeatherKit\Exceptions
*/
class TokenGenerationFailedException extends LaravelWeatherKitException
{
}
84 changes: 84 additions & 0 deletions src/JWTToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
namespace Rich2k\LaravelWeatherKit;

use Firebase\JWT\JWT;
use Rich2k\LaravelWeatherKit\Exceptions\KeyDecodingException;
use Rich2k\LaravelWeatherKit\Exceptions\KeyFileMissingException;
use Rich2k\LaravelWeatherKit\Exceptions\TokenGenerationFailedException;
use Throwable;

/**
* JWTToken
*
* @package Rich2k\LaravelWeatherKit
*/
class JWTToken
{
/**
* Generated JWT token
*
* @var string
*/
protected string $jwtToken;

/**
* @param string $p8KeyPath
* @param string $keyId
* @param string $teamId
* @param string $appBundleId
* @param int $tokenTTL
*/
public function __construct(string $p8KeyPath, string $keyId, string $teamId, string $bundleId, int $tokenTTL)
{
if (! file_exists($p8KeyPath)) {
throw new KeyFileMissingException('Cannot find key in path ' . $p8KeyPath);
}

$decodedKey = $this->decodeKey($p8KeyPath);

try {
$this->token = JWT::encode([
'iss' => $teamId,
'sub' => $bundleId,
'iat' => time(),
'exp' => time() + $tokenTTL,
], $decodedKey, 'ES256' , $keyId, [
'id' => $teamId . '.' . $bundleId
]);
} catch (Throwable $e) {
throw new TokenGenerationFailedException('Token failed to generate', 0, $e);
}
}

/**
* Get the generated JWT token
*
* @return string
*/
public function getToken(): string
{
return $this->token;
}

/**
* @return string
*/
public function __toString(): string
{
return $this->getToken();
}

/**
* @param string $p8KeyPath
* @return resource
*/
protected function decodeKey(string $p8KeyPath)
{
$key = openssl_pkey_get_private(file_get_contents($p8KeyPath));
if (! $key) {
throw new KeyDecodingException('Key could not be decoded.');
}

return $key;
}
}
26 changes: 25 additions & 1 deletion src/WeatherKit.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Rich2k\LaravelWeatherKit\Exceptions\DataSetNotFoundException;
use Rich2k\LaravelWeatherKit\Exceptions\LaravelWeatherKitException;
use Rich2k\LaravelWeatherKit\Exceptions\KeyNotFoundExceptione;
use Rich2k\LaravelWeatherKit\Exceptions\MissingCoordinatesException;
use Rich2k\LaravelWeatherKit\Exceptions\TokenGenerationFailedException;

class WeatherKit
{
public const AUTH_TYPE_TOKEN = 'jwt';
public const AUTH_TYPE_P8 = 'p8';

protected $client;

protected $jwtToken;
Expand All @@ -26,12 +32,30 @@ class WeatherKit
protected ?Carbon $hourlyEnd = null;
protected ?Carbon $dailyStart = null;
protected ?Carbon $dailyEnd = null;

/**
* WeatherKit constructor.
*
* @throws LaravelWeatherKitException
*/
public function __construct()
{
$this->jwtToken = config('weatherkit.auth.config.jwt');
if (config('weatherkit.auth.type') === WeatherKit::AUTH_TYPE_P8) {
try {
$this->jwtToken = new JWTToken(
config('weatherkit.auth.config.pathToKeyFile'),
config('weatherkit.auth.config.keyId'),
config('weatherkit.auth.config.teamId'),
config('weatherkit.auth.config.bundleId'),
config('weatherkit.auth.config.tokenTTL')
);
} catch (TokenGenerationFailedException | KeyFileMissingException | TokenGenerationFailedException $e) {
throw new LaravelWeatherKitException($e->getMessage(), $e->getCode(), $e);
}
} else {
$this->jwtToken = config('weatherkit.auth.config.jwt');
}

$this->client = new \GuzzleHttp\Client();

$this->language(config('weatherkit.languageCode'));
Expand Down

0 comments on commit 7281b6d

Please sign in to comment.