- Why you may want to read this article
- Implementation
- How to obtain Github Creds (client_id, client_secret)
- Github Auth Problems
- Demo
- Conclusion
Why you may want to read this article
In the previous part we made an OAuth web Client, using React
SPA (Single Page Application). In this part we integrated with our own OAuth Server which was built with OpenIddict and .NET.
This article will show how to create a Github OAuth client
using React SPA. To be precise - we will build a Sign with Github
button, after which we will query the Github REST API to show that the obtained access token works with Github Resource Server.
Since auth is a prerequisite to integrate with Github API, thus if you would like to integrate with Github - you any way would need to implement Github authentication and authorization.
Implementation
All the source code is in this repository in Github.
Github, as any OpenId Connect provider, just follows the well known protocol. Any OpenId Connect provider by default follows OAuth protocol by design, because it is built on top of it.
Note! Github has a bunch of problems that you need to fix, in case you would like to create a pure SPA client. Go to Github Auth Problems
section. I provided a detailed explanation there.
OAuth core entrypoint
The core of our React OAuth Client is already implemented in this article. We will just change configuration and some parts of the application. So just follow the article I referenced or clone this part of the repo.
Once you have the OpenIddict integrated React client you could follow subsequent steps.
Update utils/config.js
// custom settings that work with OAuth server
const githubSettings = {
authority: 'https://github.com/login/oauth/authorize',
client_id: 'PUT_YOUR_CLIENT_ID_HERE',
client_secret: 'PUT_YOUR_CLIENT_SECRET_HERE',
redirect_uri: 'http://localhost:3000/oauth/callback',
silent_redirect_uri: 'http://localhost:3000/oauth/callback',
post_logout_redirect_uri: 'http://localhost:3000/',
response_type: 'code',
metadata: {
authorization_endpoint: 'https://github.com/login/oauth/authorize',
token_endpoint: 'http://localhost:9999/https://github.com/login/oauth/access_token',
},
// this is for getting user.profile data, when open id connect is implemented
scope: 'repo'
};
export const githubConfig = {
settings: githubSettings,
flow: 'github'
};
authority
is set to the Github OAuth server endpoint, meaning that it is our OAuth authority that gives access_tokens and we can trust it.
client_id
and client_secret
you should get by registering your Github application, see How to obtain Github Creds
section.
redirect_uri
is the thing you are setting during Github OAuth app registration. We set it to the /oauth/callback
route to handle callbacks with authorization code.
silent_redirect_uri
is redirect uri used for token refresh.
post_logout_redirect_uri
is the url the user will be redirected to after he logged out.
response_type
is our OAuth grant. We are using code
because we chose one of the most secure flows: authorization code. According to Github official doc it supports only authorization code and device code flows.
You might notice that a new section called metadata
was added. The reason behind it is that Github does not have a Discovery endpoint (see Github does not have OIDC Discovery endpoint
section). But oidc-client-ts has the ability to set necessary values from Discovery endpoint by default.
The necessary metadata
fields are authorization_endpoint
and token_endpoint
. The first one we will use to start OAuth flow. The second is used to exchange Authorization Code for token, when Github Authorization Server will redirect to our callback page.
The metadata actually should have had end_session_endpoint
for logout. But in this regard Github Auth is not OIDC compliant as well (see Github does not have end_session_endpoint
)
As you can see for some reason token_endpoint is not pointing to github origin, but rather points to some localhost:9999
and then to the Github domain. This happens because of the CORS issue: Github does not allow third party JS to get the token (see Cors issue with token endpoint
, I provided a workaround and explanation). To overcome - we might use a proxy server that will disrespect Cors headers compared to browsers.
For scopes
we set repo
scope to be able to get private repositories of the user that was authenticated.
Update services/AuthService.js
All we need to do is change the setting used to create UserManager
.
Add configuration import
import { githubConfig } from '../utils/config';
And then change this line
const userManager = new UserManager(githubConfig.settings);
Update services/Api.js
We should add additional function that will query Github API with the token we have obtained:
export async function getGithubResources(token) {
const url = 'https://api.github.com/user/repos';
const options = {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
};
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const data = await response.json();
return data.map(d => d.name).join(', ');
} catch (error) {
console.error('There was an error fetching the data', error);
}
}
In this call we are going to use
Update pages/unauthenticated.page.js
Just cosmetic change to understand what do we authorize with:
<button onClick={sendOAuthRequest}>Login via github</button>
Update App.js
Here instead of getting data from our Resource Server in OpenIddict setup (because our Resource Server cannot validate github tokens) we will query repository data from Github API:
import { getGithubResources } from './services/Api';
const data = await getGithubResources(accessToken);
Update AuthService.js
Since we don’t have end_session_endpoint as was explained above. We need to patch our logout function:
export async function logout() {
await userManager.clearStaleState();
try {
await userManager.signoutRedirect();
} catch (e) {
console.log('error on signoutRedirect', e);
window.location.href = '/';
}
}
signoutRedirect
would fail since UserManager does not have end_session_endpoint.
So we cannot log out user on the Github side, but still we will drop all local state about authentication by calling await userManager.clearStaleState();
How to obtain Github Creds (client_id, client_secret)
First sign in to your Github account.
Then click on your avatar on the right side of the screen
Then click developer settings
If you don’t have any Github OAuth Apps - click New OAuth App
, and follow instructions to create it.
Create OAuth App
The important part of Github OAuth app is to have the Callback URL set to [http://localhost:3000/oauth/callback](http://localhost:3000/oauth/callback)
.
Obtain client id and secret
You should already see Client ID
.
Then click Generate a new client secret
to create a secret to your app.
Github Auth Problems
Github does not have OIDC Discovery endpoint
Github OpenId Connect provider DOES NOT implement OIDC Discovery endpoint. At least it is not located under the default Discovery endpoint path: .well-known/openid-configuration
.
I think it is not compliant with OpenId Connect specification because:
Github is Dynamic OpenID Provider
, since Client Registration (including our sample React Client) has dynamic nature, meaning Github didn’t know anything about existence until we registered in the How to obtain Github Creds
section.
Based on the point above we could find Dynamic OpenID Provider requirements:
Mandatory to Implement Features for Dynamic OpenID Providers
Discovery
These OPs MUST support Discovery, as defined in OpenID Connect Discovery 1.0
This requirement is not fulfilled.
You could just remove the metadata
field in oidc-client-ts configuration and try to sign in with Github, you will see that it is failing on discovery endpoint.
const githubSettings = {
authority: 'https://github.com/login/oauth/authorize',
client_id: 'PUT_YOUR_CLIENT_ID_HERE',
client_secret: 'PUT_YOUR_CLIENT_ID_HERE',
redirect_uri: 'http://localhost:3000/oauth/callback',
silent_redirect_uri: 'http://localhost:3000/oauth/callback',
post_logout_redirect_uri: 'http://localhost:3000/',
response_type: 'code'
// this is for getting user.profile data, when open id connect is implemented
scope: 'repo'
};
Talking about compliant providers - you could consider Google:
Github does not have end_session_endpoint
I searched tons of forums, stackoverflow, github, official doc, asked GPT, Copilot - but I didin’t find public Github OAuth/OIDC end_session_endpoint
.
According to OpenId Connect official documentation:
end_session_endpoint
REQUIRED. URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP.
Since Github does not provide discovery endpoints (also not compliant) - there is no source from which we can obtain it. I tried to guess it - but all guesses led me to 404.
Cors issue with token endpoint
In case you don’t set githubSettings.metadata.token_endpoint
to http://localhost:9999/https://github.com/login/oauth/access_token
, but rather to original github token endpoint (https://github.com/login/oauth/access_token) you will get this error
“Access to fetch at ‘https://github.com/login/oauth/access_token’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.”
There is well known issue with Github Authorization Server:
https://github.com/isaacs/github/issues/330 https://stackoverflow.com/questions/42150075/cors-issue-on-github-oauth
It just does not support Cross-Origin Resource Sharing headers.
Workaround
Since CORS errors might happen only in browsers for XHR/fetch requests - we can just call get tokens from the server, where CORS do not matter.
There are 3 options:
- You write your own backend server that will take auth_code, client_id, client_secret and send request to
https://github.com/login/oauth/access_token
and return this token to React client. - You use existing public one like
https://cors-anywhere.herokuapp.com/corsdemo
(I DO NOT RECOMMEND) - You use cors-anywhere docker image, set up it to your environment and use it by your app only.
I went with the third option: we will use an existing proxy server implementation called cors-anywhere. To not clone and run the app locally - let’s use a ready made docker image.
All you should do is:
docker run -p 9999:8080 --name cors-anywhere -d testcab/cors-anywhere
It will run cors-anywhere on 9999 port locally on your docker.
Demo
For the demo:
- put your Github client_id to
PUT_YOUR_CLIENT_ID_HERE
and client_secret toPUT_YOUR_CLIENT_SECRET_HERE
. - start docker container for cors-anywhere (see
workaround
). - start React application
Click Login via github
You will see in the network tab the same OAuth flow requests.
And you will see the call to github and my private repositories, which we were able to get with my access token.
Conclusion
As you might see, the Github OAuth/OpenId Connect implementation misses a lot of necessary functionality, so I would say it is not compliant. However in this article we were able to overcome all auth problems with Github: discovery endpoint missing, logout endpoint missing, CORS problem with token obtaining, etc.
As a result we were successfully authenticated to Github, obtained a token and called Github API to get all the public/private repos with the access token.
Please subscribe to my social media to not miss updates.: Instagram, Telegram
I’m talking about life as a Software Engineer at Microsoft.
Besides that, my projects:
Symptoms Diary: https://symptom-diary.com
Pet4Pet: https://pet-4-pet.com