kanidm/stable/developers/designs/oauth2_refresh_tokens.html
2023-05-06 13:04:04 +00:00

341 lines
29 KiB
HTML

<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Oauth2 Refresh Tokens - Kanidm Administration</title>
<!-- Custom HTML head -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="shortcut icon" href="../../favicon.png">
<link rel="stylesheet" href="../../css/variables.css">
<link rel="stylesheet" href="../../css/general.css">
<link rel="stylesheet" href="../../css/chrome.css">
<link rel="stylesheet" href="../../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../../highlight.css">
<link rel="stylesheet" href="../../tomorrow-night.css">
<link rel="stylesheet" href="../../ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var html = document.querySelector('html');
var sidebar = null;
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded "><a href="../../intro.html"><strong aria-hidden="true">1.</strong> Introduction to Kanidm</a></li><li class="chapter-item expanded "><a href="../../installing_the_server.html"><strong aria-hidden="true">2.</strong> Installing the Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../choosing_a_domain_name.html"><strong aria-hidden="true">2.1.</strong> Choosing a Domain Name</a></li><li class="chapter-item expanded "><a href="../../prepare_the_server.html"><strong aria-hidden="true">2.2.</strong> Preparing for your Deployment</a></li><li class="chapter-item expanded "><a href="../../server_configuration.html"><strong aria-hidden="true">2.3.</strong> Server Configuration and Install</a></li><li class="chapter-item expanded "><a href="../../security_hardening.html"><strong aria-hidden="true">2.4.</strong> Platform Security Hardening</a></li><li class="chapter-item expanded "><a href="../../server_update.html"><strong aria-hidden="true">2.5.</strong> Server Updates</a></li></ol></li><li class="chapter-item expanded "><a href="../../client_tools.html"><strong aria-hidden="true">3.</strong> Client Tools</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../installing_client_tools.html"><strong aria-hidden="true">3.1.</strong> Installing client tools</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">Administration</li><li class="chapter-item expanded "><a href="../../administrivia.html"><strong aria-hidden="true">4.</strong> Administration</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../accounts_and_groups.html"><strong aria-hidden="true">4.1.</strong> Accounts and Groups</a></li><li class="chapter-item expanded "><a href="../../authentication.html"><strong aria-hidden="true">4.2.</strong> Authentication and Credentials</a></li><li class="chapter-item expanded "><a href="../../posix_accounts.html"><strong aria-hidden="true">4.3.</strong> POSIX Accounts and Groups</a></li><li class="chapter-item expanded "><a href="../../backup_restore.html"><strong aria-hidden="true">4.4.</strong> Backup and Restore</a></li><li class="chapter-item expanded "><a href="../../database_maint.html"><strong aria-hidden="true">4.5.</strong> Database Maintenance</a></li><li class="chapter-item expanded "><a href="../../domain_rename.html"><strong aria-hidden="true">4.6.</strong> Domain Rename</a></li><li class="chapter-item expanded "><a href="../../monitoring.html"><strong aria-hidden="true">4.7.</strong> Monitoring the platform</a></li><li class="chapter-item expanded "><a href="../../password_quality.html"><strong aria-hidden="true">4.8.</strong> Password Quality and Badlisting</a></li><li class="chapter-item expanded "><a href="../../recycle_bin.html"><strong aria-hidden="true">4.9.</strong> The Recycle Bin</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">Services</li><li class="chapter-item expanded "><a href="../../integrations/pam_and_nsswitch.html"><strong aria-hidden="true">5.</strong> PAM and nsswitch</a></li><li class="chapter-item expanded "><a href="../../ssh_key_dist.html"><strong aria-hidden="true">6.</strong> SSH Key Distribution</a></li><li class="chapter-item expanded "><a href="../../integrations/oauth2.html"><strong aria-hidden="true">7.</strong> Oauth2</a></li><li class="chapter-item expanded "><a href="../../integrations/ldap.html"><strong aria-hidden="true">8.</strong> LDAP</a></li><li class="chapter-item expanded "><a href="../../integrations/radius.html"><strong aria-hidden="true">9.</strong> RADIUS</a></li><li class="chapter-item expanded affix "><li class="part-title">Synchronisation</li><li class="chapter-item expanded "><a href="../../sync/concepts.html"><strong aria-hidden="true">10.</strong> Concepts</a></li><li class="chapter-item expanded "><a href="../../sync/freeipa.html"><strong aria-hidden="true">11.</strong> FreeIPA</a></li><li class="chapter-item expanded affix "><li class="part-title">Integration Examples</li><li class="chapter-item expanded "><a href="../../examples/k8s_ingress_example.html"><strong aria-hidden="true">12.</strong> Kubernetes Ingress</a></li><li class="chapter-item expanded "><a href="../../integrations/traefik.html"><strong aria-hidden="true">13.</strong> Traefik</a></li><li class="chapter-item expanded affix "><li class="part-title">Support</li><li class="chapter-item expanded "><a href="../../troubleshooting.html"><strong aria-hidden="true">14.</strong> Troubleshooting</a></li><li class="chapter-item expanded "><a href="../../frequently_asked_questions.html"><strong aria-hidden="true">15.</strong> Frequently Asked Questions</a></li><li class="chapter-item expanded "><a href="../../glossary.html"><strong aria-hidden="true">16.</strong> Glossary of Technical Terms</a></li><li class="chapter-item expanded affix "><li class="part-title">For Developers</li><li class="chapter-item expanded "><a href="../../DEVELOPER_README.html"><strong aria-hidden="true">17.</strong> Developer Guide</a></li><li class="chapter-item expanded "><a href="../../developers/faq.html"><strong aria-hidden="true">18.</strong> FAQ</a></li><li class="chapter-item expanded "><div><strong aria-hidden="true">19.</strong> Design Documents</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../developers/designs/access_profiles_rework_2022.html"><strong aria-hidden="true">19.1.</strong> Access Profiles 2022</a></li><li class="chapter-item expanded "><a href="../../developers/designs/access_profiles_and_security.html"><strong aria-hidden="true">19.2.</strong> Access Profiles Original</a></li><li class="chapter-item expanded "><a href="../../developers/designs/rest_interface.html"><strong aria-hidden="true">19.3.</strong> REST Interface</a></li><li class="chapter-item expanded "><a href="../../developers/designs/elevated_priv_mode.html"><strong aria-hidden="true">19.4.</strong> Elevated Priv Mode</a></li><li class="chapter-item expanded "><a href="../../developers/designs/oauth2_refresh_tokens.html" class="active"><strong aria-hidden="true">19.5.</strong> Oauth2 Refresh Tokens</a></li></ol></li><li class="chapter-item expanded "><a href="../../developers/python.html"><strong aria-hidden="true">20.</strong> Python Module</a></li><li class="chapter-item expanded "><a href="../../developers/radius.html"><strong aria-hidden="true">21.</strong> RADIUS Integration</a></li><li class="chapter-item expanded "><a href="../../packaging.html"><strong aria-hidden="true">22.</strong> Packaging</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../packaging_debs.html"><strong aria-hidden="true">22.1.</strong> Debian/Ubuntu</a></li></ol></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Kanidm Administration</h1>
<div class="right-buttons">
<a href="../../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/kanidm/kanidm" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://github.com/kanidm/kanidm/edit/master/book/src/developers/designs/oauth2_refresh_tokens.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="oauth2-refresh-tokens"><a class="header" href="#oauth2-refresh-tokens">Oauth2 Refresh Tokens</a></h1>
<p>Due to how Kanidm authentication sessions were originally implemented they had short session times
(1 hour) due to the lack of privilege separation in tokens. Now with privilege separation being
implemented session lengths have been extended to 8 hours with possible increases in the future.</p>
<p>However, this leaves us with an issue with oauth2 - oauth2 access tokens are considered valid until
their expiry and we should not issue tokens with a validity of 8 hours or longer since that would
allow rogue users to have a long window of usage of the token before they were forced to re-auth. It
also means that in the case that an account must be forcefully terminated then the user would retain
access to applications for up to 8 hours or more.</p>
<p>To prevent this, we need oauth2 tokens to &quot;check in&quot; periodically to re-afirm their session
validity.</p>
<p>This is performed with access tokens and refresh tokens. The access token has a short lifespan
(proposed 15 minutes) and must be refreshed with Kanidm which can check the true session validity
and if the session has been revoked. This creates a short window for revocation to propagate to
oauth2 applications since each oauth2 application must periodically check in to keep their access
token alive.</p>
<h2 id="risks"><a class="header" href="#risks">Risks</a></h2>
<p>Refresh tokens are presented to the relying server where they receive an access token and an
optional new refresh token. Because of this, it could be possible to present a refresh token
multiple times to proliferate extra refresh and access tokens away from the system. Preventing this
is important to limit where the tokens are used and monitor and revoke them effectively.</p>
<p>In addition, old refresh tokens should not be able to be used once exchanged, they should be &quot;at
most once&quot;. If this is not enforced then old refresh tokens can be used to gain access to sessions
even if the associated access token was expired by many hours and it's refresh token was already
used.</p>
<p>This is supported by
<a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.2.2">draft oauth security topics section 2.2.2</a>
and
<a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#refresh_token_protection">draft oauth security topics refresh token protection</a></p>
<p>Refresh tokens must only be used by the client application associated. Kanidm strictly enforces this
already with our client authorisation checks. This is discussed in
<a href="https://www.rfc-editor.org/rfc/rfc6749#section-10.4">rfc6749 section 10.4</a>.</p>
<h2 id="design"><a class="header" href="#design">Design</a></h2>
<pre><code> ┌─────────────────────────────────────────┐
│Kanidm │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ Session │ 3. Update │ Session │ │
│ │ NIB 1 │─────NIB───────▶│ NIB 2 │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ └─────────┘ └─────────┘ │
│ │ │ │
└───┼───────────────────────────┼─────────┘
┌────┘ ▲ ┌────┘
│ │ │
│ │ │
1. Issued │ 4. Issued
│ │ │
│ │ │
│ │ │
▼ │ ▼
┌───────┐ │ ┌───────┐
│ │ │ │ │
│Access │ │ │Access │
│ + │ │ │ + │
│Refresh│──2. Refresh──┘ │Refresh│
│ IAT 1 │ │ IAT 2 │
│ │ │ │
└───────┘ └───────┘
</code></pre>
<p>In this design we associate a &quot;not issued before&quot; (NIB) timestamp to our sessions. For a refresh
token to be valid for issuance, the refresh tokens IAT must be greater than or equal to the NIB.</p>
<p>In this example were the refresh token with IAT 1 re-used after the second token was issued, then
this condition would fail as the NIB has advanced to 2. Since IAT 1 is not greater or equal to NIB 2
then the refresh token <em>must</em> have previously been used for access token exchange.</p>
<p>In a replicated environment this system is also stable and correct even if a session update is
missed.</p>
<pre><code> 2.
┌───────────────────────Replicate────────────────┐
│ │
│ │
┌───────┼─────────────────────────────────┐ ┌──────┼──────────────────────────────────┐
│Kanidm │ │ │Kanidm│ │
│ │ │ │ ▼ │
│ ┌─────────┐ ┌─────────┐ │ │ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ Session │ 4. Update │ Session │ │ │ │ Session │ 7. Update │ Session │ │
│ │ NIB 1 │─────NIB───────▶│ NIB 2 │ │ │ │ NIB 1 │ ─────NIB───────▶│ NIB 3 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ │ │ └─────────┘ └─────────┘ │
│ │ │ │ │ ▲ │ │
└───┼───────────────────────────┼─────────┘ └──────┼────────────────────────┼─────────┘
┌────┘ ▲ ┌────┘ │ ┌────┘
│ │ │ │ │
│ │ │ │ │
1. Issued │ 5. Issued │ 8. Issued
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
▼ │ ▼ │ ▼
┌───────┐ │ ┌───────┐ │ ┌───────┐
│ │ │ │ │ │ │ │
│Access │ │ │Access │ │ │Access │
│ + │ │ │ + │ │ │ + │
│Refresh│──3. Refresh──┘ │Refresh│ │ │Refresh│
│ IAT 1 │ │ IAT 2 │─────6. Refresh──────────┘ │ IAT 3 │
│ │ │ │ │ │
└───────┘ └───────┘ └───────┘
</code></pre>
<p>In this example, we can see that the replication of the session with NIB 1 happens to the second
Kanidm server, but the replication of session with NIB 2 has not occurred yet. If the token that was
later issued with IAT 2 was presented to the second server it would still be valid and able to
refresh since IAT 2 is greater or equal to NIB 1. This would also prompt the session to advance to
NIB 3 such that when replication begun again, the session with NIB 3 would take precedence over the
former NIB 2 session.</p>
<p>While this allows a short window where a former access token could be used on the second replica,
this infrastructure being behind load balancers and outside of an attackers influence significantly
hinders the ability to attack this for very little gain.</p>
<h2 id="attack-detection"><a class="header" href="#attack-detection">Attack Detection</a></h2>
<p><a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2">draft oauth security topics section 4.14.2</a>
specifically calls out that when refresh token re-use is detected then all tokens of the session
should be canceled to cause a new authorisation code flow to be initiated.</p>
<h2 id="inactive-refresh-tokens"><a class="header" href="#inactive-refresh-tokens">Inactive Refresh Tokens</a></h2>
<p>Similar
<a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2">draft oauth security topics section 4.14.2</a>
also discusses that inactive tokens should be invalidated after a period of time. From the view of
the refresh token this is performed by an internal exp field in the encrypted refresh token.</p>
<p>From the servers side we will require a &quot;not after&quot; parameter that is updated on token activity.
This will also require inactive session cleanup in the server which can be extended into the session
consistency plugin that already exists.</p>
<p>Since the act of refreshing a token is implied activity then we do not require other signaling
mechanisms.</p>
<h1 id="questions"><a class="header" href="#questions">Questions</a></h1>
<p>Currently with authorisation code grants and sessions we issue these where the sessions are recorded
in an async manner. For consistency I believe the same should be true here but is there a concern
with the refresh being issued but a slight delay before it's recorded? I think given the nature of
our future replication we already have to consider the async/eventual nature of things, so this
doesn't impact that further, and may just cause client latency in the update process.</p>
<p>However, we also don't want a situation where our async/delayed action queues become too full or
overworked. Maybe queue monitoring/backlog issues are a separate problem though.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../../developers/designs/elevated_priv_mode.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../../developers/python.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../../developers/designs/elevated_priv_mode.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../../developers/python.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="../../elasticlunr.min.js"></script>
<script src="../../mark.min.js"></script>
<script src="../../searcher.js"></script>
<script src="../../clipboard.min.js"></script>
<script src="../../highlight.js"></script>
<script src="../../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>