<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Auth | 2i2c</title><link>https://2i2c.org/tag/auth/</link><atom:link href="https://2i2c.org/tag/auth/index.xml" rel="self" type="application/rss+xml"/><description>Auth</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Wed, 26 Aug 2026 00:00:00 +0000</lastBuildDate><image><url>https://2i2c.org/media/sharing.png</url><title>Auth</title><link>https://2i2c.org/tag/auth/</link></image><item><title>Building a Canvas authentication helper library for JupyterHub</title><link>https://2i2c.org/blog/building-canvas-auth/</link><pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate><guid>https://2i2c.org/blog/building-canvas-auth/</guid><description>&lt;p>2i2c have been working with the &lt;a href="https://2i2c.org/collaborators/utoronto/" >University of Toronto&lt;/a> to add support for Canvas as a supported identity provider for JupyterHub. Out of the box, JupyterHub implements support for &lt;em>authenticating&lt;/em> users (figuring out who they are) and &lt;em>authorizing&lt;/em> users (figuring out whether they are permitted to log into the hub) using an &lt;code>Authenticator&lt;/code> interface. Via the separate &lt;a href="https://oauthenticator.readthedocs.io/en/latest/" target="_blank" rel="noopener" >&lt;code>oauthenticator&lt;/code> package&lt;/a>, JupyterHub has support for many kinds of OAuth2-based Identity Providers such as Google or GitHub, in addition to providing a &lt;em>generic&lt;/em> authenticator &lt;code>GenericOAuthenticator&lt;/code> that can be used with any OAuth2 implementation.&lt;/p>
&lt;figure id="figure-diagram-of-an-oauth-flow-during-the-authentication-process-taken-from-the-oauthenticator-documentationhttpsoauthenticatorreadthedocsioenlatest_imagesjupyterhub-oauth-external-flowpng">
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Diagram of an OAuth flow during the authentication process, taken from [the oauthenticator documentation](https://oauthenticator.readthedocs.io/en/latest/_images/JupyterHub-OAuth-external-flow.png)." srcset="
/blog/building-canvas-auth/oauth-flow_hu0c8a8127bb8c611237d98151170d7a50_76835_3b90dd686be44dc5e5271fe8b6a74d81.webp 400w,
/blog/building-canvas-auth/oauth-flow_hu0c8a8127bb8c611237d98151170d7a50_76835_b76b2c9bead95c9862c4df0ce3110d69.webp 760w,
/blog/building-canvas-auth/oauth-flow_hu0c8a8127bb8c611237d98151170d7a50_76835_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://2i2c.org/blog/building-canvas-auth/oauth-flow_hu0c8a8127bb8c611237d98151170d7a50_76835_3b90dd686be44dc5e5271fe8b6a74d81.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Diagram of an OAuth flow during the authentication process, taken from &lt;a href="https://oauthenticator.readthedocs.io/en/latest/_images/JupyterHub-OAuth-external-flow.png" target="_blank" rel="noopener" >the oauthenticator documentation&lt;/a>.
&lt;/figcaption>&lt;/figure>
&lt;p>Our friends at UC Berkeley maintain &lt;a href="https://github.com/berkeley-dsep-infra/canvasoauthenticator" target="_blank" rel="noopener" >the &lt;code>CanvasOAuthenticator&lt;/code> project&lt;/a>, which has performed authentication for thousands of students over the last seven years. The project provides an implementation of &lt;code>GenericOAuthenticator&lt;/code>, which queries the Canvas REST API to determine user &lt;em>identity&lt;/em> (e.g. their email address), and their &lt;em>authorization&lt;/em> (e.g. the courses that the user is enrolled in). In the time since &lt;code>CanvasOAuthenticator&lt;/code> was first written (with involvement from 2i2c members), the &lt;code>GenericOAuthenticator&lt;/code> has seen significant improvements, such that it is now possible to implement Canvas authentication without maintaining an entirely separate &lt;code>GenericOAuthenticator&lt;/code> subclass.&lt;/p>
&lt;p>After recognizing that it is much easier to maintain a small set of OAuth2 rules (adaptors) for each identity provider (like Canvas) rather than full-blown authenticators, 2i2c built &lt;a href="https://2i2c.org/jupyterhub-oauthenticator-authz-helpers/" target="_blank" rel="noopener" >&lt;code>jupyterhub_oauthenticator_authz_helpers&lt;/code>&lt;/a>. This library maintains an implementation of a subset of UC Berkeley&amp;rsquo;s &lt;code>CanvasOAuthenticator&lt;/code> rules for mapping Canvas courses and groups onto JupyterHub groups. With this work, adding Canvas authentication requires only a few additional lines:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">jupyterhub_oauthenticator_authz_helpers.canvas&lt;/span> &lt;span class="n">build_auth_urls&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">GenericOAuthenticator&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">canvas_url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;CANVAS-URL&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Configure auths&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">authorize_url&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token_url&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">userdata_url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">build_auth_urls&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">canvas_url&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Scopes that this token will need&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="n">build_auth_urls&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">scopes&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Adding support for group population (from Canvas groups and courses) requires a little bit more config:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">jupyterhub_oauthenticator_authz_helpers.canvas&lt;/span> &lt;span class="n">get_user_groups&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">get_course_groups&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">build_auth_urls&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Define a custom key for auth groups&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">auth_state_groups_key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;custom-groups&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Define and inject auth state&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">auth_state_hook&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">authenticator&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">auth_state&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">auth_state&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">access_token&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">auth_state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">auth_state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">authenticator&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">auth_state_groups_key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Populate groups from Canvas courses, using the scheme defined in get_course_groups&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">*&lt;/span>&lt;span class="k">await&lt;/span> &lt;span class="n">get_course_groups&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">canvas_url&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">access_token&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Populate groups from Canvas groups, using the scheme defined in get_user_groups&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">*&lt;/span>&lt;span class="k">await&lt;/span> &lt;span class="n">get_user_groups&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">canvas_url&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">access_token&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">auth_state&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">modify_auth_state_hook&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">auth_state_hook&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Request additional scopes for the course / user group fetching&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="n">build_auth_urls&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">scopes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">get_user_groups&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">scopes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">get_course_groups&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">scopes&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In this approach, authentication and authorization are &lt;em>composed&lt;/em> — each primitive that represents an operation that requires a set of permissions possesses a &lt;code>.scopes&lt;/code> attribute that can be added to the requested token scopes, e.g. &lt;code>get_course_groups&lt;/code> or &lt;code>build_auth_urls&lt;/code>. Hub administrators that only wish to authorize users by course groups do not need to use &lt;code>get_user_groups&lt;/code> or request its &lt;code>.scopes&lt;/code>. In order to prove the generality of this approach, 2i2c have added a Mastodon implementation of these authentication and authorization primitives that restricts hub access to those users which are followed by a particular &lt;em>authorizing user&lt;/em>.&lt;/p>
&lt;p>In addition to building out this library, 2i2c hopes to foster collaboration with other users and maintainers to continue to upstream additional OAuth2 adaptors. As part of the funded initiative, 2i2c will spend time conducting governance and community work upstream to establish a &lt;a href="https://github.com/jupyterhub-contrib" target="_blank" rel="noopener" >jupyterhub-contrib&lt;/a> organization that provides stewardship and maintenance of this work, and future third-party projects.&lt;/p>
&lt;h2 id="acknowledgements">
Acknowledgements
&lt;a class="header-anchor" href="#acknowledgements">#&lt;/a>
&lt;/h2>&lt;ul>
&lt;li>Thanks to the &lt;a href="https://2i2c.org/collaborators/utoronto/" >University of Toronto&lt;/a> for collaborating with us, and funding this work.&lt;/li>
&lt;li>Thanks to UC Berkeley for the prior work that inspired this project.&lt;/li>
&lt;/ul></description></item></channel></rss>