<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[koodos labs: Engineering]]></title><description><![CDATA[A publication for engineers who are interested in building the future of the web. We organize and co-host events with organizations like Betaworks, Supabase and the Internet Archive.

Subscribe for thinking from the engineering team @ koodos labs and invitations to events around New York.]]></description><link>https://blog.koodos.com/s/engineering</link><image><url>https://substackcdn.com/image/fetch/$s_!ik8L!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc740f03-fbd4-494f-bf05-df9049be08be_800x800.png</url><title>koodos labs: Engineering</title><link>https://blog.koodos.com/s/engineering</link></image><generator>Substack</generator><lastBuildDate>Sun, 05 Apr 2026 13:11:28 GMT</lastBuildDate><atom:link href="https://blog.koodos.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Koodos, inc.]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[koodos@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[koodos@substack.com]]></itunes:email><itunes:name><![CDATA[jad]]></itunes:name></itunes:owner><itunes:author><![CDATA[jad]]></itunes:author><googleplay:owner><![CDATA[koodos@substack.com]]></googleplay:owner><googleplay:email><![CDATA[koodos@substack.com]]></googleplay:email><googleplay:author><![CDATA[jad]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[When Half a Billion Tiny Files Broke Our Best Laid Plans]]></title><description><![CDATA[A tale of hubris, scale, and sometimes just using a really big machine]]></description><link>https://blog.koodos.com/p/when-half-a-billion-tiny-files-broke</link><guid isPermaLink="false">https://blog.koodos.com/p/when-half-a-billion-tiny-files-broke</guid><dc:creator><![CDATA[Luis Fernandez]]></dc:creator><pubDate>Tue, 23 Sep 2025 18:02:25 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7d35cabf-ddda-4126-a7ab-8e0821d414a7_1024x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>A tale of hubris, scale, and sometimes just using a really big machine</em></p><p>So here&#8217;s how we learned that 500 million tiny files will humble every &#8220;proper&#8221; engineering approach you can think of.</p><p>It started innocently enough. We needed to backfill our data warehouse as part of our new Ingestion Pipeline. Migrate all our historical consumption data from the old S3 bucket format to our shiny new warehouse ready structure. How hard could it be?</p><p>We had two buckets to deal with:</p><ul><li><p><strong>datamover bucket</strong>: 50M files (11M actually had data)</p></li><li><p><strong>raw-consumptions bucket</strong>: 500M files across ~1.7M users</p></li></ul><p>The datamover bucket was going to be our practice round. Get the approach right on 11M files, then scale it up to 500M. Classic engineering, start small, iterate, then scale.</p><p>Spoiler alert: sometimes &#8220;scale it up&#8221; breaks everything you thought you knew.</p><h2>Act I: The Proper Engineering Approach</h2><p><em>&#8220;Let&#8217;s do this right. Database indexes, sequential processing, proper state management.&#8221;</em></p><p>We built what any engineer would build for a data migration:</p><pre><code><code>create table migration_file_index
(
    id              bigserial primary key,
    s3_key          text not null unique,
    platform        text not null,
    job_enqueued_at timestamp with time zone not null,
    file_size       bigint not null,
    processed       boolean default false,
    created_at      timestamp with time zone default now()
);
</code></code></pre><p>With proper indexes, of course, we are professionals:</p><pre><code><code>create index idx_migration_sort
    on migration_file_index (platform, job_enqueued_at)
    where (NOT processed);

create index idx_migration_resume
    on migration_file_index (processed, id);

create index idx_migration_platform_stats
    on migration_file_index (platform, processed);
</code></code></pre><p>The architecture was clean:</p><ul><li><p><strong>DatabaseIndexer</strong>: Catalog all S3 files that had data into Postgres</p></li><li><p><strong>SequentialProcessor</strong>: Process files in chronological order</p></li><li><p><strong>PartitionBuffer</strong>: Smart batching for optimal warehouse file sizes</p></li></ul><p>It felt good. It felt <em>right</em>. This is how you handle a large-scale data migration.</p><p>Then we ran it on our practice dataset.</p><p><strong>11 million files took one week.</strong></p><p>For the datamover migration, a week seemed totally reasonable. Less than a sprint! Exactly what product wants to hear. &#8220;Yeah, we&#8217;ll have the historical data migrated by next Friday.&#8221; Perfect.</p><p>Let me repeat that for emphasis: 11 million files. One week.</p><p>I remember staring at the progress logs, doing the math in my head. If 11M files = 1 week, then 500M files = ... <em>reaches for calculator</em> ... 45 weeks.</p><p>Forty-five weeks. Almost a year. To backfill the raw-consumptions bucket historical data.</p><p>That&#8217;s when we realized that all our careful database design, our proper indexes, our sequential processing - it was all overhead. We were spending more time managing the migration state than actually migrating data.</p><h2>Act II: The Cloud-Native Pivot</h2><p><em>&#8220;Surely AWS can handle what we can&#8217;t, right?&#8221;</em></p><p>Time for a complete rethink. Why are we trying to manage 500M individual files when AWS has services built exactly for this kind of massive parallel processing?</p><p>Enter the Athena approach:</p><ol><li><p>Create an external table mapping to our raw-consumptions bucket</p></li><li><p>Let Athena discover and process all 500M files in a single massive query</p></li><li><p>Output everything to a staging bucket in warehouse format</p></li><li><p>Do a controlled cross-bucket migration to production</p></li></ol><p>The promise was beautiful:</p><ul><li><p>2-6 hours instead of months</p></li><li><p>~$100-200 in Athena costs vs. massive compute bills</p></li><li><p>Leverage cloud services for what they&#8217;re actually built for</p></li></ul><pre><code><code>// Instead of this:
for (const file of 500_000_000_files) {
  await processFileSequentially(file); // &#129326;
}

// We&#8217;d do this:
CREATE TABLE warehouse_data AS
SELECT 
  platform,
  user_id,
  payload,
  ingested_at,
  date_partition,
  hour_partition
FROM external_consumptions_table;
// Let Athena figure it out in parallel!
</code></code></pre><p>We built comprehensive tooling around it:</p><ul><li><p>Quality validation (95% success rate required)</p></li><li><p>Cross-bucket migration with configurable batch sizes</p></li><li><p>Progress monitoring and error recovery</p></li><li><p>Staging-to-production promotion pipeline</p></li></ul><p>It was elegant. It was cloud-native. It was exactly the kind of solution you&#8217;d present at a conference lol.</p><p>And Athena choked.</p><p>Turns out even AWS services have limits when you point them at half a billion tiny files. Queries would time out consistently. Tuns out 500M files is just too many god damn files!!</p><p>The cloud-native dream tried hard, but it died.</p><h2>Act III: The Big Machine Epiphany</h2><p><em>&#8220;You know what? Let&#8217;s just use a really big machine.&#8221;</em></p><p>Sometimes the most sophisticated solution is admitting when sophistication isn&#8217;t working.</p><p>We spun up an i4i.metal instance:</p><ul><li><p>128 vCPUs</p></li><li><p>1TB of RAM</p></li><li><p>30TB of NVMe storage</p></li><li><p>75 Gbps network</p></li></ul><p>The plan was beautifully simple:</p><ol><li><p>Download everything to local NVMe storage</p></li><li><p>Process it with maximum parallelism</p></li><li><p>Upload the results</p></li></ol><p>No databases to maintain. No cloud service limits to hit. No complex state management. Just raw compute power and storage bandwidth.</p><h3>Step 1: Setup the Beast</h3><pre><code><code># Create RAID 0 across all NVMe drives
NVME_DEVICES=$(lsblk -nd -o NAME | grep nvme | grep -v nvme0 | sed &#8216;s/^/\\/dev\\//&#8217; | tr &#8216;\\n&#8217; &#8216; &#8216;)
sudo mdadm --create /dev/md0 --level=0 --raid-devices=$NVME_COUNT $NVME_DEVICES
sudo mkfs.ext4 -F /dev/md0
sudo mount /dev/md0 /mnt/nvme
</code></code></pre><h3>Step 2: Download Everything</h3><pre><code><code># s5cmd with 4096 workers to max out the 75 Gbps network
s5cmd --numworkers 4096 cp &#8220;s3://consumptions-raw/*&#8221; /mnt/nvme/raw-data/
</code></code></pre><p>Enter s5cmd - if you haven&#8217;t used it, it&#8217;s like AWS CLI&#8217;s faster, more parallel cousin. While aws s3 cp might give you decent performance, s5cmd is built specifically for high-throughput S3 operations. It can spin up thousands of concurrent workers and actually saturate high-bandwidth connections.</p><p>With our 75 Gbps network and s5cmd cranked up to 4096 workers, we could download at speeds that would make your standard S3 sync weep.</p><h3>Step 3: The Organize Breakthrough</h3><p>This is where it got interesting. We needed to transform the data from:</p><pre><code><code>platform/user123/2024-01-15T14:30:15.json
platform/user123/2024-01-15T14:45:22.json  
platform/user456/2024-01-15T14:32:18.json
</code></code></pre><p>To warehouse partitions:</p><pre><code><code>platform=spotify/date=2024-01-15/hour=14/part1.gz
</code></code></pre><p>Our first approach was to process files sequentially, organizing by timestamp. Still too slow.</p><p>Then came the insight: &#8220;Wait, we have ~1.7M users here. What if we parallelize by user instead of by file?&#8221;</p><pre><code><code># Instead of processing 500M files sequentially:
for file in $(find . -name &#8220;*.json&#8221;); do
    organize_by_timestamp $file  # Still 500M operations!
done

# Process ~1.7M users in parallel:
find raw-data/ -maxdepth 2 -type d -name &#8220;user_*&#8221; | \\
    parallel -j 128 ./[organize-single-user.sh](&lt;http://organize-single-user.sh&gt;) {} /mnt/nvme/partitions
</code></code></pre><p>Each worker would:</p><ol><li><p>Take a user directory</p></li><li><p>Parse all their timestamp.json files</p></li><li><p>Route each file to the correct date/hour partition</p></li><li><p>Coordinate at the partition level to merge everything</p></li></ol><p>This reframed the problem completely. Instead of &#8220;how do we process 500M files?&#8221; it became &#8220;how do we process 1.7M users in parallel?&#8221;</p><p>One is hard. The other is Tuesday.</p><h3>Step 4: Process Partitions</h3><p>Now each partition could be processed independently:</p><pre><code><code># Each partition just worries about:
# 1. Collecting all files for that date/hour
# 2. Gzipping them efficiently
# 3. Creating warehouse-format records
find /mnt/nvme/partitions -name &#8220;file_list.txt&#8221; | \\
    parallel -j 128 --progress ./[process-single-partition.sh](&lt;http://process-single-partition.sh&gt;)
</code></code></pre><h3>Step 5: Upload and Snowpipe Magic</h3><pre><code><code># Upload to partitioned S3 structure
s5cmd --numworkers 4096 cp /mnt/nvme/output/* s3://raw-extractions-prod/data/
</code></code></pre><p>And here&#8217;s the beautiful part: once the files land in S3 with the correct partitioned structure (<code>platform=spotify/date=2024-01-15/hour=14/</code>), S3 notifications automatically trigger Snowpipe to ingest them into our data warehouse. No manual intervention. No complex orchestration. Just files appearing in the right place, and the rest happens automatically.</p><p><strong>It worked. Fast.</strong></p><p>And here&#8217;s the kicker: the entire 500M file migration took about a week. The same time it took our initial database approach to handle 11M files.</p><p>Let that sink in for a moment. We went from processing 11M files in a week to processing 500M files in a week. That&#8217;s a 45x improvement in efficiency, not just from throwing more hardware at the problem, but from completely reframing how we thought about it.</p><h2>What We Actually Learned</h2><h3>About Scale</h3><p>500 million files isn&#8217;t just &#8220;a lot of files.&#8221; It&#8217;s a scalability problem that broke our initial assumptions and iterations.</p><p>The overhead of &#8220;doing it right&#8221; - indexes, state management, proper abstractions - can exceed the actual work when you hit extreme scale. Sometimes the most elegant solution is the one that avoids the problem entirely.</p><h3>About Problem Reframing</h3><p>The breakthrough wasn&#8217;t better algorithms or more sophisticated architecture. It was changing the unit of work:</p><ul><li><p>&#10060; &#8220;How do we process 500M files efficiently?&#8221;</p></li><li><p>&#9989; &#8220;How do we process 1.7M users in parallel?&#8221;</p></li></ul><p>Sometimes the bottleneck isn&#8217;t compute power, it&#8217;s how you think about the problem.</p><h3>About Cloud Services</h3><p>AWS services are incredibly powerful, but they have limits. You just have to test them!</p><p>&#8220;Cloud-native&#8221; isn&#8217;t automatically better than &#8220;local processing with a big machine.&#8221; The right tool depends on your specific constraints and scale.</p><h3>About Engineering Pragmatism</h3><p>The best architecture is the one that ships. There&#8217;s real elegance in recognizing when your careful abstractions are getting in the way and just solving the actual problem.</p><p>Sometimes &#8220;just use a bigger machine&#8221; isn&#8217;t lazy engineering, sometimes it&#8217;s the sophisticated approach.</p><h2>The Real Lesson</h2><p>This isn&#8217;t a story about finding the &#8220;right&#8221; solution. It&#8217;s about recognizing when your assumptions break down and being willing to completely change approaches.</p><ul><li><p><strong>Approach 1 failed</strong> because we tried to apply normal database patterns to abnormal scale</p></li><li><p><strong>Approach 2 failed</strong> because we assumed cloud services could handle anything</p></li><li><p><strong>Approach 3 succeeded</strong> because we stopped trying to be clever and just solved the problem</p></li></ul><p>The most elegant engineering isn&#8217;t always in the architecture - sometimes it&#8217;s in knowing when to stop being architectural.</p><div><hr></div><p><em>Now our data warehouse hums along, ingesting new files automatically via Snowpipe, and our 500 million historical files are exactly where they need to be. Sometimes brute force is the sophisticated solution.</em></p><p><strong>Want to work on problems like this?</strong> We&#8217;re <a href="https://www.notion.so/Join-the-koodos-labs-team-f99d4eaab4a440ffb773e9535c8c1ab4?pvs=21">hiring widely</a> at <a href="https://koodos.com/">Koodos</a>. If this kind of scale, reframing, and pragmatism excites you, drop us a line at <strong><a href="mailto:team@koodos.com">team@koodos.com</a></strong>.</p>]]></content:encoded></item><item><title><![CDATA[Identity, authentication, and authorisation from the ground up]]></title><description><![CDATA[In an earlier post we learnt the basic concepts of identity, authentication, and authorisation &#8212; important terms that are easily confused by hackers and technologists.]]></description><link>https://blog.koodos.com/p/identity-authentication-and-authorisation-b7a</link><guid isPermaLink="false">https://blog.koodos.com/p/identity-authentication-and-authorisation-b7a</guid><dc:creator><![CDATA[Apurva Chitnis]]></dc:creator><pubDate>Sun, 27 Aug 2023 18:04:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!pJJL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In an <a href="https://apuchitnis.substack.com/p/demystifying-identity-authentication">earlier post</a> we learnt the basic concepts of identity, authentication, and authorisation &#8212; important terms that are easily confused by hackers and technologists.</p><p>In this post we will dive deeper and demystify how apps actually implement authentication. Do it right, and you barely notice it. But do it wrong, and you lock users out or open major security holes. </p><p>We'll see how apps authenticate you, from old school passwords to slick new standards like WebAuthn, and we&#8217;ll get our hands dirty discussing security and usability trade-offs, encryption, individual sovereignty, and more!</p><p>By the end, you'll have a solid grasp of how real-world apps tackle authentication using the methods we've covered.</p><p>Let's get started!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pJJL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pJJL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512 424w, https://substackcdn.com/image/fetch/$s_!pJJL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512 848w, https://substackcdn.com/image/fetch/$s_!pJJL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512 1272w, https://substackcdn.com/image/fetch/$s_!pJJL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pJJL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512" width="512" height="512" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:512,&quot;width&quot;:512,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!pJJL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512 424w, https://substackcdn.com/image/fetch/$s_!pJJL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512 848w, https://substackcdn.com/image/fetch/$s_!pJJL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512 1272w, https://substackcdn.com/image/fetch/$s_!pJJL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e57f49a-52a2-427d-8513-cfdedf7ed4e7_512x512 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Surreal digital landscape with keys, locks, and fingerprint symbols</em>, generated by Substack.</figcaption></figure></div><h1>Authentication &#129706;</h1><p>The first thing you do when you log into an app is identify yourself.</p><p>This might be using some kind of identifier &#8212; typically an email address or phone number &#8212; or logging in through Google, Facebook, or some other third party service, and using their knowledge of your identity instead.</p><p>Taking that identification and ensuring that it is yours is the process of authentication<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. Once you&#8217;re authenticated, you can use the app!</p><p>In the <a href="https://apuchitnis.substack.com/p/demystifying-identity-authentication">earlier post</a> we explained that authentication can be achieved in one of three different ways. You can demonstrate that you:</p><ul><li><p><em>know</em> something</p></li><li><p><em>own</em> something, or</p></li><li><p><em>are</em> something</p></li></ul><p>Only the person you're identifying as would know or own this information, or have that identity.</p><p>All modern apps use a technique that boils down to one of these three methods to authenticate you.</p><p>So let&#8217;s look at each in turn!</p><h2>Knowledge-based authentication &#129504;</h2><p>Password-based authentication uses the first of these authentication methods &#8212; it verifies that you <em>know</em> something only the user you're logging in as would <em>know</em>: their password, pin, passphrase, or any other secret shared between the user and the service.</p><p>To prevent attackers being able to access your account in case an app&#8217;s authentication data is leaked through a hack, the app doesn&#8217;t actually store the password, but a <em>hashed</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a> version of it.</p><p>Roughly speaking, the hash of your password is like an encrypted version of your password, except that given your password&#8217;s hash, it&#8217;s very hard (near-impossible, if hashed correctly) to retrieve the original password<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><h3>Limitations of passwords</h3><p>Implementing password-based authentication yourself is fraught with difficulty.</p><p>Some of the &#8220;fun&#8221; challenges you can anticipate are:</p><ul><li><p><strong>Properly implementing passwords</strong>&nbsp;&#8212; You need to manage salting, hashing, encryption and then store them securely in your database.</p></li><li><p><strong>Password recovery flows</strong>&nbsp;&#8212; You&#8217;ll also need to implement email-based password recovery. But what if your password-recovery emails go to spam? Having angry users contact customer support because they can't log in is not fun.</p></li><li><p><strong>Handle "password1@3"</strong>&nbsp;&#8212; You'll quickly realise that people like to use weak and easy-to-guess passwords.</p><p>This could allow hackers to find a password through a <em>dictionary attack</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a> &#8212; something they can do if they get access to a leaked version of the database, for example &#8212; before later gaining access to the running system.</p></li><li><p><strong>Block bots</strong> &#8212; Users also <em>really</em>&nbsp;like to reuse passwords<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>, so you might have hackers attacking your system because of a data breach on a completely different website. Blocking these hackers when they&#8217;re attacking your site from many different IP addresses is a major challenge<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p></li><li><p><strong>Additional friction</strong>&nbsp;- There will be quite a few users who won't have the patience to go through the password-recovery flows after forgetting their password. Instead, they just give up and go do something else. Forcing your user to remember something to log-in can add additional friction and increase churn.</p></li><li><p><strong>And much more</strong><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a></p></li></ul><p>Each of these steps is hard and requires scarce engineering resources, which is why authentication providers such as <a href="https://supabase.com/">Supabase</a>, <a href="https://auth0.com/">Auth0</a> and <a href="https://clerk.com/">Clerk</a> offer managed versions, at a cost.</p><p>But most significantly, users may forget their password and never even use your site in the first place!</p><p><strong>To summarise: passwords are a poor authentication mechanism. They&#8217;re hard to implement, unsafe, and lead to a poor user experience!</strong></p><p>So: is there a way to authenticate users without relying on something that only they would know?</p><p>I wouldn&#8217;t still be writing if there wasn&#8217;t, so let&#8217;s get into that!</p><h2>Ownership-based authentication &#128274;</h2><p>Ownership-based authentication is our second authentication method &#8212; it involves verifying that you <em>own</em> something that only the user you are attempting to log in as would <em>own</em>, such as their email inbox or phone number.</p><h3>One-time passwords</h3><p>For instance, when you log in, an app might send you a one-time password (OTP<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>) via email. This is typically a random short, server-generated, alphanumeric code like <code>ZXY1</code>, but it can also be a <em>magic link</em>, which is a URL containing the OTP as part of it.</p><p>The OTP is a temporary secret that is only shared between the app and the user's email inbox, is valid for that specific interaction, and is invalidated once used.</p><p>If you log in using this OTP, then you&#8217;ve successfully demonstrated ownership of the email address connected to the user account, and so you must be that user &#8212; you&#8217;re successfully authenticated!</p><p>SMS-based logins can also be used, but SMS are not encrypted and can easily be discovered by anyone on the network by sniffing for text messages<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>. </p><p>SMS-based logins can also be gamed for <a href="https://apuchitnis.substack.com/p/how-sms-fraud-works-and-how-to-guard-against-it">profit</a> &#8212; a big reason why many apps no longer solely rely on SMS for authentication, or even only offer SMS based authentication for premium or paying users.</p><p>But worst of all, SMS and emails may take minutes to be delivered, or may never be delivered at all<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>!</p><p>Is there a way for the user and service to coordinate on a shared, time-based secret, without sending it over an insecure, slow and unreliable communication channel?</p><p>I haven&#8217;t stopped writing, so yes!</p><h3>Time-based one-time passwords</h3><p>Authentication apps, such as Google Authenticator, provide <em>time-based</em> OTPs (TOTP<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>).</p><p>These authentication apps are set up well in advance of logging in, typically during the first onboarding session. Instead of using SMS or email to send the OTP to the user, an app on the user&#8217;s device generates a TOTP using the current time and a secret shared between the user and the web app.</p><p>This is much safer than SMS or email OTP delivery, as it does not rely on the security of external communication channels. The TOTP is generated locally on a device the user possesses and controls, making phishing and interception attacks much more difficult.</p><h3>Hardware authenticators</h3><p>Finally, hardware security keys, such as a YubiKey<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-12" href="#footnote-12" target="_self">12</a>, are a physical authentication device that a user can carry around with them. These are often used in high-security environments, such as in an enterprise, and can also create OTPs. These are really powerful devices, and we&#8217;ll dig into them more soon.</p><p>Whilst ownership-based authentication methods are often more secure, easier to use (no need to remember a password!), and easier to implement than passwords, they still require the user to do <em>something cumbersome</em> to authenticate.</p><p><strong>This is an opportunity to switch device, get distracted, </strong><em><strong>churn,</strong></em><strong> and never use your app at all!</strong></p><p>For this reason, authentication methods that require the user to switch device are used sparingly, only for sensitive operations such as when logging in on a new device for the first time.</p><p>So &#8212; can we authenticate a user without requiring them to use two devices? Absolutely!</p><h3>WebAuthn &#128376;&#65039;</h3><p>WebAuthn<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-13" href="#footnote-13" target="_self">13</a> is an awesome, new(ish) protocol that allows users to authenticate themselves whilst browsing the web.</p><p>Instead of using emails, passwords, YubiKeys, or OTPs, the information needed to authenticate a user is stored in an <em>authenticator</em> &#8212; typically the browser or device.</p><h4>Public-key cryptography</h4><p>The method used for authentication is public key cryptography. It&#8217;s the same technology used in Transport Layer Security (TLS) &#8212; a networking protocol which is used for nearly all traffic on the internet. It&#8217;s fundamental to security, so let&#8217;s take a quick aside to dig in:</p><p>The idea behind public-key cryptography, or asymmetric encryption, is pretty simple: each person has two keys &#8212; a public and private key. The public key is shared with everyone, and the private key is kept to themselves.</p><p>Seems pretty simple so far. But we can now do two <em>magical</em> things:</p><ul><li><p>Anyone can send the user a message by encrypting it with the user&#8217;s public key. Only the owner of the private key &#8212; the user &#8212; is able to decrypt that message.</p></li><li><p>The user can use their private key to <em>sign</em> a message. Anyone with the public key can verify that that user signed the message, and no one other than the user is able to sign it as that user.</p></li></ul><h4>Public-key cryptography in WebAuthn</h4><p>The browser or device, acting as an authenticator, <em>generates</em> a new public/private key pair as the user onboards onto a web service. The private key is kept hidden, potentially stored securely in a hardware security module (HSM<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-14" href="#footnote-14" target="_self">14</a>) on the device, and the public key is shared with the web service.</p><p>When the user later wishes to log in, the website asks the browser to <em>sign</em> a specific message. The browser or device does so using the private key, the website verifies it is indeed the same user using the previously shared public key, and the user is now authenticated!</p><blockquote><p><a href="https://webauthn.io">https://webauthn.io</a> has an awesome demo of this &#8212; try it out!</p></blockquote><p><strong>With WebAuthn, the user doesn&#8217;t need to type their username or remember a password </strong><em><strong>and</strong></em><strong> can login with a single click &#8212; very impressive!</strong></p><p>And excitingly, the prompt to log in with WebAuthn will only appear on the correct site, thereby preventing the phishing attacks that were possible with passwords &#127881;.</p><p>But logging in with a single click also makes it easy for friends, enemies, and attackers to log in with a single click, if they got close enough to your machine.</p><p>So, can we ensure it&#8217;s <em>you</em> actually doing the clicking? Surely!</p><h4>Multi-factor authentication</h4><p>The HSM can actually prompt the browser for <em>something</em> before it allows access to cryptographic operations, such as generating a public and private key or signing a message. And the browser can prompt the user for this data in turn.</p><p>The prompt can ask for a password &#8212; another example of knowledge-based authentication &#8212; or, even better, ask for <em>biometrics</em>, such as your fingerprint or face. Laptops and mobile phones commonly support fingerprint readers, so this is a fast, easy, and secure<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-15" href="#footnote-15" target="_self">15</a> way for users to authenticate.</p><p>Clicking a button to sign in and using your fingerprint is multi-factor authentication, since it requires <em>two</em> forms of authentication for you to log in: ownership of the device with the HSM and your inherent identity and ownership of your fingerprint.</p><p>And because you attest to your own identity, this is a form of <em>self-sovereign identity</em> &#8212; we&#8217;ll talk about this more in the next post.</p><h4>Cross-device authentication</h4><p>WebAuthn does pose a problem, though.</p><p>Users typically use a website on multiple devices: their phone, tablet, and laptop. It&#8217;s easy to share a password between devices &#8212; does WebAuthn offer something similar?</p><p>Yes! Well, sort of.</p><p>You can <em>delegate</em> the authentication to a roaming device &#8212; which could be your phone, laptop, or YubiKey &#8212; anything that is accessible via bluetooth, NFC, or USB. In turn, that device can sign the message on behalf of the main device.</p><p>This is an additional layer of protection, since malware on a site would need to infect an entirely different device than the main one you&#8217;re using to steal your private key &#8212; neat!</p><p>However, this means that we&#8217;d always be reliant on that first device for logging into that service, or we would need to set up a private key on <em>each</em> device we want to log in from &#8212; hardly a good user experience, and potentially worse than multi-factor login using passwords and OTPs.</p><h4>Limitations of WebAuthn</h4><p>WebAuthn isn&#8217;t a panacea, and does come with some notable downsides:</p><ul><li><p><strong>Single point of failure</strong> &#8212; If you lose a device, you&#8217;d lose access to <em>all</em> of your logins that that device stores the private keys for. Recreating those logins would take <em>ages</em>, and would be a massive pain!</p><p>Passkeys<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-16" href="#footnote-16" target="_self">16</a> are an exciting new feature to help plug this gap and to share private keys across devices, allowing for ease-of-access and easy recovery. The fun part is that Google<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-17" href="#footnote-17" target="_self">17</a> and Apple&#8217;s iCloud would sync these private keys for you &#8212; it&#8217;s built into the platform!</p><p>As excited as I am about Passkeys, they are new and controversial<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-18" href="#footnote-18" target="_self">18</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-19" href="#footnote-19" target="_self">19</a>, and it&#8217;s unclear if they&#8217;ll achieve mass adoption.</p></li><li><p><strong>Inflexibility &#8212;</strong> Passwords are flexible enough to support most flows that users need<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-20" href="#footnote-20" target="_self">20</a>. In contrast, WebAuthn only supports a subset of flows.</p><p>For example, sharing access to a single account (eg to access the bills of an energy provider) with a friend is straight-forward with passwords, but much tricker with WebAuthn.</p></li><li><p><strong>Uncertain adoption</strong> &#8212; Perhaps most important is that users aren&#8217;t familiar with the passwordless world.</p><p>Will they onboard? Will they log in again on a new device? Will they understand the user flow? We don&#8217;t know yet &#8212; and it&#8217;s a risk to try out something as innovative as this.</p></li></ul><p>Whilst I really want a passwordless future, and think WebAuthn and Passkeys could be our saviour, there&#8217;s one more form of authentication that is widely used and worth learning about.</p><h2>Identity-based authentication &#128587;</h2><p>The third way we can authenticate is to use something we <em>are</em> &#8212; our identity. Biometric authentication is one example of this, and we briefly covered it in the last section.</p><p>But we can also implement this by outsourcing identity verification to a third party that users already have an account with &#8212; an <em>identity</em> <em>provider</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-21" href="#footnote-21" target="_self">21</a>.</p><p>When you see &#8220;Login with Google&#8221; or &#8220;Login with Facebook&#8221;, that's identity-based authentication in action.</p><p>The identity provider tells the app that you are indeed the person you claim to be by providing it with a <em>signed</em> ID token.</p><p>When the app you&#8217;re signing into gets this token, it uses the signature to verify that the identity provider did indeed create that token &#8212; and now you&#8217;re fully authenticated!</p><h4>OpenID Connect</h4><p>OpenID Connect (OIDC<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-22" href="#footnote-22" target="_self">22</a>) is the underlying protocol used for this flow. OIDC is a layer on top of OAuth 2.0 &#8212; a protocol used for access delegation<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-23" href="#footnote-23" target="_self">23</a> &#8212; that standardises identity data like your name and email address.</p><p>Big tech giants like Google, Facebook, Twitter, and Microsoft all offer identity services that implement OIDC. It's easy and convenient for users since they can leverage their existing accounts.</p><p>And it saves a ton of headaches for developers since they no longer need to build secure authentication systems. Instead of authenticating a user yourself, just plug into Google, and you're done!</p><p>This is an example of <em>federated</em> identity<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-24" href="#footnote-24" target="_self">24</a>, where a user&#8217;s identity is used across multiple systems &#8212; Google, Facebook, and all the apps that rely on these identity providers &#8212; instead of just one.</p><h4>Limitations of identity federation</h4><p>However, identity federation has a fundamental flaw: the identity provider can revoke access at any time, instantly locking you out of all sites. You don&#8217;t <em>own</em> your identity &#8212; it&#8217;s stored on Google&#8217;s servers, not yours.</p><p>You may think it&#8217;s rare, but it&#8217;s surprisingly common<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-25" href="#footnote-25" target="_self">25</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-26" href="#footnote-26" target="_self">26</a> to find posts on Hacker News of users being denied access to their Google or Facebook accounts due to one reason or another, and totally unable to access any service that they used Google or Facebook as an identity provider for.</p><p>The quest for true user-owned identity is an active area of innovation which I'll cover in an upcoming post! Subscribe now to be updated when it&#8217;s released &#128071;</p><p>I would love to hear folks thoughts on this &#8212; feel free to comment below or <a href="https://twitter.com/apuchitnis">DM</a> me!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.koodos.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading the engineering blog on koodos! Subscribe for free to receive new posts and support our work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Have a great week all &#8212; stay tuned for Part 3!</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Authentication is in fact broader than simply validating your identity &#8212; it can refer to proving any assertion, such as your age being greater than some threshold, that you have a valid driving licence, or that you have enough funds in your bank.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/Cryptographic_hash_function">https://en.wikipedia.org/wiki/Cryptographic_hash_function</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>We can make the hash even harder to crack by adding salt to the original &#8212; random data that prevents attackers using pre-computed data to guess your original password.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/Dictionary_attack">https://en.wikipedia.org/wiki/Dictionary_attack</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>About 64% of people reuse at least one password across multiple services: <a href="https://auth0.com/blog/what-is-credential-stuffing">https://auth0.com/blog/what-is-credential-stuffing</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/Bot_prevention">https://en.wikipedia.org/wiki/Bot_prevention</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>There&#8217;s a few other reasons why passwords are challenging &#8212; and many of these are outside of your control as an app developer. Malware can log the keys the user presses whilst typing the password to secretly exfiltrate it, an enemy (or friend!) could watch over your shoulder as you type it in (this is known as <a href="https://en.wikipedia.org/wiki/Shoulder_surfing_(computer_security)">shoulder surfing</a>), and phishing attacks could prompt the user to enter their password when they&#8217;re on similarly branded but malicious website.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/One-time_password">https://en.wikipedia.org/wiki/One-time_password</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p><a href="https://security.stackexchange.com/questions/112111/capturing-text-messages-on-the-fly">https://security.stackexchange.com/questions/112111/capturing-text-messages-on-the-fly</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p><a href="https://messageiq.io/sms-deliverability">https://messageiq.io/sms-deliverability</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">https://en.wikipedia.org/wiki/Time-based_one-time_password</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-12" href="#footnote-anchor-12" class="footnote-number" contenteditable="false" target="_self">12</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/YubiKey">https://en.wikipedia.org/wiki/YubiKey</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-13" href="#footnote-anchor-13" class="footnote-number" contenteditable="false" target="_self">13</a><div class="footnote-content"><p><a href="https://webauthn.io">https://webauthn.io</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-14" href="#footnote-anchor-14" class="footnote-number" contenteditable="false" target="_self">14</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/Hardware_security_module">https://en.wikipedia.org/wiki/Hardware_security_module</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-15" href="#footnote-anchor-15" class="footnote-number" contenteditable="false" target="_self">15</a><div class="footnote-content"><p>Your biometrics are stored securely, and never leave your device &#8212; they&#8217;re only used to unlock the HSM used to store your private keys.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-16" href="#footnote-anchor-16" class="footnote-number" contenteditable="false" target="_self">16</a><div class="footnote-content"><p><a href="https://simplewebauthn.dev/docs/advanced/passkeys">https://simplewebauthn.dev/docs/advanced/passkeys</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-17" href="#footnote-anchor-17" class="footnote-number" contenteditable="false" target="_self">17</a><div class="footnote-content"><p><a href="https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password">https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-18" href="#footnote-anchor-18" class="footnote-number" contenteditable="false" target="_self">18</a><div class="footnote-content"><p><a href="https://auth0-com.cdn.ampproject.org/v/s/auth0.com/blog/amp/our-take-on-passkeys">https://auth0-com.cdn.ampproject.org/v/s/auth0.com/blog/amp/our-take-on-passkeys</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-19" href="#footnote-anchor-19" class="footnote-number" contenteditable="false" target="_self">19</a><div class="footnote-content"><p><a href="https://fy.blackhats.net.au/blog/2023-02-02-how-hype-will-turn-your-security-key-into-junk/">https://fy.blackhats.net.au/blog/2023-02-02-how-hype-will-turn-your-security-key-into-junk</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-20" href="#footnote-anchor-20" class="footnote-number" contenteditable="false" target="_self">20</a><div class="footnote-content"><p><a href="https://jrhawley.ca/2023/08/07/blocked-by-cloudflare">https://jrhawley.ca/2023/08/07/blocked-by-cloudflare</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-21" href="#footnote-anchor-21" class="footnote-number" contenteditable="false" target="_self">21</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/Identity_provider">https://en.wikipedia.org/wiki/Identity_provider</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-22" href="#footnote-anchor-22" class="footnote-number" contenteditable="false" target="_self">22</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC)">https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC)</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-23" href="#footnote-anchor-23" class="footnote-number" contenteditable="false" target="_self">23</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/OAuth#OAuth_2.0">https://en.wikipedia.org/wiki/OAuth#OAuth_2.0</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-24" href="#footnote-anchor-24" class="footnote-number" contenteditable="false" target="_self">24</a><div class="footnote-content"><p><a href="https://en.wikipedia.org/wiki/Federated_identity">https://en.wikipedia.org/wiki/Federated_identity</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-25" href="#footnote-anchor-25" class="footnote-number" contenteditable="false" target="_self">25</a><div class="footnote-content"><p><a href="http://zuckbannedme.com">http://zuckbannedme.com</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-26" href="#footnote-anchor-26" class="footnote-number" contenteditable="false" target="_self">26</a><div class="footnote-content"><p><a href="https://www.google.com/search?q=lost+access+google+account+site%3Anews.ycombinator.com">https://www.google.com/search?q=lost+access+google+account+site%3Anews.ycombinator.com</a>.</p></div></div>]]></content:encoded></item></channel></rss>