EVOLUTION-MANAGER
Edit File: managing-personal-access-tokens.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="generator" content="pandoc" /> <meta http-equiv="X-UA-Compatible" content="IE=EDGE" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Managing Personal Access Tokens</title> <script>// Pandoc 2.9 adds attributes on both header and div. We remove the former (to // be compatible with the behavior of Pandoc < 2.8). document.addEventListener('DOMContentLoaded', function(e) { var hs = document.querySelectorAll("div.section[class*='level'] > :first-child"); var i, h, a; for (i = 0; i < hs.length; i++) { h = hs[i]; if (!/^h[1-6]$/i.test(h.tagName)) continue; // it should be a header h1-h6 a = h.attributes; while (a.length > 0) h.removeAttribute(a[0].name); } }); </script> <style type="text/css"> code{white-space: pre-wrap;} span.smallcaps{font-variant: small-caps;} span.underline{text-decoration: underline;} div.column{display: inline-block; vertical-align: top; width: 50%;} div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} ul.task-list{list-style: none;} </style> <style type="text/css"> code { white-space: pre; } .sourceCode { overflow: visible; } </style> <style type="text/css" data-origin="pandoc"> pre > code.sourceCode { white-space: pre; position: relative; } pre > code.sourceCode > span { display: inline-block; line-height: 1.25; } pre > code.sourceCode > span:empty { height: 1.2em; } .sourceCode { overflow: visible; } code.sourceCode > span { color: inherit; text-decoration: inherit; } div.sourceCode { margin: 1em 0; } pre.sourceCode { margin: 0; } @media screen { div.sourceCode { overflow: auto; } } @media print { pre > code.sourceCode { white-space: pre-wrap; } pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; } } pre.numberSource code { counter-reset: source-line 0; } pre.numberSource code > span { position: relative; left: -4em; counter-increment: source-line; } pre.numberSource code > span > a:first-child::before { content: counter(source-line); position: relative; left: -1em; text-align: right; vertical-align: baseline; border: none; display: inline-block; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; padding: 0 4px; width: 4em; color: #aaaaaa; } pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; } div.sourceCode { } @media screen { pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; } } code span.al { color: #ff0000; font-weight: bold; } /* Alert */ code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */ code span.at { color: #7d9029; } /* Attribute */ code span.bn { color: #40a070; } /* BaseN */ code span.bu { } /* BuiltIn */ code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */ code span.ch { color: #4070a0; } /* Char */ code span.cn { color: #880000; } /* Constant */ code span.co { color: #60a0b0; font-style: italic; } /* Comment */ code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */ code span.do { color: #ba2121; font-style: italic; } /* Documentation */ code span.dt { color: #902000; } /* DataType */ code span.dv { color: #40a070; } /* DecVal */ code span.er { color: #ff0000; font-weight: bold; } /* Error */ code span.ex { } /* Extension */ code span.fl { color: #40a070; } /* Float */ code span.fu { color: #06287e; } /* Function */ code span.im { } /* Import */ code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */ code span.kw { color: #007020; font-weight: bold; } /* Keyword */ code span.op { color: #666666; } /* Operator */ code span.ot { color: #007020; } /* Other */ code span.pp { color: #bc7a00; } /* Preprocessor */ code span.sc { color: #4070a0; } /* SpecialChar */ code span.ss { color: #bb6688; } /* SpecialString */ code span.st { color: #4070a0; } /* String */ code span.va { color: #19177c; } /* Variable */ code span.vs { color: #4070a0; } /* VerbatimString */ code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */ </style> <script> // apply pandoc div.sourceCode style to pre.sourceCode instead (function() { var sheets = document.styleSheets; for (var i = 0; i < sheets.length; i++) { if (sheets[i].ownerNode.dataset["origin"] !== "pandoc") continue; try { var rules = sheets[i].cssRules; } catch (e) { continue; } var j = 0; while (j < rules.length) { var rule = rules[j]; // check if there is a div.sourceCode rule if (rule.type !== rule.STYLE_RULE || rule.selectorText !== "div.sourceCode") { j++; continue; } var style = rule.style.cssText; // check if color or background-color is set if (rule.style.color === '' && rule.style.backgroundColor === '') { j++; continue; } // replace div.sourceCode by a pre.sourceCode rule sheets[i].deleteRule(j); sheets[i].insertRule('pre.sourceCode{' + style + '}', j); } } })(); </script> <style type="text/css">body { background-color: #fff; margin: 1em auto; max-width: 700px; overflow: visible; padding-left: 2em; padding-right: 2em; font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.35; } #TOC { clear: both; margin: 0 0 10px 10px; padding: 4px; width: 400px; border: 1px solid #CCCCCC; border-radius: 5px; background-color: #f6f6f6; font-size: 13px; line-height: 1.3; } #TOC .toctitle { font-weight: bold; font-size: 15px; margin-left: 5px; } #TOC ul { padding-left: 40px; margin-left: -1.5em; margin-top: 5px; margin-bottom: 5px; } #TOC ul ul { margin-left: -2em; } #TOC li { line-height: 16px; } table { margin: 1em auto; border-width: 1px; border-color: #DDDDDD; border-style: outset; border-collapse: collapse; } table th { border-width: 2px; padding: 5px; border-style: inset; } table td { border-width: 1px; border-style: inset; line-height: 18px; padding: 5px 5px; } table, table th, table td { border-left-style: none; border-right-style: none; } table thead, table tr.even { background-color: #f7f7f7; } p { margin: 0.5em 0; } blockquote { background-color: #f6f6f6; padding: 0.25em 0.75em; } hr { border-style: solid; border: none; border-top: 1px solid #777; margin: 28px 0; } dl { margin-left: 0; } dl dd { margin-bottom: 13px; margin-left: 13px; } dl dt { font-weight: bold; } ul { margin-top: 0; } ul li { list-style: circle outside; } ul ul { margin-bottom: 0; } pre, code { background-color: #f7f7f7; border-radius: 3px; color: #333; white-space: pre-wrap; } pre { border-radius: 3px; margin: 5px 0px 10px 0px; padding: 10px; } pre:not([class]) { background-color: #f7f7f7; } code { font-family: Consolas, Monaco, 'Courier New', monospace; font-size: 85%; } p > code, li > code { padding: 2px 0px; } div.figure { text-align: center; } img { background-color: #FFFFFF; padding: 2px; border: 1px solid #DDDDDD; border-radius: 3px; border: 1px solid #CCCCCC; margin: 0 5px; } h1 { margin-top: 0; font-size: 35px; line-height: 40px; } h2 { border-bottom: 4px solid #f7f7f7; padding-top: 10px; padding-bottom: 2px; font-size: 145%; } h3 { border-bottom: 2px solid #f7f7f7; padding-top: 10px; font-size: 120%; } h4 { border-bottom: 1px solid #f7f7f7; margin-left: 8px; font-size: 105%; } h5, h6 { border-bottom: 1px solid #ccc; font-size: 105%; } a { color: #0033dd; text-decoration: none; } a:hover { color: #6666ff; } a:visited { color: #800080; } a:visited:hover { color: #BB00BB; } a[href^="http:"] { text-decoration: underline; } a[href^="https:"] { text-decoration: underline; } code > span.kw { color: #555; font-weight: bold; } code > span.dt { color: #902000; } code > span.dv { color: #40a070; } code > span.bn { color: #d14; } code > span.fl { color: #d14; } code > span.ch { color: #d14; } code > span.st { color: #d14; } code > span.co { color: #888888; font-style: italic; } code > span.ot { color: #007020; } code > span.al { color: #ff0000; font-weight: bold; } code > span.fu { color: #900; font-weight: bold; } code > span.er { color: #a61717; background-color: #e3d2d2; } </style> </head> <body> <h1 class="title toc-ignore">Managing Personal Access Tokens</h1> <div class="sourceCode" id="cb1"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(gh)</span></code></pre></div> <!-- This vignette uses a convention of "one sentence per line" in prose. --> <p>gh generally sends a Personal Access Token (PAT) with its requests. Some endpoints of the GitHub API can be accessed without authenticating yourself. But once your API use becomes more frequent, you will want a PAT to prevent problems with rate limits and to access all possible endpoints.</p> <p>This article describes how to store your PAT, so that gh can find it (automatically, in most cases). The function gh uses for this is <code>gh_token()</code>.</p> <p>More resources on PAT management:</p> <ul> <li>GitHub documentation on <a href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token">Creating a personal access token</a></li> <li>In the <a href="https://usethis.r-lib.org">usethis package</a>: <ul> <li>Vignette: <a href="https://usethis.r-lib.org/articles/articles/git-credentials.html">Managing Git(Hub) Credentials</a></li> <li><code>usethis::gh_token_help()</code> and <code>usethis::git_sitrep()</code> help you check if a PAT is discoverable and has suitable scopes</li> <li><code>usethis::create_github_token()</code> guides you through the process of getting a new PAT</li> </ul></li> <li>In the <a href="https://gitcreds.r-lib.org/">gitcreds package</a>: <ul> <li><code>gitcreds::gitcreds_set()</code> helps you explicitly put your PAT into the Git credential store</li> </ul></li> </ul> <div id="pat-and-host" class="section level2"> <h2>PAT and host</h2> <p><code>gh::gh()</code> allows the user to provide a PAT via the <code>.token</code> argument and to specify a host other than “github.com” via the <code>.api_url</code> argument. (Some companies and universities run their own instance of GitHub Enterprise.)</p> <div class="sourceCode" id="cb2"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">gh</span>(endpoint, ..., <span class="at">.token =</span> <span class="cn">NULL</span>, ..., <span class="at">.api_url =</span> <span class="cn">NULL</span>, ...)</span></code></pre></div> <p>However, it’s annoying to always provide your PAT or host and it’s unsafe for your PAT to appear explicitly in your R code. It’s important to make it <em>possible</em> for the user to provide the PAT and/or API URL directly, but it should rarely be necessary. <code>gh::gh()</code> is designed to play well with more secure, less fiddly methods for expressing what you want.</p> <p>How are <code>.api_url</code> and <code>.token</code> determined when the user does not provide them?</p> <ol style="list-style-type: decimal"> <li><code>.api_url</code> defaults to the value of the <code>GITHUB_API_URL</code> environment variable and, if that is unset, falls back to <code>"https://api.github.com"</code>. This is always done before worrying about the PAT.</li> <li>The PAT is obtained via a call to <code>gh_token(.api_url)</code>. That is, the token is looked up based on the host.</li> </ol> </div> <div id="the-gitcreds-package" class="section level2"> <h2>The gitcreds package</h2> <p>gh now uses the gitcreds package to interact with the Git credential store.</p> <p>gh calls <code>gitcreds::gitcreds_get()</code> with a URL to try to find a matching PAT. <code>gitcreds::gitcreds_get()</code> checks session environment variables and then the local Git credential store. Therefore, if you have previously used a PAT with, e.g., command line Git, gh may retrieve and re-use it. You can call <code>gitcreds::gitcreds_get()</code> directly, yourself, if you want to see what is found for a specific URL.</p> <div class="sourceCode" id="cb3"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>gitcreds<span class="sc">::</span><span class="fu">gitcreds_get</span>()</span></code></pre></div> <p>If you see something like this:</p> <div class="sourceCode" id="cb4"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co">#> <gitcreds></span></span> <span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="co">#> protocol: https</span></span> <span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="co">#> host : github.com</span></span> <span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="co">#> username: PersonalAccessToken</span></span> <span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="co">#> password: <-- hidden --></span></span></code></pre></div> <p>that means that gitcreds could get the PAT from the Git credential store. You can call <code>gitcreds_get()$password</code> to see the actual PAT.</p> <p>If no matching PAT is found, <code>gitcreds::gitcreds_get()</code> errors.</p> </div> <div id="pat-in-an-environment-variable" class="section level2"> <h2>PAT in an environment variable</h2> <p>If you don’t have a Git installation, or your Git installation does not have a working credential store, then you can specify the PAT in an environment variable. For <code>github.com</code> you can set the <code>GITHUB_PAT_GITHUB_COM</code> or <code>GITHUB_PAT</code> variable. For a different GitHub host, call <code>gitcreds::gitcreds_cache_envvar()</code> with the API URL to see the environment variable you need to set. For example:</p> <div class="sourceCode" id="cb5"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>gitcreds<span class="sc">::</span><span class="fu">gitcreds_cache_envvar</span>(<span class="st">"https://github.acme.com"</span>)</span> <span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="co">#> [1] "GITHUB_PAT_GITHUB_ACME_COM"</span></span></code></pre></div> </div> <div id="recommendations" class="section level2"> <h2>Recommendations</h2> <p>On a machine used for interactive development, we recommend:</p> <ul> <li><p>Store your PAT(s) in an official credential store.</p></li> <li><p>Do <strong>not</strong> store your PAT(s) in plain text in, e.g., <code>.Renviron</code>. In the past, this has been a common and recommended practice for pragmatic reasons. However, gitcreds/gh have now evolved to the point where it’s possible for all of us to follow better security practices.</p></li> <li><p>If you use a general-purpose password manager, like 1Password or LastPass, you may <em>also</em> want to store your PAT(s) there. Why? If your PAT is “forgotten” from the OS-level credential store, intentionally or not, you’ll need to provide it again when prompted.</p> <p>If you don’t have any other record of your PAT, you’ll have to get a new PAT whenever this happens. This is not the end of the world. But if you aren’t disciplined about deleting lost PATs from <a href="https://github.com/settings/tokens" class="uri">https://github.com/settings/tokens</a>, you will eventually find yourself in a confusing situation where you can’t be sure which PAT(s) are in use.</p></li> </ul> <p>On a headless system, such as on a CI/CD platform, provide the necessary PAT(s) via secure environment variables. Regular environment variables can be used to configure less sensitive settings, such as the API host. Don’t expose your PAT by doing something silly like dumping all environment variables to a log file.</p> <p>Note that on GitHub Actions, specifically, a personal access token is <a href="https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token">automatically available to the workflow</a> as the <code>GITHUB_TOKEN</code> secret. That is why many workflows in the R community contain this snippet:</p> <div class="sourceCode" id="cb6"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">env</span><span class="kw">:</span></span> <span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">GITHUB_PAT</span><span class="kw">:</span><span class="at"> ${{ secrets.GITHUB_TOKEN }}</span></span></code></pre></div> <p>This makes the automatic PAT available as the <code>GITHUB_PAT</code> environment variable. If that PAT doesn’t have the right permissions, then you’ll need to explicitly provide one that does (see link above for more).</p> </div> <div id="failure" class="section level2"> <h2>Failure</h2> <p>If there is no PAT to be had, <code>gh::gh()</code> sends a request with no token. (Internally, the <code>Authorization</code> header is omitted if the PAT is found to be the empty string, <code>""</code>.)</p> <p>What do PAT-related failures look like?</p> <p>If no PAT is sent and the endpoint requires no auth, the request probably succeeds! At least until you run up against rate limits. If the endpoint requires auth, you’ll get an HTTP error, possibly this one:</p> <pre><code>GitHub API error (401): 401 Unauthorized Message: Requires authentication</code></pre> <p>If a PAT is first discovered in an environment variable, it is taken at face value. The two most common ways to arrive here are PAT specification via <code>.Renviron</code> or as a secret in a CI/CD platform, such as GitHub Actions. If the PAT is invalid, the first affected request will fail, probably like so:</p> <pre><code>GitHub API error (401): 401 Unauthorized Message: Bad credentials</code></pre> <p>This will also be the experience if an invalid PAT is provided directly via <code>.token</code>.</p> <p>Even a valid PAT can lead to a downstream error, if it has insufficient scopes with respect to a specific request.</p> </div> <!-- code folding --> <!-- dynamically load mathjax for compatibility with self-contained --> <script> (function () { var script = document.createElement("script"); script.type = "text/javascript"; script.src = "https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"; document.getElementsByTagName("head")[0].appendChild(script); })(); </script> </body> </html>