loading...
Cover image for Github API Authentication - Username & Password (Basic)

Github API Authentication - Username & Password (Basic)

gr2m profile image Gregor Martynus ãƒŧ5 min read

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.

  1. 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
  2. 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

  1. OAuth: How to implement the OAuth web flow using a server and a client
  2. 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

Posted on by:

gr2m profile

Gregor Martynus

@gr2m

Father of triplets Nico, Ada & Kian. Web developer with 20+ yrs experience. JavaScript Octokit Maintainer for GitHub. He/him

Discussion

markdown guide
 

thanks, Gregor for this together <3