Cotter just launched a Github Login integration 🎉. This means you can easily log your users in and get an access token to enable Github integrations in your app.

What we're building

We're going to build a website with Next.js that allows your users to login with email or with Github and get a list of their public and private repositories.

Sign in with Github using Cotter and Next.js to use Github API

Overview

Let's Start – Make our Home Page

Create your Next.js project

Start with creating a new Next.js project by running the code below, and follow the instructions.

yarn create next-app

Add a Login Form in the Home Page

We're using Cotter for the Login form to quickly enable an Email Magic Link login and Sign in with Github.

Add Cotter as a dependency

yarn add cotter

Add a Login Form and a Title

Modify our home page at pages/index.js. We'll start with the simple Email Magic Link login. Remove everything in pages/index.js and add a title and Cotter's login form:

import { useEffect } from "react";
import styles from "../styles/Home.module.css";
import Cotter from "cotter"; //  1️⃣  Import Cotter
import { useRouter } from "next/router";

export default function Home() {
  const router = useRouter();
  //  2️⃣ Initialize and show the form
  useEffect(() => {
    var cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
    cotter
      .signInWithLink() // use .signInWithOTP() to send an OTP
      .showEmailForm() // use .showPhoneForm() to send magic link to a phone number
      .then((response) => {
        console.log(response); // show the response
        router.push("/dashboard");
      })
      .catch((err) => console.log(err));
  }, []);

  return (
      <div className={styles.container}>
        <h1 className={styles.subtitle}>Welcome to my Github App</h1>

        {/*  3️⃣  Put a <div> that will contain the form */}
        <div id="cotter-form-container" style={{ width: 300, height: 300 }} />
      </div>
  );
}
pages/index.js

You'll need an API_KEY_ID, create a new project, and copy the API_KEY_ID from the dashboard. The code above should give you a simple Login page that looks like this:

Enable Github Login

The documentation laid out the steps you need to take to enable Social Login to your login form. We'll follow it step by step below:

First, create a Github OAuth App. Summarizing Github's documentation, you should do the following:

  • Click on your profile picture on the top right > Settings > Developer Settings > OAuth Apps > New OAuth App
  • Fill in your Application Name, Homepage URL, and description based on your app.
  • Fill in  https://www.cotter.app/api/v0/oauth/token/callback/GITHUB for the Authorization callback URL.
  • Then click Register Application.

Go to your Cotter Dashboard and add a Social Login connection.

Go to Dashboard > Social Login > New Login Connection > Github. Then copy your Client ID and Client Secret from Github. We'll add the repo scope because we want to get the users' repository data.

Add the Client ID, Client Secret, and repo scope

Press Create to create the login connection.

Show Github Login on your Form

Now that your Social Login connection is set up, we can show it in our Login Form. Go to Dashboard > Branding > Magic Link. Check the box for Github under Social Login Providers.

Add Github Login to your Form

Press Save to update your customization.

You should now see the Sign in with Github button in our Next.js app.

Sign in with Github enabled on Cotter Login Form

Let's see how this works before moving on to Github API

We'll go over how the login works, how you can authenticate users to your backend, and how you can get Github's access token to access private repo data.

1. Let's try logging in with your email address first.

Enter your email address and press "Sign in Without Password". Tap the magic link in your email and you should be logged-in.

Now check your console log, you should see something like this:

{
  "token": {...},
  "email": "[email protected]",  // 👈 the user's email
  "oauth_token": {
    "access_token": "eyJhbGciOiJFUzI...",   // 👈  access token
    "id_token": "eyJhbGciOiJFUzI1...",
    "refresh_token": "236:QDVxW6...",
    "expires_in": 3600,
    "token_type": "Bearer",
    "auth_method": "OTP"
  },
  "user": {
    "ID": "abcdefgh-abcd-abcd-9959-67ebae3cdfcf",   // 👈  user ID
    "issuer": "abcdefgh-abcd-abcd-81ad-5cc8b69051e8",
    "identifier": "[email protected]",
    ...
  }
}
Note: If your console log can't be expanded because you redirected to /dashboard, comment out the redirection part router.push("/dashboard")

Three things that you should take note of are the user's email, the Cotter User ID, and the access_token that we will use to protect our API endpoints. These information will be available to you at anytime the user is logged-in by calling cotter.tokenHandler.getAccessToken() and cotter.getLoggedInUser()

2. Let's try logging-in again but with your Github account that has the same email address

When you used a Github account that has the same address as an existing account, you should see a prompt asking if you'd like to link the accounts:

If you're using a Github account that has an email address that is not recognized, then it will automatically create a new user. You'll see the same JSON response like the above when the user has successfully log in with Github.

Designing our API endpoints to get data from Github

  1. We will have a dashboard page that will call our API endpoint at /api/repo to get a list of repositories owned by the user.
  2. We'll make an API endpoint /api/repo that will:

Make our API endpoint at /api/repo

Our endpoint will look like this:

GET http://localhost:3000/api/repo
Authorization: Bearer <Cotter Access Token>

1. Make a function to handle API calls to `/api/repo`

Next.js gives you a neat way to add server code that can handle API requests. To handle an API call to /api/repo, make a file pages/api/repo.js. Then, we'll add a skeleton handler function with a list of things that we need to do:

const handler = async (req, res) => {
  // TODO: Check if Authorization Header has a valid access_token
  // TODO: Parse the access_token to get cotter_user_id to
  // TODO: Call Cotter's API to get Github Access Token for the user
  // TODO: Call Github API to get the repository data
};

export default handler;
pages/api/repo.js

2. Check if the Authorization Header have a valid access token

We'll make a separate function above our handler function to do this check. We'll be using Cotter's client library to help us validate the access token.

yarn add cotter-node
// 1) Import Cotter
import { CotterValidateJWT } from "cotter-node";

const checkJWT = (handler) => async (req, res) => {
  // 2) Check that the access_token exists
  if (!("authorization" in req.headers)) {
    res.statusCode = 401;
    res.end("Authorization header missing");
    return;
  }
  const auth = await req.headers.authorization;
  const bearer = auth?.split(" ");
  const token = bearer?.length > 0 && bearer[1];

  // 3) Validate the access_token
  var valid = false;
  try {
    valid = await CotterValidateJWT(token);
  } catch (e) {
    console.log(e);
    valid = false;
  }
  if (!valid) {
    res.statusCode = 403;
    res.end("Authorization header is invalid");
    return;
  }

  // 4) Pass the access token to the next handler
  req.access_token = token;
  handler(req, res);
};

const handler = async (req, res) => {...};
                                     
// 5) We are passing our handler function into
// `checkJWT` so that `checkJWT` will be run first
// before our `handler` is run.
export default checkJWT(handler); 
pages/api/repo.js

What we did was pretty simple:

  • First, we check if the Authorization header exists
  • If it exists, then we check if the access_token is valid using Cotter's helper function.
  • Then we call checkJWT(handler) to run the check and then run the handler if the check passed.

3. Get the Cotter User ID from the access_token.

We will need this for our API call to Cotter. The access_token is a JWT token that contains the user's Cotter User ID. Check here for the full spec. We'll use another Cotter helper function to parse the access token and get the Cotter User ID.

yarn add cotter-token-js
import { CotterValidateJWT } from "cotter-node";
// 1) Import Cotter Token
import { CotterAccessToken } from "cotter-token-js";

const checkJWT = (handler) => async (req, res) => {...};

const handler = async (req, res) => {
  // Parse the access_token to get cotter_user_id
  const decodedToken = new CotterAccessToken(req.access_token);
  const cotterUserID = decodedToken.getID();
  // TODO: Call Cotter's API to get Github Access Token for the user
  // TODO: Call Github API to get the repository data
};

export default checkJWT(handler);
pages/api/repo.js

4. Get Github Access Token from Cotter API

The API to get a Social Provider access token from Cotter looks like this

curl -XGET \
-H 'API_KEY_ID: <COTTER API KEY ID>' \
-H 'API_SECRET_KEY: <COTTER API SECRET KEY>' \
'https://www.cotter.app/api/v0/oauth/token/GITHUB/<COTTER USER ID>'

Let's install axios and create our request

yarn add axios
import axios from "axios"; // Import axios

const checkJWT = (handler) => async (req, res) => {...};

const handler = async (req, res) => {
  // Parse the access_token to get cotter_user_id
  ...

  // Call Cotter's API to get Github Access Token for the user
  let githubAccessToken = "";
  const config = {
    headers: {
      API_KEY_ID: process.env.COTTER_API_KEY_ID,
      API_SECRET_KEY: process.env.COTTER_API_SECRET_KEY,
    },
  };
  try {
    let resp = await axios.get(
     `https://www.cotter.app/api/v0/oauth/token/GITHUB/${cotterUserID}`,
      config
    );
    githubAccessToken = resp.data.tokens?.access_token;
  } catch (err) {
    res.statusCode = 500;
    res.end("Fail getting Github access token from Cotter API");
    return;
  }
  // TODO: Call Github API to get the repository data
};

export default checkJWT(handler);
pages/api/repo.js

As you can see we're storing our secrets in an environment variable. Get your API_KEY_ID and API_SECRET_KEY from the dashboard and export it in your terminal, then run yarn dev.

❯ export COTTER_API_KEY_ID=<API KEY ID>
❯ export COTTER_API_SECRET_KEY=<API SECRET KEY>
❯ yarn dev

5. Call Github API to get the repositories list

Github's API to get the list of repositories of the authenticated user looks like this:

curl \
  -H "Accept: application/vnd.github.v3+json" \
  -H "Authorization: token <GITHUB ACCESS TOKEN>" \
  "https://api.github.com/user/repos"

Let's make the request using axios and the Github Access Token that we get in the earlier step.

const handler = async (req, res) => {
  // Parse the access_token to get cotter_user_id to
  ...
  // Call Cotter's API to get Github Access Token for the user
  ...

  // Call Github API to get the repository data
  const githubConfig = {
    headers: {
      Accept: "application/vnd.github.v3+json",
      Authorization: `token ${githubAccessToken}`,
    },
  };
  try {
    let resp = await axios.get(
      `https://api.github.com/user/repos`,
      githubConfig
    );
    // We only want to show the repo name and url
    const repoData = resp.data?.map((repo) => ({
      full_name: repo.full_name,
      url: repo.html_url,
    }));
    res.statusCode = 200;
    res.json(repoData);
    return;
  } catch (err) {
    res.statusCode = 500;
    res.end("Fail getting repostories from Github API");
    return;
  }
};

export default checkJWT(handler);
pages/api/repo.js

That's it, let's try our API endpoint

Copy your access token from the console log when logging in and run:

curl \
  -H "Authorization: Bearer <COTTER ACCESS TOKEN>" \
  "http://localhost:3000/api/repo"

You should see the following response:

[
  {
    "full_name": "putrikarunia/project1",
    "url": "https://github.com/putrikarunia/project1"
  },
  {
    "full_name": "putrikarunia/project2",
    "url": "https://github.com/putrikarunia/project2"
  },
  {
    "full_name": "putrikarunia/project3",
    "url": "https://github.com/putrikarunia/project3"
  }
]
Response from our backend at /api/repo

Showing the Repo List in our Dashboard Page

Make the Dashboard Page

Add a dashboard page by making a file at pages/dashboard.js. Using useEffect we will call our API endpoint to get the repositories, and put the results in our React state:

import { useEffect, useState } from "react";
import styles from "../styles/Home.module.css";
import Cotter from "cotter";
import axios from "axios";

export default function Dashboard() {
  const [err, seterr] = useState(null);
  const [repos, setrepos] = useState([]);

  // Get a list of repositories
  useEffect(() => {
    getRepositories();
  }, []);

  const getRepositories = async () => {
    // 1️⃣  Get Access Token for Logged-in User
    var cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
    const accessToken = await cotter.tokenHander.getAccessToken();

    // 2️⃣ Make the request to our `/api/repo` endpoint
    const config = {
      headers: {
        Authorization: `Bearer ${accessToken?.token}`,
      },
    };
    try {
      let resp = await axios.get("/api/repo", config);
      setrepos(resp.data);
    } catch (err) {
      seterr(JSON.stringify(err.response?.data));
    }
  };

  return (
    <div className={styles.container}>
      <h1 className={styles.subtitle}>
        Welcome! Here's a list of your Github Repos
      </h1>
      {/*  Show any error here */}
      <div style={{ color: "#FF0000" }}>{err}</div>

      {/*  3️⃣ Show the list of repositories */}
      <div className={styles.main}>
        {repos.map((repo) => (
          <div className={styles.card}>
            <h3>{repo.full_name}</h3>
            <a href={repo.url}>{repo.url}</a>
          </div>
        ))}
      </div>
    </div>
  );
}
pages/dashboard.js

Let's go over what we did:

  • We added 2 React states, err and repos, to show errors and the repo data.
  • When the component mounts, we call getRepositories which first gets the user's access token using Cotter's function cotter.tokenHandler.getAccessToken(), then calls an API request to our backend endpoint at /api/repo.
  • When the API call is successful, the function will update our repos state with the list of repositories, or show an error.

If you sign-in with Github, then go to localhost:3000/dashboard , you'll see the following:

Don't forget to uncomment the redirection part in /pages/index.js : router.push("/dashboard")
A list of your Github Repositories

Add a NavBar to Log Out or Log In and navigate between pages

Let's add a Navbar component to help our users navigate our website. Make a file /components/Navbar/index.js in your project directory.

import { useState, useEffect } from "react";
import Link from "next/link";
import Cotter from "cotter";

export default function Navbar() {
  const [loggedIn, setloggedIn] = useState(false);
  const [email, setemail] = useState(null);
  useEffect(() => {
    checkLoggedIn();
  }, []);

  // TODO: Check if the user is logged-in
  const checkLoggedIn = async () => {};

  // TODO: Log out the user
  const logOut = () => {};

  return (
    <div style={{ display: "flex", justifyContent: "flex-end" }}>
      {loggedIn ? (
        <div style={{ padding: 20 }} onClick={logOut}>
          Log Out
        </div>
      ) : (
        <Link href="/">
          <a style={{ padding: 20 }}>Log In</a>
        </Link>
      )}

      {loggedIn && <div style={{ padding: 20 }}>{email}</div>}
      <Link href="/dashboard">
        <a style={{ padding: 20 }}>Go to Dashboard</a>
      </Link>
    </div>
  );
}
components/Navbar/index.js
  • We added a loggedIn and email state. If the user is logged-in, we'll display the Log Out button and the user's email, otherwise we'll display the Login button.
  • The function checkLoggedIn will check if the user is logged in and update the loggedIn state and set the user's email state
  • We also added a function called logOut to log out the user.

Make the checkLoggedIn function

We can do this using Cotter's function by checking if an access token exists. Update your checkLoggedIn function:

  const checkLoggedIn = async () => {
    const cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
    const accessToken = await cotter.tokenHander.getAccessToken();
    if (accessToken?.token.length > 0) {
      setloggedIn(true);
      const user = cotter.getLoggedInUser();
      setemail(user?.identifier);
    } else {
      setloggedIn(false);
    }
  };
components/Navbar/index.js

Make the logOut function

We can also do this by calling Cotter's cotter.logOut() function. Update your logOut function:

  const logOut = async () => {
    const cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
    await cotter.logOut();
    setloggedIn(false);
    window.location.href = "/";
  };
components/Navbar/index.js

Import the Navbar in your Home Page and Dashboard Page

In /pages/index.js:

import Navbar from "../components/Navbar";

export default function Home() {
  ...
  return (
    <>
      <Navbar /> // Add the navbar
      <div className={styles.container}>...</div>
    </>
  );
}
pages/index.js

In /pages/dashboard.js:

import Navbar from "../components/Navbar";

export default function Dashboard() {
  ...
  return (
    <>
      <Navbar /> // Add the navbar
      <div className={styles.container}>...</div>
    </>
  );
}
pages/dashboard.js

Great! Now our website works well and users can log in/log out and get their Repositories list.

But, what if the user didn't Sign in with Github?

If the user didn't Sign in with Github, then we wouldn't get Github's Access Token, and it will return an error like this:

How do we fix this?

Fortunately, Cotter has a function to allow logged-in users to Connect a Github Account of their choosing to their current account. This means we can add a button in the dashboard that tells the user to Connect Github if we get this error.

Add a button to Connect Github if not connected yet.

Following the guide to Connect a Github account to an Existing User, we'll add a function and a button at pages/dashboard.js

import Cotter from "cotter";
export default function Dashboard() {
  ...

  // Get a list of repositories
  useEffect(() => {...}, []);

  const getRepositories = async () => {...};

  const connectToGithub = async () => {
    var cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
    const accessToken = await cotter.tokenHandler.getAccessToken();
    cotter.connectSocialLogin("GITHUB", accessToken?.token); // pass in the provider's name
  };

  return (
    <>
      <Navbar />
      <div className={styles.container}>
        {/*  Show any error here */}
        ...

        {/* If there's no Github access token, show a button to connect a Github account */}
        {err?.includes("Fail getting Github access token from Cotter API") && (
          <div className={styles.card} onClick={connectToGithub}>
            Connect Github
          </div>
        )}

        {/*  Show the list of repositories */}
        ...
      </div>
    </>
  );
}
pages/dashboard.js

Now let's try logging in with an email that is not associated with your Github account using the Email Address field. You should see something like this:

Show a Connect Github button if the user doesn't have any Github Account connected

Press Connect Github, and it will connect your currently logged-in Github Account with this email address.

If you log out and log in again with Github, you will now be logged-in to this new email address.

How do I disconnect a Github Account

We won't cover this in the tutorial, but you can use our API endpoint to delete a connection.

That's it!

We now have a working Github API integration with a simple way to get your user's Github Access Token.

Sign in with Github using Cotter and Next.js to use Github API

What's Next?

There's a lot of things that you can do using Github's API.


Questions & Feedback

Come and talk to the founders of Cotter and other developers who are using Cotter on Cotter's Slack Channel.

Ready to use Cotter?

If you enjoyed this tutorial and want to integrate Cotter into your website or app, you can create a free account and check out our documentation.

If you need help, ping us on our Slack channel or email us at [email protected]