Congratulations!

[Valid RSS] This is a valid RSS feed.

Recommendations

This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.

Source: http://akrabat.com/feed/

  1. <?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
  2. xmlns:content="http://purl.org/rss/1.0/modules/content/"
  3. xmlns:wfw="http://wellformedweb.org/CommentAPI/"
  4. xmlns:dc="http://purl.org/dc/elements/1.1/"
  5. xmlns:atom="http://www.w3.org/2005/Atom"
  6. xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
  7. xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
  8. >
  9.  
  10. <channel>
  11. <title>Rob Allen</title>
  12. <atom:link href="https://akrabat.com/feed/" rel="self" type="application/rss+xml" />
  13. <link>https://akrabat.com</link>
  14. <description>Pragmatism in the real world</description>
  15. <lastBuildDate>Wed, 17 Apr 2024 09:26:05 +0000</lastBuildDate>
  16. <language>en-US</language>
  17. <sy:updatePeriod>
  18. hourly </sy:updatePeriod>
  19. <sy:updateFrequency>
  20. 1 </sy:updateFrequency>
  21. <generator>https://wordpress.org/?v=6.5.2</generator>
  22. <item>
  23. <title>Command line access to the Mac Keychain with keyring</title>
  24. <link>https://akrabat.com/command-line-access-to-the-mac-keychain-with-keyring/</link>
  25. <comments>https://akrabat.com/command-line-access-to-the-mac-keychain-with-keyring/#respond</comments>
  26. <dc:creator><![CDATA[Rob]]></dc:creator>
  27. <pubDate>Tue, 23 Apr 2024 10:00:00 +0000</pubDate>
  28. <category><![CDATA[Command Line]]></category>
  29. <category><![CDATA[Computing]]></category>
  30. <guid isPermaLink="false">https://akrabat.com/?p=7007</guid>
  31.  
  32. <description><![CDATA[While reading Alex Chan's post about experimenting with the Flickr API, I noticed the call out to keyring by Jason Coombs for accessing the macOS Keychain. The built-in app: security The built-in way to access the keychain from the command line is /usr/bin/security: To create a password: $ security add-generic-password -s FlickrAPI -a rodeo -w redacted-key Note that you need to include the password on the command line in clear test, so it's now in… <a href="https://akrabat.com/command-line-access-to-the-mac-keychain-with-keyring/">continue reading</a>.]]></description>
  33. <content:encoded><![CDATA[<p>While reading <a href="https://alexwlchan.net/2024/flapi/">Alex Chan's post about experimenting with the Flickr API</a>, I noticed the call out to <a href="https://github.com/jaraco/keyring">keyring</a> by Jason Coombs for accessing the macOS Keychain.</p>
  34. <h2>The built-in app: security</h2>
  35. <p>The built-in way to access the keychain from the command line is <tt>/usr/bin/security</tt>:</p>
  36. <p>To create a password:</p>
  37. <pre>$ security add-generic-password -s FlickrAPI -a rodeo -w redacted-key
  38. </pre>
  39. <p>Note that you need to include the password on the command line in clear test, so it's now in your history unless you remembered to include a space before <tt>security</tt>.</p>
  40. <p>Then, to retrieve it:</p>
  41. <pre>$ security find-generic-password -s FlickrAPI -a rodeo -w
  42. redacted-key
  43. </pre>
  44. <p>Not especially difficult, but not the easiest to remember.</p>
  45. <h2>Keyring makes it simpler</h2>
  46. <p>To set a password using keyring:</p>
  47. <pre>
  48. $ keyring set FlickrAPI caledonia
  49. Password for 'caledonia' in 'FlickrAPI':
  50. </pre>
  51. <p>It doesn't display your password as you enter it, so no history issues to worry about.</p>
  52. <p>Again, retrieving is simpler too:</p>
  53. <pre>
  54. $keyring get FlickrAPI rodeo
  55. redacted-key
  56. </pre>
  57. <p>Rather usefully, it also works on Windows and Linux in addition to Mac, utilising the appropriate backend. You can even use it with <a href="https://github.com/jaraco/keyring#third-party-backends">other backends</a>.</p>
  58. <p>As with Alex's use-case, I can see how this is a nice tool for using in CLI scripts to get access to API keys or other secrets while keeping them secure.</p>
  59. ]]></content:encoded>
  60. <wfw:commentRss>https://akrabat.com/command-line-access-to-the-mac-keychain-with-keyring/feed/</wfw:commentRss>
  61. <slash:comments>0</slash:comments>
  62. </item>
  63. <item>
  64. <title>Matt Gemmell&#039;s short stories</title>
  65. <link>https://akrabat.com/matt-gemmells-short-stories/</link>
  66. <comments>https://akrabat.com/matt-gemmells-short-stories/#respond</comments>
  67. <dc:creator><![CDATA[Rob]]></dc:creator>
  68. <pubDate>Tue, 16 Apr 2024 10:00:00 +0000</pubDate>
  69. <category><![CDATA[Around the web]]></category>
  70. <category><![CDATA[Life]]></category>
  71. <guid isPermaLink="false">https://akrabat.com/?p=7009</guid>
  72.  
  73. <description><![CDATA[I've been following the work of Matt Gemmell for years. His techno-thriller Kestrel series a great fun to read and I recommend that you read them if that's your thing. He also writes short stories, one every week. These are excellent. They are free and as they are short, they don't take long to read at all. A wonderful break from the reality of the world of work, I enjoy reading each week's story with… <a href="https://akrabat.com/matt-gemmells-short-stories/">continue reading</a>.]]></description>
  74. <content:encoded><![CDATA[<p>I've been following the work of <a href="https://mattgemmell.scot">Matt Gemmell</a> for years. His techno-thriller Kestrel series a great fun to read and I recommend that you read them if that's your thing.</p>
  75. <p>He also writes <a href="https://mattgemmell.scot/books/once-upon-a-time/stories/">short stories</a>, one every week. These are <em>excellent</em>. They are free and as they are short, they don't take long to read at all. A wonderful break from the reality of the world of work, I enjoy reading each week's story with a <a href="https://www.yorkshiretea.co.uk">cup of tea</a>.</p>
  76. <p>This week's story, <a href="https://mattgemmell.scot/books/once-upon-a-time/stories/the-pathogen/">The Pathogen</a> is as good an introduction to the <em>One Upon A Time</em> collective as any I've come across. At less than 1000 words, I was gripped and very much enjoyed it. </p>
  77. <p>However, the true mark of a good story is that I'm still thinking about it a day later.</p>
  78. ]]></content:encoded>
  79. <wfw:commentRss>https://akrabat.com/matt-gemmells-short-stories/feed/</wfw:commentRss>
  80. <slash:comments>0</slash:comments>
  81. </item>
  82. <item>
  83. <title>Check licenses of composer dependencies</title>
  84. <link>https://akrabat.com/check-licenses-of-composer-dependencies/</link>
  85. <comments>https://akrabat.com/check-licenses-of-composer-dependencies/#comments</comments>
  86. <dc:creator><![CDATA[Rob]]></dc:creator>
  87. <pubDate>Tue, 09 Apr 2024 10:00:00 +0000</pubDate>
  88. <category><![CDATA[PHP]]></category>
  89. <guid isPermaLink="false">https://akrabat.com/?p=6995</guid>
  90.  
  91. <description><![CDATA[With some commercial projects, it can be useful to know that all your dependencies have licences that your organisation deems acceptable. I had this requirement for a few clients now and came up with this script that we ran as part of our CI which would then fail if a dependency used a license that wasn't allowed. This proved to be reasonably easy as composer licenses will provide a list of all packages with their… <a href="https://akrabat.com/check-licenses-of-composer-dependencies/">continue reading</a>.]]></description>
  92. <content:encoded><![CDATA[<p>With some commercial projects, it can be useful to know that all your dependencies have licences that your organisation deems acceptable.</p>
  93. <p>I had this requirement for a few clients now and came up with this script that we ran as part of our CI which would then fail if a dependency used a license that wasn't allowed.</p>
  94. <p>This proved to be reasonably easy as <tt>composer licenses</tt> will provide a list of all packages with their license, and more usefully, the <tt>-f json</tt> switch will output the list as JSON. With a machine-readable format, the script just came together!</p>
  95. <p>At some point, we discovered that we needed to allow exceptions for specifically authorised packages, so I added that and haven't changed it since.</p>
  96. <h2>check-licenses.php</h2>
  97. <pre lang="php">
  98. &lt;?php
  99.  
  100. $allowedLicenses = ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MIT', 'MPL-2.0', 'OSL-3.0'];
  101. $allowedExceptions = [
  102.    'some-provider/some-provider-php', // Proprietary license used by SMS provider
  103. ];
  104.  
  105. $licences = shell_exec('composer licenses -f json');
  106. if ($licences === null || $licences === false) {
  107.    echo "Failed to retrieve licenses\n";
  108.    exit(1);
  109. }
  110.  
  111. try {
  112.    $data = json_decode($licences, true, 512, JSON_THROW_ON_ERROR);
  113. } catch (JsonException $e) {
  114.    echo "Failed to decode licenses JSON: " . $e->getMessage() . "\n";
  115.    exit(1);
  116. }
  117.  
  118. // Filter out all dependencies that have an allowed license or exception
  119. $disallowed = array_filter(
  120.    $data['dependencies'],
  121.    fn(array $info, $name) => ! in_array($name, $allowedExceptions)
  122.        && count(array_diff($info['license'], $allowedLicenses)) === 1,
  123.    ARRAY_FILTER_USE_BOTH
  124. );
  125. if (count($disallowed)) {
  126.    $disallowedList = array_map(
  127.        fn(string $k, array $info) => sprintf("$k (%s)", implode(',', $info['license'])),
  128.        array_keys($disallowed),
  129.        $disallowed
  130.    );
  131.  
  132.    printf("Disallowed licenses found in PHP dependencies: %s\n", implode(', ', $disallowedList));
  133.    exit(1);
  134. }
  135.  
  136. exit(0);
  137. </pre>
  138. <h2>Running check-licenses.php</h2>
  139. <p>If all dependencies are allowed, then <tt>check-licenses</tt> will output nothing and exit with status code <tt>0</tt>:</p>
  140. <pre>
  141. $ php bin/check-licenses.php
  142. $ echo $?
  143. 0
  144. </pre>
  145. <p>If at least one dependency is not allowed, then <tt>check-licenses</tt> will list the packages that have licenses that ar not allowed and exit with status code <tt>1</tt>:</p>
  146. <pre>
  147. $ php bin/check-licenses.php
  148. Disallowed licenses found in PHP dependencies: foo/bar (GPL-3.0)
  149. $ echo $?
  150. 1
  151. </pre>
  152. <p>Maybe it's useful to others too. If you use it, put it in your CI system.</p>
  153. ]]></content:encoded>
  154. <wfw:commentRss>https://akrabat.com/check-licenses-of-composer-dependencies/feed/</wfw:commentRss>
  155. <slash:comments>3</slash:comments>
  156. </item>
  157. <item>
  158. <title>Reinstall pipx apps after Homebrew Python upgrade</title>
  159. <link>https://akrabat.com/reinstall-pipx-apps-after-homebrew-python-upgrade/</link>
  160. <comments>https://akrabat.com/reinstall-pipx-apps-after-homebrew-python-upgrade/#respond</comments>
  161. <dc:creator><![CDATA[Rob]]></dc:creator>
  162. <pubDate>Tue, 02 Apr 2024 10:00:00 +0000</pubDate>
  163. <category><![CDATA[Computing]]></category>
  164. <category><![CDATA[rst2pdf]]></category>
  165. <guid isPermaLink="false">https://akrabat.com/?p=6991</guid>
  166.  
  167. <description><![CDATA[I install Python apps on my Mac using pipx like this: pipx install rst2pdf This will then install rst2pdf into its own isolated environment so that its dependencies do not affect and are not affected by any other Python app I have installed. Internally, it creates a venv at /.local/pipx/venvs/rst2pdf with symlinks to the currently installed python in the bin directory: lrwxrwxr-x@ 1 rob staff 10 24 May 2023 python -> python3.11 lrwxrwxr-x@ 1 rob… <a href="https://akrabat.com/reinstall-pipx-apps-after-homebrew-python-upgrade/">continue reading</a>.]]></description>
  168. <content:encoded><![CDATA[<p>I install Python apps on my Mac using <a href="https://pipx.pypa.io">pipx</a> like this:</p>
  169. <pre>pipx install rst2pdf</pre>
  170. <p>This will then install <a href="https://rst2pdf.org">rst2pdf</a> into its own isolated environment so that its dependencies do not affect and are not affected by any other Python app I have installed.</p>
  171. <p>Internally, it creates a venv at <tt>/.local/pipx/venvs/rst2pdf</tt> with symlinks to the currently installed python in the <tt>bin</tt> directory:</p>
  172. <pre>
  173. lrwxrwxr-x@  1 rob  staff     10 24 May  2023 python -> python3.11
  174. lrwxrwxr-x@  1 rob  staff     10 24 May  2023 python3 -> python3.11
  175. lrwxrwxr-x@  1 rob  staff     96 24 May  2023 python3.11 -> /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/bin/python3.11
  176. </pre>
  177. <p>All is good. </p>
  178. <p>However, when the Homebrew version of Python is updated, the old version is removed.</p>
  179. <p>Running rst2pdf now fails with "cannot execute: required file not found" as the linked python no longer exists.</p>
  180. <p>It's easy enough to fix when you work out what's happened:</p>
  181. <pre>
  182. pipx reinstall-all
  183. </pre>
  184. <p>I wish this situation was handled a little bit better though.</p>
  185. ]]></content:encoded>
  186. <wfw:commentRss>https://akrabat.com/reinstall-pipx-apps-after-homebrew-python-upgrade/feed/</wfw:commentRss>
  187. <slash:comments>0</slash:comments>
  188. </item>
  189. <item>
  190. <title>Creating JWKS.json file in PHP</title>
  191. <link>https://akrabat.com/creating-jwks-json-file-in-php/</link>
  192. <comments>https://akrabat.com/creating-jwks-json-file-in-php/#respond</comments>
  193. <dc:creator><![CDATA[Rob]]></dc:creator>
  194. <pubDate>Tue, 26 Mar 2024 11:00:01 +0000</pubDate>
  195. <category><![CDATA[PHP]]></category>
  196. <guid isPermaLink="false">https://akrabat.com/?p=6986</guid>
  197.  
  198. <description><![CDATA[In order to verify a JWT created with an asymmetric key, the verifier needs to get the correct public key. One way to do is described in RFC7517 which describes the JSON Web Key format. Within the header of the JWT there is a kid property which is the key ID which is then used to find the correct key within a list provided at the /.well-known/jwks.json endpoint. The JWT header therefore looks something like… <a href="https://akrabat.com/creating-jwks-json-file-in-php/">continue reading</a>.]]></description>
  199. <content:encoded><![CDATA[<p>In order to verify a JWT created with an asymmetric key, the verifier needs to get the correct public key. One way to do is described in <a href="https://www.rfc-editor.org/rfc/rfc7517">RFC7517</a> which describes the JSON Web Key format.</p>
  200. <p>Within the header of the JWT there is a <tt>kid</tt> property which is the key ID which is then used to find the correct key within a list provided at the <tt>/.well-known/jwks.json</tt> endpoint.</p>
  201. <p>The JWT header therefore looks something like this:</p>
  202. <pre lang="json">
  203. {
  204.  "alg" : "RS256",
  205.  "kid" : "6eaf334518784ff392c3123b41ae49f5",
  206.  "typ" : "JWT"
  207. }
  208. </pre>
  209. <p>And the <tt>jwks.json</tt> is structured something like this:</p>
  210. <pre lang="json">
  211. {
  212.    "keys": [
  213.        {
  214.            "alg": "RS256",
  215.            "kty": "RSA",
  216.            "use": "sig",
  217.            "kid": "6eaf334518784ff392c3123b41ae49f5",
  218.            "n": "sj6R1AYPKISqYKFxmQMMFJSm583Jfn6ef51SpQPCe17SM10Ljp2YIte924U ...",
  219.            "e": "AQAB"
  220.        }
  221.    ]
  222. }
  223. </pre>
  224. <p>This is an interesting format as it doesn't use the standard PEM format for the key, but rather stores it as a modulus ("n") and exponent ("e") as per <a href="https://datatracker.ietf.org/doc/html/rfc7518#section-6.3">RFC 7518 section 6.3</a>:</p>
  225. <dl>
  226. <dt>6.3.1.1. "n" (Modulus) Parameter</dt>
  227. <dd>The "n" (modulus) parameter contains the modulus value for the RSA<br />
  228.   public key.  It is represented as a Base64urlUInt-encoded value.</dd>
  229. <dd>Note that implementers have found that some cryptographic libraries<br />
  230.   prefix an extra zero-valued octet to the modulus representations they<br />
  231.   return, for instance, returning 257 octets for a 2048-bit key, rather<br />
  232.   than 256.  Implementations using such libraries will need to take<br />
  233.   care to omit the extra octet from the base64url-encoded<br />
  234.   representation.</dd>
  235. <dt>6.3.1.2. "e" (Exponent) Parameter</dt>
  236. <dd>The "e" (exponent) parameter contains the exponent value for the RSA<br />
  237.   public key.  It is represented as a Base64urlUInt-encoded value.</dd>
  238. </dl>
  239. <p>Fortunately, we can use openssl to sort this all out for us:</p>
  240. <pre lang="php">
  241. // $keyString is a PEM encoded public key
  242. $key = openssl_get_publickey($keyString);
  243. $details = openssl_pkey_get_details($key);
  244. </pre>
  245. <p>Assuming <tt>$key</tt> is an instance of <tt>OpenSSLAsymmetricKey</tt> and <tt>$details</tt> is an array, then:</p>
  246. <pre lang="php">
  247. $modulus = $details['rsa']['n'];
  248. $exponent = $details['rsa']['e'];
  249. </pre>
  250. <p>Putting this into a PSR-15 request handler that is passed an array of public keys, we can put together a <tt>jwks.json</tt> response:</p>
  251. <pre lang="php">
  252. &lt;?php
  253.  
  254. declare(strict_types=1);
  255.  
  256. namespace App\Handler;
  257.  
  258. use Laminas\Diactoros\Response\JsonResponse;
  259. use Psr\Http\Message\ResponseInterface;
  260. use Psr\Http\Message\ServerRequestInterface;
  261. use Psr\Http\Server\RequestHandlerInterface;
  262. use Webmozart\Assert\Assert;
  263.  
  264. class JwksHandler implements RequestHandlerInterface
  265. {
  266.    /**
  267.     * @param string[] $publicKeys
  268.     */
  269.    public function __construct(private readonly array $publicKeys)
  270.    {
  271.    }
  272.  
  273.    public function handle(ServerRequestInterface $request): ResponseInterface
  274.    {
  275.        $keys = [];
  276.  
  277.        foreach ($this->publicKeys as $keyString) {
  278.            $key = openssl_get_publickey($keyString);
  279.            Assert::isInstanceOf(\OpenSSLAsymmetricKey::class, 'Public key is not valid.');
  280.  
  281.            $details = openssl_pkey_get_details($key);
  282.            Assert::isArray($details, 'Public key details are not valid.');
  283.  
  284.            $keys[] = [
  285.                'kty' => 'RSA',
  286.                'alg' => 'RS256',
  287.                'use' => 'sig',
  288.                'kid' => sha1($keyString),
  289.                'n'   => strtr(rtrim(base64_encode($details['rsa']['n']), '='), '+/', '-_'),
  290.                'e'   => strtr(rtrim(base64_encode($details['rsa']['e']), '='), '+/', '-_'),
  291.            ];
  292.        }
  293.  
  294.        return new JsonResponse(['keys' => $keys]);
  295.    }
  296. }
  297. </pre>
  298. <p>Note that we remove the base64 padding (<tt>=</tt> at the end) and also use the <a href="https://base64.guru/standards/base64url">Base64Url modification</a> where "<tt>+</tt>" is replaced with "<tt>-</tt>" and "<tt>/</tt>" with "<tt>_</tt>".</p>
  299. <p>The other properties in the JSON object are:</p>
  300. <ul>
  301. <li><a href="https://www.rfc-editor.org/rfc/rfc7517#section-4.1"><tt>kty</tt></a>: Key Type &#8211; the cryptographic algorithm family used with the key</li>
  302. <li><a href="https://www.rfc-editor.org/rfc/rfc7517#section-4.4"><tt>alg</tt></a>: Algorithm &#8211; the specific cryptographic algorithm used.</li>
  303. <li><a href="https://www.rfc-editor.org/rfc/rfc7517#section-4.2"><tt>use</tt></a>: Use &#8211; the intended use of the public key. "sig" for signature, "enc" for encryption.</li>
  304. <li><a href="https://www.rfc-editor.org/rfc/rfc7517#section-4.5"><tt>kid</tt></a>: Key ID &#8211; used to match a specific key</li>
  305. </ul>
  306. <p>The verifier reads the <tt>jwks.json</tt> file and iterates over the list to find the one that matches the <tt>kid</tt> in the JWT header that they are trying to verify. When they find it, the can then convert the modulus exponent back into a public key and verify the JWT as per usual.</p>
  307. ]]></content:encoded>
  308. <wfw:commentRss>https://akrabat.com/creating-jwks-json-file-in-php/feed/</wfw:commentRss>
  309. <slash:comments>0</slash:comments>
  310. </item>
  311. <item>
  312. <title>A quick guide to JWTs in PHP</title>
  313. <link>https://akrabat.com/a-quick-guide-to-jwts-in-php/</link>
  314. <comments>https://akrabat.com/a-quick-guide-to-jwts-in-php/#respond</comments>
  315. <dc:creator><![CDATA[Rob]]></dc:creator>
  316. <pubDate>Tue, 19 Mar 2024 11:00:00 +0000</pubDate>
  317. <category><![CDATA[PHP]]></category>
  318. <guid isPermaLink="false">https://akrabat.com/?p=6983</guid>
  319.  
  320. <description><![CDATA[The most common use of JWTs is as an authentication token, usually within an OAuth2 workflow. Creating these tokens is part and parcel of the authentication library that you use. I recently had a requirement to use a JWT independent of authentication and these are some notes on what I learned when researching with Lcobucci\JWT. Make up of a JWT To really understand JWTs, read RFC7519. For a more readable introduction, read the one on… <a href="https://akrabat.com/a-quick-guide-to-jwts-in-php/">continue reading</a>.]]></description>
  321. <content:encoded><![CDATA[<p>The most common use of JWTs is as an authentication token, usually within an OAuth2 workflow. Creating these tokens is part and parcel of the authentication library that you use.</p>
  322. <p>I recently had a requirement to use a JWT independent of authentication and these are some notes on what I learned when researching with <a href="https://github.com/lcobucci/jwt">Lcobucci\JWT</a>.</p>
  323. <h2>Make up of a JWT</h2>
  324. <p>To really understand JWTs, read <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-1">RFC7519</a>. For a more readable introduction, read the one on <a href="https://jwt.io/introduction">jwt.io</a>.</p>
  325. <p>Also, JWT is pronounced "<em>jot</em>".</p>
  326. <p>The most important thing about a JWT is that it contains data and a signature. The signature allows you to verify that the JWT's data hasn't been tampered with. This works because the signature is signed with a secret. This can be a shared secret (a symmetrical algorithm) or a public/private key pair (asymmetric algorithm).</p>
  327. <p>In my case, I'm only interested in signing a JWT using a public/private key as my client cannot securely hold a shared secret.</p>
  328. <h2>Creating a public/private key</h2>
  329. <p><a href="https://www.openssl.org">OpenSSL</a> is your friend here. Create a <tt>keys</tt> directory and then use the command line.</p>
  330. <p>To create a private key:</p>
  331. <pre>
  332. openssl genpkey -algorithm RSA -out keys/private.key -pkeyopt rsa_keygen_bits:2048
  333. </pre>
  334. <p>To create the public key from the private key:</p>
  335. <pre>
  336. openssl rsa -pubout -in keys/private.key -out keys/public.key
  337. </pre>
  338. <p>You will now have two files in the <tt>keys</tt> directory: <tt>private.key</tt> and <tt>public.key</tt>. Keep <tt>private.key</tt> safe and make <tt>public.key</tt> available to your clients.</p>
  339. <h2>Creating a token</h2>
  340. <p>Using Lcobucci\JWT, we create a <tt>Configuration</tt>:</p>
  341. <pre lang="php">
  342. use Lcobucci\JWT\Configuration;
  343. use Lcobucci\JWT\Signer\Rsa\Sha256;
  344. use Lcobucci\JWT\Signer\Key\InMemory;
  345.  
  346. $configuration = Configuration::forAsymmetricSigner(
  347.    new Sha256(),
  348.    InMemory::file(__DIR__ . '/keys/private.key'),
  349.    InMemory::file(__DIR__ . '/keys/public.key')
  350. );
  351. </pre>
  352. <p>From our configuration, we can obtain a builder and specify our token:</p>
  353. <pre lang="php">
  354. $keyId = '40597EB1-5E20-49B5-BDDF-B24D1B3B05B5';
  355. $subject = '851828E8-C376-4026-9FD8-D2CCE406CD1C';
  356. $now = new \DateTimeImmutable();
  357.  
  358. $builder = $configuration->builder()
  359.    ->identifiedBy($keyId)
  360.    ->relatedTo($subject)
  361.    ->issuedBy('https://app.example.com')
  362.    ->issuedAt($now)
  363.    ->expiresAt($now->modify('+2 weeks'))
  364.    ->withClaim('foo', 'bar');
  365. </pre>
  366. <p>The data elements in a JWT are called claims. There are a number of <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1">registered claims</a> that are not mandatory, but you should set if they are relevant as all clients will recognise them and their purpose. In particular JWT ID, issuer, subject, issued at and expiration time are particularly useful. There are also a set of <a href="https://www.iana.org/assignments/jwt/jwt.xhtml#claims">public claims</a> that provide a standardised set of key names for common information which help avoid clashes.</p>
  367. <p>Then there is the data specific to your application which are known as private claims. These can be whatever you like and in the example above, we have created a claim called "foo".</p>
  368. <p>Finally we create the token itself:</p>
  369. <pre lang="php">
  370. $token = $builder->getToken($configuration->signer(), $configuration->signingKey());
  371. $tokenString = $token->toString();
  372. </pre>
  373. <p>We can then send the token string in response to an API request, etc.</p>
  374. <h2>Validating a token</h2>
  375. <p>When we receive a token, we need to validate it. This means that we check that it hasn't been tampered with and we can also check that the data within it is as expected.</p>
  376. <p>Firstly, we parse the token string back into a token object:</p>
  377. <pre lang="php">
  378. use Lcobucci\Clock\SystemClock;
  379. use Lcobucci\JWT\Encoding\JoseEncoder;
  380. use Lcobucci\JWT\Signer\Rsa\Sha256;
  381. use Lcobucci\JWT\Token\Parser;
  382.  
  383. $parser = new Parser(new JoseEncoder());
  384. $token = $parser->parse($tokenString);
  385. </pre>
  386. <p>To validate it, we need a validator and a set of constraints that the validator will test the token for:</p>
  387. <pre lang="php">
  388. $validator = new Validator();
  389.  
  390. $clock = new SystemClock();
  391. $constraints = [
  392.    new SignedWith(new Sha256(), InMemory::file(__DIR__ . '/keys/public.key')),
  393.    new LooseValidAt($clock),
  394.    new IssuedBy('https://app.example.com'),
  395. ];
  396. </pre>
  397. <p>The three constraints here check that the JWT:</p>
  398. <ul>
  399. <li>has been signed with the private key (by using the public key to verify)</li>
  400. <li>is current and has not expired</li>
  401. <li>was issued by the expected issuer</li>
  402. </ul>
  403. <p>There are other constraints too &#8211; check the docs.</p>
  404. <p>Note that the time based constraints use the <a href="https://www.php-fig.org/psr/psr-20/">PSR-20:Clock</a> interface, so we use <a href="https://github.com/lcobucci/clock">Lcobbucci/clock</a> as an implementation of it.</p>
  405. <p>We can then assert these constraints:</p>
  406. <pre>
  407. try {
  408.    $validator->assert($token, ...$constraints);
  409.    // Token is verified
  410.    $claims = $token->claims()->all();
  411. } catch (RequiredConstraintsViolated $e) {
  412.    // Invalid token.
  413.    echo "**Invalid Token**\n";
  414.    foreach ($e->violations() as $violation) {
  415.        echo "- " . $violation->getMessage() . "\n";
  416.    }
  417.    exit(1);
  418. }
  419. </pre>
  420. <p>The <tt>assert()</tt> method will throw an exception with all the violations, so we can see everything that failed in one go which is useful.</p>
  421. <p>That's all there is to issuing and validating arbitrary JWT tokens in PHP with Lcobucci\JWT.</p>
  422. ]]></content:encoded>
  423. <wfw:commentRss>https://akrabat.com/a-quick-guide-to-jwts-in-php/feed/</wfw:commentRss>
  424. <slash:comments>0</slash:comments>
  425. </item>
  426. <item>
  427. <title>Sleeping an external hard drive</title>
  428. <link>https://akrabat.com/sleeping-an-external-hard-drive/</link>
  429. <comments>https://akrabat.com/sleeping-an-external-hard-drive/#respond</comments>
  430. <dc:creator><![CDATA[Rob]]></dc:creator>
  431. <pubDate>Tue, 12 Mar 2024 11:00:00 +0000</pubDate>
  432. <category><![CDATA[Computing]]></category>
  433. <guid isPermaLink="false">https://akrabat.com/?p=6964</guid>
  434.  
  435. <description><![CDATA[One annoyance I had with my external USB hard drives is that they weren't sleeping when idle which makes them noisy. We can't have that! My first port of call was hdparm and its -S parameter: sudo hdparm -S 60 /dev/sdb However this didn't help. Fortunately, I found hd-idle which worked! After installing, you need to edit /etc/default/hd-idle and change the HD_IDLE_OPTS setting from -h to whatever you need. For me, I have set: HD_IDLE_OPTS="-i… <a href="https://akrabat.com/sleeping-an-external-hard-drive/">continue reading</a>.]]></description>
  436. <content:encoded><![CDATA[<p>One annoyance I had with my external USB hard drives is that they weren't sleeping when idle which makes them noisy. We can't have that!</p>
  437. <p>My first port of call was <a href="https://en.wikipedia.org/wiki/Hdparm"><tt>hdparm</tt></a> and its <tt>-S</tt> parameter:</p>
  438. <pre>
  439. sudo hdparm -S 60 /dev/sdb
  440. </pre>
  441. <p>However this didn't help.</p>
  442. <p>Fortunately, I found <a href="https://hd-idle.sourceforge.net">hd-idle</a> which worked!</p>
  443. <p>After installing, you need to edit <tt>/etc/default/hd-idle</tt> and change the <tt>HD_IDLE_OPTS</tt> setting from <tt>-h</tt> to whatever you need.</p>
  444. <p>For me, I have set: <tt>HD_IDLE_OPTS="-i 0 -a sdb -i 60 -a sdc -i 60"</tt></p>
  445. <p>These parameters are:</p>
  446. <ul style="list-style-type: none">
  447. <li><tt>-i 0</tt>: This first <tt>-i</tt> sets the idle timeout for all drives. <tt>0</tt> means no timeout which is what I want for all drives except my external USB ones.</li>
  448. <li><tt>-a sdb -i 60</tt>: For only the disk set after the <tt>-a</tt>, set the idle time to the value of the subsequent <tt>-i</tt>. i.e. set sdb to 60 seconds timeout. </li>
  449. <li><tt>-a sdc -i 60</tt>: For only the disk set after the <tt>-a</tt>, set the idle time to the value of the subsequent <tt>-i</tt>. i.e. set sdc to 60 seconds timeout. </li>
  450. </ul>
  451. <p>Not completely intuitive, but easy enough once you have understood what's going on. I have left a comment in the <tt>/etc/default/hd-idle</tt> to remind me! </p>
  452. <p>Then after a reboot, <tt>ps aux | grep hd-idle</tt> showed that it was running and now my external drives are quiet.</p>
  453. ]]></content:encoded>
  454. <wfw:commentRss>https://akrabat.com/sleeping-an-external-hard-drive/feed/</wfw:commentRss>
  455. <slash:comments>0</slash:comments>
  456. </item>
  457. <item>
  458. <title>Setting up a new hard drive in Linux</title>
  459. <link>https://akrabat.com/setting-up-a-new-hard-drive-in-linux/</link>
  460. <comments>https://akrabat.com/setting-up-a-new-hard-drive-in-linux/#respond</comments>
  461. <dc:creator><![CDATA[Rob]]></dc:creator>
  462. <pubDate>Tue, 05 Mar 2024 11:00:00 +0000</pubDate>
  463. <category><![CDATA[Computing]]></category>
  464. <guid isPermaLink="false">https://akrabat.com/?p=6941</guid>
  465.  
  466. <description><![CDATA[I recently added a second SSD to my Linux server and had to look up how to format it and set it up, having not taken notes for the first one. These are the notes I took the second time. This is all done from the command line and the monospace text is to be typed directly &#8211; though change the identifiers if requried. sudo lshw -C disk to confirm disk is seen by the… <a href="https://akrabat.com/setting-up-a-new-hard-drive-in-linux/">continue reading</a>.]]></description>
  467. <content:encoded><![CDATA[<p>I recently added a second SSD to my Linux server and had to look up how to format it and set it up, having not taken notes for the first one. These are the notes I took the second time.</p>
  468. <p>This is all done from the command line and the <tt>monospace</tt> text is to be typed directly &#8211; though change the identifiers if requried.</p>
  469. <ol>
  470. <li><tt>sudo lshw -C disk</tt> to confirm disk is seen by the BIOS and work out it's path. In this case, it's <tt>/dev/sdb</tt></li>
  471. <li>Partition the disk with a single partition:
  472. <ol>
  473. <li><tt>sudo parted /dev/sdb</tt></li>
  474. <li><tt>mklabel gpt</tt></li>
  475. <li><tt>unit TB</tt></li>
  476. <li><tt>mkpart</tt>
  477. <ul>
  478. <li>Partition Name: <tt>primary</tt></li>
  479. <li>File system type: <tt>ext4</tt></li>
  480. <li>Start: <tt>0%</tt></li>
  481. <li>End: <tt>100%</tt></li>
  482. </ul>
  483. </li>
  484. <li><tt>print</tt> to check everything is correct</li>
  485. <li><tt>quit</tt> to save and exit</li>
  486. </ol>
  487. </li>
  488. <li>Format the disk:
  489. <ol>
  490. <li><tt>sudo mkfs -t ext4 /dev/sdb1</tt></li>
  491. <li>(press the &lt;return&gt; key to create the journal)</li>
  492. </ol>
  493. </li>
  494. <li>Create mount point:
  495. <ol>
  496. <li><tt>sudo mkdir /media/ssd2</tt></li>
  497. </ol>
  498. </li>
  499. <li>Update the filesystem table:
  500. <ol>
  501. <li>sudo vim /etc/fstab</li>
  502. <li>Add this line: <tt>/dev/sdb1       /media/ssd2     ext4    defaults        0       2</tt></li>
  503. <li>Mount it now: <tt>sudo mount -a</tt></li>
  504. </ol>
  505. <li>Set permissions:
  506. <ol>
  507. <li><tt>sudo chgrp plugdev /media/ssd2</tt></li>
  508. <li><tt>sudo chmod g+rws /media/ssd2</tt></li>
  509. <li><tt>sudo chmod +t /media/ssd2</tt></li>
  510. </ol>
  511. </li>
  512. </ol>
  513. <p>All done and working. I then updated by backup scripts to back this drive up to an external hard drive too.</p>
  514. ]]></content:encoded>
  515. <wfw:commentRss>https://akrabat.com/setting-up-a-new-hard-drive-in-linux/feed/</wfw:commentRss>
  516. <slash:comments>0</slash:comments>
  517. </item>
  518. <item>
  519. <title>Using GitHub Actions to add Go binaries to a Release</title>
  520. <link>https://akrabat.com/using-github-actions-to-add-go-binaries-to-a-release/</link>
  521. <comments>https://akrabat.com/using-github-actions-to-add-go-binaries-to-a-release/#comments</comments>
  522. <dc:creator><![CDATA[Rob]]></dc:creator>
  523. <pubDate>Tue, 27 Feb 2024 11:00:00 +0000</pubDate>
  524. <category><![CDATA[Development]]></category>
  525. <category><![CDATA[Go]]></category>
  526. <category><![CDATA[Rodeo Flickr Uploader]]></category>
  527. <guid isPermaLink="false">https://akrabat.com/?p=6950</guid>
  528.  
  529. <description><![CDATA[Shortly after building a script to create binaries for Rodeo, my command line Flickr uploader, I realised that I could use a Github Actions workflow to run it and attach the created binaries to the Release page once I had created it. This is what I did. Trigger on release A GitHub Actions workflow is a YAML file and each workflow has to be triggered. For Continuous Integration, it's common to trigger when new PR… <a href="https://akrabat.com/using-github-actions-to-add-go-binaries-to-a-release/">continue reading</a>.]]></description>
  530. <content:encoded><![CDATA[<p>Shortly after building a script to create binaries for <a href="https://github.com/akrabat/rodeo">Rodeo</a>, my command line <a href="https://www.flickr.com">Flickr</a> uploader, I realised that I could use a <a href="https://github.com/features/actions">Github Actions</a> workflow to run it and attach the created binaries to the Release page once I had created it.</p>
  531. <p>This is what I did.</p>
  532. <h2>Trigger on release</h2>
  533. <p>A GitHub Actions workflow is a YAML file and each workflow has to be triggered. For Continuous Integration, it's common to trigger when new PR is opened or a new commit is pushed and then run tests on the code. In this case, I want to trigger when a new release is created. </p>
  534. <p>This is done in the <tt>on</tt> section:</p>
  535. <p><strong>.github/workflows/build-release-binaries.yml</strong></p>
  536. <pre lang="yaml">
  537. name: Build Release Binaries
  538.  
  539. on:
  540.  release:
  541.    types:
  542.      - created
  543. </pre>
  544. <p>GitHUb documents the list of <a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows">events that trigger workflows</a> and there's many. For each event (such as <tt>release</tt>) there are a number of activity types so you can be quite selective on exactly when you want the workflow to run. In this case, I want to bulid the binaries when the release is created.</p>
  545. <h2>The workflow steps</h2>
  546. <p>I have one job which is imaginatively named <tt>build</tt>. </p>
  547. <h3>Set up</h3>
  548. <p>Each job has a set of steps and the first few are set up ones:</p>
  549. <pre lang="yaml">
  550. jobs:
  551.  build:
  552.    name: Build Release Assets
  553.    runs-on: ubuntu-latest
  554.    steps:
  555.      - name: Checkout code
  556.        uses: actions/checkout@v2
  557.  
  558.      - name: Set up Go
  559.        uses: actions/setup-go@v1
  560.        with:
  561.          go-version: 1.20
  562.  
  563.      - name: Display the version of go that we have installed
  564.        run: go version
  565.  
  566.      - name: Display the release tag
  567.        run: echo ${{ github.event.release.tag_name }}
  568. </pre>
  569. <p>The workflow runs inside a container, so firstly we checkout the code and install Go. I then display the version of Go and the tag name to check that all is okay. This is mostly useful when something goes wrong.</p>
  570. <h3>Build the binaries</h3>
  571. <p>We already know <a href="/building-go-binaries-for-different-platforms">how to build release binaries</a>, so we can just run a script that does that:</p>
  572. <pre lang="yaml">
  573.      - name: Build the Rodeo executables
  574.        run: ./build-executables.sh ${{ github.event.release.tag_name }}
  575.  
  576.      - name: List the Rodeo executables
  577.        run: ls -l ./release
  578. </pre>
  579. <p>I've written <tt>build-executables.sh</tt> to take the version number as the first argument so that we can pass in the tag name. We then list what has been built to ensure that it is as we expect.</p>
  580. <h3>Upload the binaries</h3>
  581. <p>Finally we need to upload the binaries to the release. We use Sven-Hendrik Haase's <a href="https://github.com/svenstaro/upload-release-action">upload-release-action</a> to do this.</p>
  582. <pre lang="yaml">
  583.      - name: Upload the Rodeo binaries
  584.        uses: svenstaro/upload-release-action@v2
  585.        with:
  586.          repo_token: ${{ secrets.GITHUB_TOKEN }}
  587.          tag: ${{ github.ref }}
  588.          file: ./release/rodeo-*
  589.          file_glob: true
  590. </pre>
  591. <p>A really nice feature of this action is that it can upload multiple binaries in a single step. This is incredibly useful as GitHub Actions workflows do not support looping of steps, just looping of jobs via the matrix property. Using matrix is wastegul for us as it creates a whole new container for each iteration. We do not need this as with Go one container can build all the binaries.</p>
  592. <p>By setting the <tt>file_glob</tt> property to <tt>true</tt>, we can set the <tt>file</tt> property to a <a href="https://en.wikipedia.org/wiki/Glob_(programming)">glob pattern</a> and so reference all the files in the <tt>release</tt> directory.<br />
  593. 
</p>
  594. <h2>and done</h2>
  595. <p>This workflow runs automatically whenever I create a new release on GitHub, automatically building the binaries against that version of the code and attaching them so that people can download them without needing to build themselves.</p>
  596. <p>Automations like this are what CI/CD pipelines are for.</p>
  597. ]]></content:encoded>
  598. <wfw:commentRss>https://akrabat.com/using-github-actions-to-add-go-binaries-to-a-release/feed/</wfw:commentRss>
  599. <slash:comments>1</slash:comments>
  600. </item>
  601. <item>
  602. <title>A few thoughts on conferences</title>
  603. <link>https://akrabat.com/a-few-thoughts-on-conferences/</link>
  604. <comments>https://akrabat.com/a-few-thoughts-on-conferences/#respond</comments>
  605. <dc:creator><![CDATA[Rob]]></dc:creator>
  606. <pubDate>Tue, 20 Feb 2024 11:00:00 +0000</pubDate>
  607. <category><![CDATA[Conferences]]></category>
  608. <guid isPermaLink="false">https://akrabat.com/?p=6971</guid>
  609.  
  610. <description><![CDATA[Last week, I attended PHPUK 2024. This is one of the major PHP conferences and I was pleased to speak about DDD there. Sam and the team did a fantastic job this year with the videos already published. To my mind, attending a conference provides a number of benefits. The first and most obvious one is that you learn some amazing things, from people who tend to know what they are talking about. At PHPUK… <a href="https://akrabat.com/a-few-thoughts-on-conferences/">continue reading</a>.]]></description>
  611. <content:encoded><![CDATA[<p>Last week, I attended <a href="https://www.phpconference.co.uk">PHPUK 2024</a>. This is one of the major PHP conferences and I was pleased to speak about DDD there. Sam and the team did a fantastic job this year with the videos <a href="https://www.youtube.com/playlist?list=PL_aPVo2HeGF-xaeC3amS4xSmPisiYQxgV">already published</a>.</p>
  612. <p>To my mind, attending a conference provides a number of benefits. The first and most obvious one is that you learn some amazing things, from people who tend to know what they are talking about. At PHPUK there were talks directly relevant to day-to-day development, such as testing, API development and git usage. In addition, you get the opportunity to learn new things that may not be directly applicable today, but are useful for wider understanding or future applicability. I include the talks on serverless, pair programming, and non-traditional uses of PHP from this year's conference in this category. </p>
  613. <p>One thing that a conference programme provides is curation. Sure, you can go to YouTube and find things to learn, but a conference provides a constrained set that the organisers believe is relevant now. </p>
  614. <p>In addition to the content itself, there's the so-called hallway track. This is short-hand for "talking to people informally during the conference". We could also call it networking. </p>
  615. <p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2024/02/IMG_7918-web.jpg" alt="" title="IMG_7918-web.jpg" border="0" width="400" style="float:right; margin-left: 0.5em;" /></p>
  616. <p>The people at a conference know things that you don't and lots of them love to talk about the conference subjects. You can learn much by asking someone what they thought about a talk and then letting the conversation grow from there. If I see a group of people chatting in a break, I'll wander over and join the circle and listen to the conversation. I'll invariably learn something. The people you meet can also become useful contacts for the future. I have met many people at conferences and by these connections, have advanced my knowledge and career.</p>
  617. <p>It's hard to summon up the courage to talk to someone, but in my experience, the speakers in particular are approachable. You already have a conversation starter as you can ask them about their talk and I've found that they are always very happy to chat.</p>
  618. <p>I invariably come away from a conference inspired to do better work at my day-job and also to contribute to the community scene too. The energy from like-minded people at the same event all excited about the same topic cannot be underestimated.</p>
  619. <p>There are many PHP and related conferences all over the world this year and I recommend that you try to attend one. In the US, <a href="https://tek.phparch.com">php[tek]</a> is in Chicago in April, In Europe, there's Amsterdam's <a href="https://phpconference.nl">Dutch PHP Conference</a> in March and then <a href="https://phpday.it">phpday</a> in Verona in May. For UK folks, <a href="https://laravellive.uk">Laravel Live UK</a> is in London in June. This is just scratching the surface and of course there are more in the second half of the year. The PHP website has a rather useful <a href="https://www.php.net/conferences/">list of PHP conferences</a>, so check it out. </p>
  620. <p>There's also tech-agnostics conferences on topics like APIs, DDD, Open Source, and so on. I also recommend considering these too. You will be exposed to different ways of thinking that you can bring back to your own projects. The value of this cross-contamination of ideas across communities cannot be underestimated.</p>
  621. <p>I've also found that relatively few people spend much of their time immersed in learning online in their own time. Another clear advantage of a conference is that you have reserved time and space for learning both formally and informally. Give yourself this opportunity and take advantage it.</p>
  622. ]]></content:encoded>
  623. <wfw:commentRss>https://akrabat.com/a-few-thoughts-on-conferences/feed/</wfw:commentRss>
  624. <slash:comments>0</slash:comments>
  625. </item>
  626. </channel>
  627. </rss>
  628.  

If you would like to create a banner that links to this page (i.e. this validation result), do the following:

  1. Download the "valid RSS" banner.

  2. Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)

  3. Add this HTML to your page (change the image src attribute if necessary):

If you would like to create a text link instead, here is the URL you can use:

http://www.feedvalidator.org/check.cgi?url=http%3A//akrabat.com/feed/

Copyright © 2002-9 Sam Ruby, Mark Pilgrim, Joseph Walton, and Phil Ringnalda