Today, on February 14, 2020, GitHub announced its deprecation timeline for authenticating using a username and a password. Which means you have only time until November 13, 2020 to give this a try 🤪
I have mixed feelings about the deprecation. It makes me happy because Basic authentication has all kinds of security problems. Adding two-factor authentication made it a bit more secure, but also a pain in the 🍑
I'm sad, because I created @octokit/auth-basic
to hide away most of the complexities introduced by two-factor authentication, and I think it turned out pretty nicely 😭 I think it's a good example of what an API client library can do to hide away complexities from consumers of that API.
So, for the history books, let's see how to use @octokit/auth-basic
to authenticate using username, password, and two-factor authentication.
How Basic authentication works for the GitHub API
Let's try to send a request to GitHubs GET /user
API using the @octokit/request
package.
// my-cli.js
const { request } = require("@octokit/request");
const USERNAME = "octocat";
const PASSWORD = "secret";
request("GET /user", {
headers: {
authorization: `basic ${Buffer.from(`${USERNAME}:${PASSWORD}`).toString(
"base64"
)}`
}
}).then(response => console.log(response.data), console.error);
Depending on your GitHub security Settings, the above code will either log the user object for your account, or it will fail with a 401
response, including a 'X-GitHub-Otp' header with the value set to required; app
or required; sms
.
In order to retrieve your user account, you will need to send the same request again, including a header containing the OTP.
By the way, OTP stands for one-time password. In GitHub's case, you can use the OTP more than once, because it is actually a time-based password. It usually is valid for about a minute. 🤷♂️
If you use an authenticator app (you should!), you already know the right OTP to send along, the request code looks like this
// my-cli.js
const { request } = require("@octokit/request");
const USERNAME = "octocat";
const PASSWORD = "secret";
const OTP = "012345";
request("GET /user", {
headers: {
authorization: `basic ${Buffer.from(`${USERNAME}:${PASSWORD}`).toString(
"base64"
)}`,
"x-github-otp": OTP
}
}).then(response => console.log(response.data), console.error);
If you have SMS setup for your two-factor authentication (you should not!), then you are out of luck. Not only do you not know the OTP at the time of the first request, you won't even receive an SMS with an OTP from GitHub. Why? Because only certain REST API routes trigger the SMS delivery. The OAuth Authorizations API routes, to be precise.
In order to workaround this problem, the recommend best practice is to not use basic authentication for every request. Instead, use it to create a personal access token, then use that token for the following requests.
And because you create a personal access token that you probably won't need ever again, it's a good practice to delete that token when you are done. However, the OTP you used to create the token might no longer be valid (time based, remember), so it's well possible that GitHub will respond with a 401
asking for a new OTP.
You can see, this is getting complicated pretty quick, and it's only the tip of the ice berg. For example, some requests require to be authenticated using your username and password, while for most others you can use the token. If you are curious, you can read trough the source code of @octokit/auth-basic
to learn all about it. The tests will give you a pretty good summary.
🎩 @octokit/basic-auth
@octokit/basic-auth
takes away most of the pain that is Basic Auth and two-factor authentication for GitHub's REST API. It even integrates neatly with your favorite Octokit libraries such as @octokit/rest
, @octokit/core
or even the super low-level @octokit/request
.
In this example I'll use @octokit/basic-auth
, @octokit/request
and readline-sync
// my-cli.js
const { createBasicAuth } = require("@octokit/auth-basic");
const { request } = require("@octokit/request");
const { question } = require("readline-sync");
const auth = createBasicAuth({
username: question("Username: "),
password: question("Password: "),
async on2Fa() {
// prompt user for the one-time password retrieved via SMS or authenticator app
return question("Two-factor authentication Code: ");
}
});
const requestWithBasicAuth = request.defaults({
request: {
hook: auth.hook
}
});
requestWithBasicAuth("GET /user").then(
response => console.log(response.data),
console.error
);
When you run the above code with Node, you will be prompted for your username and password. If you have two-factor auth setup and SMS configured for delivery, you will receive an SMS with the OTP. Once you enter the OTP the script will log the user object for your GitHub Account to your terminal.
Now lets say you need to send so many requests that the OTP becomes invalid (usually about a minute), but you still want to delete the personal access token at the end. The code would look something like this
// my-cli.js
const { createBasicAuth } = require("@octokit/auth-basic");
const { request } = require("@octokit/request");
const { question } = require("readline-sync");
run();
async function run() {
const auth = createBasicAuth({
username: question("Username: "),
password: question("Password: "),
async on2Fa() {
// prompt user for the one-time password retrieved via SMS or authenticator app
return question("Two-factor authentication Code: ");
}
});
const requestWithBasicAuth = request.defaults({
request: {
hook: auth.hook
}
});
const { data } = await requestWithBasicAuth("GET /user");
console.log(`Your GitHub Account ID: ${data.id}`);
console.log(`Sending some more requests that take a while ...`);
const TWO_MINUTES_IN_MS = 2 * 60 * 1000;
await new Promise(resolve => setTimeout(resolve, TWO_MINUTES_IN_MS));
const { id } = await auth({ type: "token" });
await requestWithBasicAuth("DELETE /authorizations/:authorization_id", {
authorization_id: id
});
console.log("TOKEN deleted");
}
The code above has a two minute timeout build in to make sure the OTP becomes invalid. You will see that you will get prompted for an OTP for the 2nd time:
$ node my-cli.js
Username: gr2m
Password: ***
Two-factor authentication Code: 068194
Your GitHub Account ID: 39992
Sending some more requests that take a while ...
Two-factor authentication Code: 975808
TOKEN deleted
What are the alternatives to Basic authentication
Well, the Basic authentication party is over soon, so make sure to use alternative means of authentication before November 2020.
You can do one of two things.
- Ask your users to create a personal access token and share that with you. I wrote a blog post with more details about that: GitHub API Authentication - Personal Access Tokens
- Use GitHub's OAuth web application flow.
Now 2.
is a nicer user experience, but it's easier said that done. Luckily, I have two blog posts lined up that will help you
-
OAuth
: How to implement the OAuth web flow using a server and a client -
CLI
: How to use the OAuth web flow for CLI apps.
Stay tuned 💐
Credit
Header image: Women Code in Tech Chat CC BY 2.0
Top comments (2)
Thanks for this. Still waiting for that OAuth tutorial :)
thanks, Gregor for this together <3