GitHub is the provider that breaks the pattern. If you have already wired Google or Microsoft into Directus, none of that configuration transfers — different driver, different variables, and one setting that is optional everywhere else but effectively mandatory here. Skip it and GitHub will authenticate the user perfectly and Directus will still refuse them, with nothing on screen to say why.

Throughout, the Directus install is at https://cms.contensu.com. Substitute your own address wherever you see it.

Why GitHub cannot use the OpenID driver

Google and Microsoft are configured through the openid driver, which works by fetching a discovery document from the provider — a well-known URL that lists the authorisation endpoint, the token endpoint, the signing keys and the supported claims. You give Directus one issuer URL and it finds everything else itself.

GitHub does not publish one for user sign-in. It is an OAuth 2.0 provider, not an OpenID Connect one — it never issues an id_token, and there is no sub claim anywhere in the flow. So GitHub needs the oauth2 driver, where you supply all three endpoints yourself and then tell Directus how to read the profile that comes back.

That last part is the real work, and it is where this guide spends most of its time.

Step 1 — Register an OAuth App

Go to Settings → Developer settings → OAuth Apps → New OAuth App. For an app your whole team uses, do this on the organisation rather than your personal account, under Organisation settings → Developer settings — otherwise the app belongs to whoever created it, and leaves when they do.

Note the choice on that screen: OAuth App, not GitHub App. GitHub Apps are the newer thing and are aimed at acting on repositories with their own identity. For signing users in, the OAuth App is what you want.

Three fields matter:

Application name is what users see on the consent screen. Put something they will recognise, because "test-app-2" asking for account access does not inspire confidence.

Homepage URL is cosmetic. Your Directus URL is fine.

Authorisation callback URL is the one that has to be exact:

https://cms.contensu.com/auth/login/github/callback

The github in that path is the provider name you choose in the Directus config below, not a fixed string. Call your provider gh and the URL becomes /auth/login/gh/callback. They must agree.

Then generate a client secret. As with every provider, it is shown once.

An OAuth App holds a single callback URL, which is a real difference from Google — there you can list production and localhost on one client. Here you will generally end up registering a second OAuth App for local development. It is free and takes a minute, but it catches people who expect to add a second URL and find nowhere to put it.

Step 2 — Configure Directus

There is no issuer URL to discover from, so all three endpoints are given explicitly:

AUTH_PROVIDERS="github"

AUTH_GITHUB_DRIVER="oauth2" AUTH_GITHUB_CLIENT_ID="Ov23li..." AUTH_GITHUB_CLIENT_SECRET="the-secret-you-only-see-once"

AUTH_GITHUB_AUTHORIZE_URL="https://github.com/login/oauth/authorize" AUTH_GITHUB_ACCESS_URL="https://github.com/login/oauth/access_token" AUTH_GITHUB_PROFILE_URL="https://api.github.com/user"

AUTH_GITHUB_IDENTIFIER_KEY="id" AUTH_GITHUB_SCOPE="read:user user:email" AUTH_GITHUB_FIRST_NAME_KEY="name"

AUTH_GITHUB_ALLOW_PUBLIC_REGISTRATION="true" AUTH_GITHUB_DEFAULT_ROLE_ID="<a role UUID>"

Miss any of the five in the first two blocks and Directus logs Invalid provider config at startup and the provider never loads. The remaining four are the interesting ones, and each has a reason.

Step 3 — The setting that decides whether this works at all

AUTH_GITHUB_IDENTIFIER_KEY="id" is the line to get right.

Directus needs a stable, permanent string to identify the account — it goes into the external_identifier column and links that GitHub user to that Directus user forever. With the OpenID providers this defaults sensibly to sub. GitHub sends no sub, so Directus falls back to the email address.

And here is the trap: https://api.github.com/user returns email: null for most accounts. That field is the user’s public profile email, which the majority of people never set — and it stays null even when your scope includes user:email, because private addresses live on a different endpoint entirely, one Directus does not call.

So with no identifier key, there is no identifier. We ran exactly this against a real Directus, and the result is worth describing precisely, because it looks like nothing at all: the user approves on GitHub, GitHub redirects back, Directus exchanges the code successfully, fetches the profile successfully — and then drops them on the login page. No error banner. No message. The only evidence is one line in the server log:

WARN: [OAuth2] Failed to find user identifier for provider "github"

Setting AUTH_GITHUB_IDENTIFIER_KEY="id" fixes it. GitHub’s numeric account id is permanent — it survives username changes, which is exactly what you want, since GitHub usernames can be changed at will and a login tied to one would break the day someone rebrands. With that set, the same flow created the user immediately, storing 583231 as the external identifier.

The cost is that the identifier is unreadable. If you ever need to match a Directus user to a human by hand, you are looking at a number. Store the username too if that matters to you — AUTH_GITHUB_LAST_NAME_KEY="login" is a slightly grubby but effective way to keep it visible in the admin app.

Step 4 — Scope, and a default that is wrong for GitHub

Directus defaults the OAuth2 scope to email. We confirmed this by watching the redirect it builds:

https://github.com/login/oauth/authorize?client_id=...&scope=email&...

email is a valid OpenID scope. It is not a GitHub scope. GitHub’s are named differently — read:user, user:email, repo and so on — so the default asks for something GitHub does not recognise.

AUTH_GITHUB_SCOPE="read:user user:email"

read:user gets you the profile. user:email is what lets GitHub include a verified address in the profile response for accounts that have one. Neither grants any access to code, and the consent screen will say so — which is worth knowing before someone asks whether signing in gives your CMS access to the company’s repositories. It does not.

Step 5 — Names, and why last name stays empty

OpenID providers send given_name and family_name as separate claims, which is what Directus reads by default. GitHub sends a single name field — one free-text string, whatever the person typed into their profile, and often empty.

So map the whole thing to the first name and accept that the last name will be blank:

AUTH_GITHUB_FIRST_NAME_KEY="name"

In our test the created user came out with a first name of The Octocat, no last name, and no email. That is a correctly configured GitHub login, not a broken one. If empty name fields bother your team, AUTH_GITHUB_SYNC_USER_INFO="true" at least re-reads them on every login, so someone who fills in their GitHub profile later will see it appear.

What actually travels between the clicks

Useful to have in your head when something goes wrong halfway.

Directus sets a short-lived cookie called oauth2.github holding a signed JWT with the PKCE verifier and the callback URL. It expires after five minutes, so a user who wanders off mid-login comes back to a failure rather than a session.

The redirect to GitHub carries code_challenge and code_challenge_method=S256. Directus always uses PKCE, for every OAuth2 provider, whether or not the provider asked for it — GitHub ignores the extra parameters and the flow completes normally. It also always sends access_type=offline, which is a Google convention that GitHub likewise ignores. Both are harmless; both look alarming in a URL if you do not know to expect them.

Directus then exchanges the code server-to-server against ACCESS_URL, sending Accept: application/json. This matters more than it sounds: GitHub’s token endpoint returns form-encoded text by default and only switches to JSON when asked. Directus asks, so it works — but it is why a hand-rolled integration against the same endpoint so often returns something unparseable.

Finally it calls PROFILE_URL with a bearer token. Whatever JSON comes back is flattened, so nested values can be addressed with dots — AUTH_GITHUB_IDENTIFIER_KEY="plan.name" would be legal, if pointless.

The steps people forget

The identifier key

Covered at length above because it accounts for most failures. If GitHub sign-in "does nothing", check the log for Failed to find user identifier before anything else.

Public registration is off by default

An unrecognised GitHub account is rejected, not created, and the error is a generic invalid credentials. Either set AUTH_GITHUB_ALLOW_PUBLIC_REGISTRATION="true", or create the user in Directus by hand with the External Identifier field set to their numeric GitHub id.

And think about what that switch means on GitHub specifically. Unlike a corporate Google Workspace or Entra tenant, anyone in the world can create a GitHub account. Public registration plus GitHub sign-in means the door is open to everyone, so the default role you hand out had better be a small one.

Registration is on, but no default role

The user is created, signs in, and sees an empty Directus with no explanation. Set AUTH_GITHUB_DEFAULT_ROLE_ID to a real role UUID.

Expecting organisation or team mapping

AUTH_GITHUB_ROLE_MAPPING reads a groups claim from the profile response, and https://api.github.com/user contains no such field. Organisation and team membership live on separate API endpoints that Directus does not call, so there is nothing to map. Everyone lands on the default role. If you need "members of this org get editor", that is a custom auth.create hook, not a configuration setting.

If you do set a role mapping, note it must be declared with the json: prefix. Without it the value parses as an array and Directus refuses to start the provider, which at least fails loudly.

An email that collides with an existing user

If the GitHub account does expose an email and a Directus user already has it, creation fails on a uniqueness constraint and the log reads Failed to register user. User not unique. Common when someone already has a password account and then tries the GitHub button.

Cookies over plain HTTP

On a real domain set AUTH_GITHUB_COOKIE_SECURE="true". On plain HTTP leave it off, or the browser discards the oauth2.github cookie and the callback fails saying it cannot verify it.

The licence tier, but only on Directus 12

Directus 11 has no licence module at all, so this works on a stock 11.17.4. Licensing arrived in 12.0.0, where SSO routes sit behind an sso_enabled entitlement — without it the login route returns 404 and Directus logs you have SSO providers configured these will be unavailable under the current license tier at startup.

Trying it on localhost first

GitHub accepts http://localhost callback URLs, so the whole flow runs on your laptop. Two differences from the Google setup are worth planning for.

You will want a second OAuth App. An OAuth App holds one callback URL, so localhost cannot simply be added alongside production. Register a separate development app and keep its credentials in your local environment only.

Directus still builds the callback from PUBLIC_URL, not from the address in your browser. Set PUBLIC_URL=http://localhost:8055 locally, or — on Directus 11.14.1 and later — keep production’s and add AUTH_ALLOWED_PUBLIC_URLS=http://localhost:8055; Directus matches the request protocol and host against that list and falls back to PUBLIC_URL. The match is exact, port included.

Then leave the cookie flags alone. The defaults — SESSION_COOKIE_SECURE false, same-site lax — are already right for HTTP, and it is copying a production .env that breaks local sign-in, silently, by making the browser discard a secure cookie sent over an insecure connection.

One diagnostic that saves time: if you pass a ?redirect= parameter to the login route, a failure comes back as ?reason=SOMETHING on that URL rather than vanishing. With no redirect parameter Directus sends you to / on success, which is why a successful login and a failed one can look identical in the browser.

Common questions

Does Directus support GitHub login?

Yes, through the oauth2 driver rather than the openid one. GitHub does not publish an OpenID discovery document for user sign-in and never issues an id_token, so you supply the authorise, token and profile URLs yourself and tell Directus how to read the profile response.

Why does GitHub sign-in silently return me to the Directus login page?

Almost always a missing identifier key. GitHub sends no sub claim, so Directus falls back to the email address — and https://api.github.com/user returns email: null for any account without a public email. With no identifier the login is abandoned with no visible error and a single server log line reading "[OAuth2] Failed to find user identifier". Set AUTH_GITHUB_IDENTIFIER_KEY to id.

What should AUTH_GITHUB_IDENTIFIER_KEY be set to?

id — GitHub’s numeric account identifier. It is permanent and survives username changes, which matters because GitHub usernames can be changed at any time. Using login instead would break every affected account the day someone renames themselves.

Which scope does Directus need for GitHub?

read:user user:email. The Directus default is email, which is an OpenID scope and not a GitHub one, so it must be set explicitly. Neither scope grants any access to code, and the GitHub consent screen will say so.

What redirect URI does GitHub need?

Your PUBLIC_URL followed by /auth/login/<provider>/callback — for example https://cms.contensu.com/auth/login/github/callback, where the provider segment is the name you chose in AUTH_PROVIDERS. Note that a GitHub OAuth App holds only one callback URL, so local development usually needs a second OAuth App.

Why is the last name empty for GitHub users?

GitHub sends a single free-text name field rather than the separate given_name and family_name claims that OpenID providers use. Map it with AUTH_GITHUB_FIRST_NAME_KEY="name" and expect the last name to stay blank. That is a correctly configured GitHub login, not a broken one.

Can I map GitHub organisations or teams to Directus roles?

Not with configuration. AUTH_GITHUB_ROLE_MAPPING reads a groups claim from the profile response, and https://api.github.com/user contains no organisation or team information — that lives on separate API endpoints Directus does not call. Everyone lands on the default role unless you write a custom auth.create hook.

Is it safe to let anyone sign in to Directus with GitHub?

Only with a deliberately small default role. Unlike a Google Workspace or Entra tenant, anyone in the world can create a GitHub account, so turning on AUTH_GITHUB_ALLOW_PUBLIC_REGISTRATION opens registration globally. Either leave it off and create users by hand with their numeric GitHub id, or make sure AUTH_GITHUB_DEFAULT_ROLE_ID grants almost nothing.

Can I test GitHub SSO on localhost?

Yes. GitHub accepts http://localhost callback URLs, but an OAuth App holds only one callback URL, so register a separate development app rather than trying to add localhost to the production one. Point PUBLIC_URL at localhost or list it in AUTH_ALLOWED_PUBLIC_URLS on 11.14.1 and later, and leave the cookie secure flags off while you are on plain HTTP.

Want GitHub sign-in working without the guesswork?

The configuration is the easy half. The half that takes judgement is who is allowed in, what they can see on day one, and how you keep a public identity provider from becoming a public door into your CMS. We build on Directus full time. Fifteen minutes and you will know exactly what your setup needs.

Book a 15-minute call

More articles

Directus8 August 20269 min read

How to Set Up Microsoft (Entra ID) SSO Login in Directus

Directus8 August 202612 min read

How to Set Up Google SSO Login in Directus (Step by Step)

Directus8 August 202613 min read

How to set up LDAP and Active Directory login in Directus

Back to all articlesBack to top