Simple example to implement OAuth2 using Angular,Node and GitHub in 4 steps

Angular&NodeEnthusiast
12 min readApr 26, 2020

--

OAuth2 flow

OAuth2 is just an alternative to the traditional client-server authentication model. In this model,an application requests access to a protected resource on the server ,on behalf of the user of the application and using the user’s credentials.

OAuth2 has 4 players in operation:

1.Resource owner: A resource owner has the capacity to give access to a protected resource. If the resource owner is a person then you call it as end user.

2.Resource server: A server storing the protected resource.

3.Client:The client is the application that makes a request on behalf of the resource owner to access the protected resource.

4.Authorisation server: The authorization server may be the same server as the resource server or a separate entity. It authenticates the resource owner and after obtaining authorization, issues access tokens to the client.

How is OAuth2 different from traditional model?

Instead of using the resource owner’s credentials to access the protected
resource, the client obtains an access token(a string denoting a
specific scope, lifetime, and other access attributes) from the authorisation server.

This token is issued only after the approval of the resource owner.

The client uses the access token to access the protected resource hosted by the resource server.

I will demonstrate a simple example how an angular application behaves as the client,Git as the authorisation and resource server and I am the resource owner/end-user:)

Step-1: Register the application with the Authorisation server.

It is essential that the application registers with Git first.

Go to this link below for the steps to complete the application registration. Its quite simple and hardly takes a few minutes.

Registering application with GitHub

As you can see, I have provided an application name,some description,homepage url and a callback url. We shall understand later the importance of the homepage and callback url.

Once your application is registered, Git will issue “client credentials” in
the form of a client identifier and a client secret.

The Client ID is a publicly exposed string that is used by GitHub to identify the application

The Client Secret is used to authenticate the identity of the application to Git
and must be kept private.

Step-2:Configuring Node server.

I have used npm module express-generator to produce a skeleton of the project. Below are the steps:

npm install -g express-generator  //install the module
express — view=ejs OAuthServer //generate a project OAuthServer
npm install //install dependencies in package.json
Node project

=>There is a .env file which contains the server port,client ID,client secret and the redirect uri.

APP_PORT=8080
CLIENT_ID=Git_client_id
CLIENT_SECRET=Git_client_secret
REDIRECT_URI=http://localhost:4200/redirect

=>Next, we have a config.js which enables us to access and export the environmental variables in .env.

const dotenv=require(‘dotenv’).config({path: ‘./.env’});

module.exports={
APP_PORT:process.env.APP_PORT,
CLIENT_ID:process.env.CLIENT_ID,
CLIENT_SECRET:process.env.CLIENT_SECRET,
REDIRECT_URI:process.env.REDIRECT_URI
}

=>modules.js contains all the imported modules.

const express=require(‘express’);
const cookieParser = require(‘cookie-parser’);
const path = require(‘path’);
const logger = require(‘morgan’);
const axios=require(‘axios’);
const bodyParser=require(‘body-parser’);
const cors=require(‘cors’);
const config=require(‘./config’);
const crypto=require(‘crypto’);
const cookieSession=require(‘cookie-session’);

exports.module={
express:express,
cookieParser:cookieParser,
path:path,
logger:logger,
axios:axios,
bodyParser:bodyParser,
cors:cors,
config:config,
crypto:crypto,
cookieSession:cookieSession}

axios module is required to make http requests to GitHub.

cors module is usually required because the Node server and the Angular dev server have different origins and the browser doesnt allow cross-origin communication. In order to relax the Same-Origin Policy we are using the module to specify the accepted the origins and methods.

CORS errors have been taken care by using proxy in the angular application.We have used cors module to restrict the origins and the methods that can access the server resources.

crypto module is required to produce a random unguessable string,useful to prevent CSRF attacks.

cookieSession is required to store the access token in a cookie.

=>app.js contains the middleware functions.

const mod=require(‘./modules’).module;
const app = mod.express();
// view engine setup
app.set(‘views’, mod.path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘ejs’);
app.use(mod.logger(‘dev’));
app.use(mod.express.json());
app.use(mod.express.urlencoded({ extended: false }));
app.use(mod.cookieParser());
app.use(mod.express.static(mod.path.join(__dirname, ‘public’)));

app.use(mod.cors({
origin:[‘http://localhost:4200'],
methods:[‘GET’,’PUT’,’POST’,’DELETE’]
}))

app.use(mod.cookieSession({
name:’sess’, //name of the cookie containing access token in the //browser
secret:’asdfgh’,
httpOnly:true
}))

const indexRouter = require(‘./routes/index’);

app.use(‘/oauth’, indexRouter);

// error handler
app.use(function(err, req, res, next) {
res.status(err.status).send();
});

app.listen(mod.config.APP_PORT,function(){
console.log(“app listening on port”+mod.config.APP_PORT);
})
module.exports = app;

=>Finally we have the index.js where are all the routes are defined.

const mod=require(‘../modules’).module;
const router = mod.express.Router();

router.get(‘/AuthPage’,function(req,res){
let state=mod.crypto.randomBytes(16).toString(‘hex’);
res.cookie(‘XSRF-TOKEN’,state);
res.send({authUrl:”https://github.com/login/oauth/authorize?client_id="+mod.config.CLIENT_ID+'&redirect_uri='+mod.config.REDIRECT_URI+'&scope=read:user&allow_signup='+true+'&state='+state});
})

router.post(‘/getAccessToken’,function(req,res){
let state=req.headers[“x-xsrf-token”];
mod.axios({
url:’https://github.com/login/oauth/access_token?client_id='+mod.config.CLIENT_ID+'&client_secret='+mod.config.CLIENT_SECRET+'&code='+req.body.code+'&redirect_uri='+mod.config.REDIRECT_URI+'&state='+state,
method:’POST’,
headers:{‘Accept’:’application/json’}
})
.then(function(resp){
if(resp.data.access_token){
req.session.token=resp.data.access_token;
}
res.send(resp.data);
})
.catch(function(err){
res.send(err);
})})

router.get(‘/getUserDetails’,function(req,res){
if(req.session.token){
mod.axios({
url:’https://api.github.com/user',
method:’GET’,
headers:{‘Authorization’:”token”+” “+req.session.token}
})
.then(function(resp){
res.cookie(‘login’,resp.data.login,{httpOnly:true});
res.send(resp.data);
})
.catch(function(err){
res.send(err);
})
}
else{
res.status(401).send();
}
})

router.get(‘/logout’,function(req,res){
req.session=null;
res.clearCookie(‘sess’);
res.clearCookie(‘login’);
res.status(200).send();
})

module.exports = router;

The entire Node project is available at the below link.

The 3 endpoints provided by GitHub:

  1. https://github.com/login/oauth/authorize is the authorisation endpoint.

The scope param value- read:user means granting the client access to read user’s profile data. There are many other available values for this param and needs to be set as per requirement.

allow_signup param value-true means unauthenticated users will be offered an option to sign up for GitHub during the OAuth flow.

redirect_uri param is the callback url set by the client during client registration.

client_id param was provided by Git post client registration.

state param is random string meant to protect against CSRF attacks. I have generated a random string using crypto module.

There is a login param available as well which I have not passed. The value of this param is a specific Git user account to use for signing into Git and authorising the client.

client_id is a mandatory query param which is required to be passed. Other params are optional.

2. https://github.com/login/oauth/access_token is the token endpoint

client_id and client_secret param values are generated by Git post client registration.

code param contains the authorisation code generated by Git after the resource owner is authenticated by Git and it authorises the client to access the resource.

redirect_uri is the callback url provided by client during registration.

state param should contain the same random string passed in the authorisation end point. We are accessing the string from a custom request header “X-XSRF-TOKEN” sent by the angular application.

client_id,client_secret and code are the required params. Remaining are optional.

3. https://api.github.com/user is an API that the client can make requests to using the access token.

To get more details on all the query params for each endpoint,please check the below link.

There are 4 routes.

/AuthPage returns GitHub’s authorisation endpoint to the angular application.

/getAccessToken is a request made to the token endpoint of the authorisation server i.e GitHub to provide an access token upon successful validation of the authorisation code and state.We are storing the token in a session cookie.

/getUserDetails is a request to the resource server i.e GitHub to provide the protected resource.

/logout is a request to log the user out of the OAuth application. Please note that the user is not logged out of GitHub.

Its little difficult to comprehend the meaning of the above now. But you shall understand better as you go further.

Step-3: Navigating to Authorisation Server’s authorisation endpoint.

Lets begin with our Angular project.

There are 3 main components:

LoginComponent: Application navigates to GitHub’s authorisation endpoint from this component.

RedirectComponent:GitHib redirects the user from authorisation endpoint after successfull authentication and authorisation back to this component.

DashboardComponent: RedirectComponent redirects to this component.

There are 2 main services:

OAuthService to communicate with the server.

ExtUrlResolverService to enable navigation to GitHub’s authorisation endpoint.

Routing Defintion: We are navigating to the LoginComponent(/login) once the application loads.

const routes: Routes = [
{
path:””,
pathMatch:”full”,
redirectTo:’login’
},
{
path:’dashboard’,
component:DashboardComponent
},
{
path:’login’,
component:LoginComponent
},
{
path:'redirect',
component:RedirectComponent
},
{
path: 'test',
component: GitAuthComponent,
resolve: {
url: ExtUrlResolverService
}
},
{
path:'**',
component:NoSuchComponent
}
];

AppModule Definition:

@NgModule({
declarations: [AppComponent,DashboardComponent,LoginComponent,GitAuthComponent],
imports: [BrowserModule,AppRoutingModule,HttpClientModule,
HttpClientXsrfModule],
providers: [],
bootstrap: [AppComponent]
})

There is nothing unusual about the above definition except for the HttpClientXsrfModule module. We have imported this module to utilise the built in CSRF protection provided by Angular. This module by default will extract the value of the cookie XSRF-TOKEN and send it in a custom X-XSRF-TOKEN in all mutating requests to the server.

This should explain why we sent a cookie by name XSRF-TOKEN in the GET /AuthPage route and extracted the value of X-XSRF-TOKEN request header in the POST /getAccessToken route in Node.

Environment Config:

export const environment = {
production: false,
baseUrl:’/oauth’
};

Proxy Config:

//proxy.conf.json
{
“/oauth/*”:{
“target”:”http://localhost:8080",
“secure”:false,
“logLevel”:”debug”,
“changeOrigin”:true
}
}

LoginComponent Template:

<button (click)="login()">Login with GitHub</button>
<app-error></app-error>

Its just a button that enables the user to login into the application via GitHub.

<app-error> is the reusable ErrorComponent.

When I click the button,I should navigate to the GitHub authorisation endpoint i.e authorisation server’s authorisation endpoint:

https://github.com/login/oauth/authorize

The purpose of navigation to this endpoint is that, Git authenticates the end-user and upon successful authentication,verifies whether the end-user accepts/declines the client application’s request to access the protected resource.

Its important that I navigate to this endpoint from http://localhost:4200/login because I have specified it as Homepage Url in the client registration with Git.

If you try from some other url ,you would get a CORS error.

Now lets proceed to see how are achieving this external URL navigation with Angular Router.

LoginComponent Class:

export class LoginComponent implements OnInit {

AuthUrl:string;
constructor(private serv:OAuthService,private router:Router) { }

ngOnInit() {
this.serv.GetAuthPage().subscribe(data=>this.AuthUrl=data["authUrl"],err=>{console.log(err)});
}

login(){
this.router.navigate([‘/test’],{queryParams:{url:this.AuthUrl}});
}
}

As soon as the component loads, I am calling a method GetAuthPage() in the OAuthService. This method in turn connects to the server and returns the authorisation endpoint. We shall see later how the service does this.

Now we have the endpoint stored in a variable AuthUrl.

When I click on the Login button,login() in the component is called, which enables navigation to a path /test,passing the authorisation endpoint as a query param.

If you noticed in the Routing definition, we have specified a dummy component GitAuthComponent corresponding to this path.

{
path: 'test',
component: GitAuthComponent,
resolve: {
url: ExtUrlResolverService
}
}

We call this component dummy because it doesnt do any work. We need this component just for the sake of navigation.

How does the ExtUrlResolverService help?

export class ExtUrlResolverService implements Resolve<any> {
constructor() { }

resolve(route: ActivatedRouteSnapshot,state:RouterStateSnapshot):Observable<any>
{
window.location.href=route.queryParamMap.get(‘url’);
return of(null);
}}

When a class implements the Resolve interface, it is called a data provider class which can be used to resolve data during navigation.The interface defines a resolve() which will be invoked when the navigation to the path /test starts.

In this method we are accessing the url passed as query param to /test and navigating to that url. This way we successfully navigated to GitHub’s authorisation endpoint.

Git Authorisation endpoint

Let me know show the OAuthService:

export class OAuthService {
constructor(private http:HttpClient) { }

GetAuthPage():Observable<string>{
return this.http.get<string>(environment.baseUrl+'/AuthPage');
}

getAcessToken(auth_code:string){
return this.http.post(environment.baseUrl+'/getAccessToken',{code:auth_code});
}

getUserDetails(){
return this.http.get(environment.baseUrl+'/getUserDetails');
}

logout(){
return this.http.get(environment.baseUrl+'/logout');
}}

Its quite simple with just 4 methods.

For now, lets just look at the first method GetAuthPage() which connects to the server http://localhost:8080/AuthPage to retreive the Authorisation endpoint.

router.get(‘/AuthPage’,function(req,res){
let state=mod.crypto.randomBytes(16).toString(‘hex’);
res.cookie(‘XSRF-TOKEN’,state);
res.send({authUrl:”https://github.com/login/oauth/authorize?client_id="+mod.config.CLIENT_ID+'&redirect_uri='+mod.config.REDIRECT_URI+'&scope=read:user&allow_signup='+true+'&state='+state});
})

We have used the crypto module to generate a random string and passed it to the state query param.

The state is also stored in a cookie named “XSRF-TOKEN” and sent to the browser,needed for validating subsequent requests for CSRF attacks.

As you can see, this endpoint has the redirect URI to which Git will redirect upon successfull user authentication and authorisation.

Step-4: Redirecting to RedirectComponent with Authorisation code.

So the end-user has navigated to the authorisation endpoint and he needs to enter his GitHub username and password to authenticate.

If successfully authenticated, Git asks the end-user if he authorizes the client application ,a restricted access to the user’s data. If the user authorizes, Git redirects the user to the redirect URL provided by the client during registration i.e http://localhost:4200/redirect. It also sends the authorisation code and the state(its the same state param passed to the authorisation end point)as query params in the redirect url as below:

http://localhost:4200/redirect?code=some_code&state=random_string

What is an Authorisation Code?

An authorization grant is a credential representing the resource
owner’s authorization to access its protected resources.

OAuth 2 defines four grant types, each of which is useful in different cases:

1.Authorization Code: used with server-side Applications

2.Implicit: used with Mobile Apps or Web Applications (applications that run on the user’s device)

3.Resource Owner Password Credentials: used with trusted Applications, such as those owned by the authorisation server itself.

4.Client Credentials: used with Applications API access

Now we have redirected to the RedirectComponent. In this component, we have the task of extracting the authorisation code from the URL and submitting it to Git along with the Client ID and secret to retrieve the access token.

Lets see how we can go about it.

export class RedirectComponent implements OnInit {
constructor(private active:ActivatedRoute,private serv:OAuthService,private router:Router) { }

ngOnInit() {
this.active.queryParamMap.pipe(concatMap(x=>
this.serv.getAcessToken(x.get(‘code’)) )).subscribe(data=>this.router.navigate([‘/dashboard’]),err=>{console.log(err)});
}
}

Once the RedirectComponent loads, we are extracting the authorisation code from the URL using ActivatedRoute module.

Next,we are calling a method getAccessToken() in the OAuthService, passing the code as argument.

//OAuthService
getAcessToken(auth_code:string){
return this.http.post(this.serverUrl+'getAccessToken',{code:auth_code})
}

As you can see, this method makes a POST request to the server to retrieve the access token.

//index.js

router.post(‘/getAccessToken’,function(req,res){
let state=req.headers[“x-xsrf-token”];
mod.axios({
url:’https://github.com/login/oauth/access_token?client_id='+mod.config.CLIENT_ID+'&client_secret='+mod.config.CLIENT_SECRET+'&code='+req.body.code+'&redirect_uri='+mod.config.REDIRECT_URI+'&state='+state,
method:’POST’,
headers:{‘Accept’:’application/json’}
})
.then(function(resp){
if(resp.data.access_token){
req.session.token=resp.data.access_token;
}
res.send(resp.data);
})
.catch(function(err){
res.send(err);
})})

We have sent the Client ID,Client Secret,Authorisation code,Redirect URL and the state in the POST request.

The authorisation code and state is validated by Git before producing an access token.

The access token is stored in a session cookie named “sess”.It is stored on both browser and server.

Finally we redirect from the RedirectComponent to the DashboardComponent once response received from server.

What is an access token?

Access tokens are credentials used to access protected resources. An
access token is a string representing an authorization issued to the
client to access the protected resource.

Tokens represent specific scopes and durations of access, granted by the
resource owner, and enforced by the resource server and authorization
server.The access token provides an abstraction layer, replacing different
authorization constructs (e.g., username and password) with a single
token understood by the resource server.

DashboardComponent Class

export class DashboardComponent implements OnInit {

constructor(private active:ActivatedRoute,private serv:OAuthService,private router:Router) { }

username:string;

ngOnInit() {
this.serv.getUserDetails().subscribe(data=>this.username=data["login"],err=>{console.log(err)});
}


logout(){
this.serv.logout().subscribe(data=>this.router.navigate(['/login']),err=>{console.log(err)});
}
}

DashboardComponent Template:

<h4>User Dashboard</h4>
Welcome {{username}}
<button (click)=”logout()”>Logout</button>
<app-error></app-error>

Once the DashboardComponent loads,it calls a method getUserData() in the OAuthService to access the protected resource i.e the user details using the access token.

//OAuthService

getUserDetails(){
return this.http.get(this.serverUrl+'getUserDetails');
}

Once the server recevies the request, it calls the Users API passing the access token in the Authorisation header.

//index.js

router.get(‘/getUserDetails’,function(req,res){
if(req.session.token){
mod.axios({
url:’https://api.github.com/user',
method:’GET’,
headers:{‘Authorization’:”token”+” “+req.session.token}
})
.then(function(resp){
res.cookie(‘login’,resp.data.login,{httpOnly:true});
res.send(resp.data);
})
.catch(function(err){
res.send(err);
})
}
else{
res.status(401).send();
}
})

The user details are returned by the resource server i.e GitHub and are we are using the login property of the response to print the username in the dashboard.

When the user clicks on the Logout button,we are sending a request to the server to remove the session cookie containing the access token and username from the browser and the server. We are redirecting back to LoginComponent.

//index.js
router.get(‘/logout’,function(req,res){
req.session=null;
res.clearCookie(‘sess’);
res.clearCookie(‘login’);
res.status(200).send();
})

In order to check how the entire Angular application code,go to the below link.

Summary:

Lets summarise the steps involved in a OAuth2 authentication using Authorisation code in brief:

  1. The client initiates the flow by directing the resource owner’s browser to the authorisation end point.
  2. The Authorisation server authenticates the resource owner and post successful authentication, it verifies whether the resource owner accepts or declines the client’s request to access the protected resource.
  3. Lets assume the resource owner accepts the request. In that case, the authorisation server redirects the resource owner’s browser back to the redirection url(provided at the time of client registration)along with an authorisation code.
  4. The client uses the authorisation code to request an access token from the authorisation server.
  5. The client uses the access token to access the protected resource from the resource server.
  6. In the example, the resource server and Authorisation server were the same. If they were different,then necessary communication between these 2 entities is also needed for the authentication to work as expected

Please let me know your comments:)

--

--

Angular&NodeEnthusiast
Angular&NodeEnthusiast

Written by Angular&NodeEnthusiast

Loves Angular and Node. I wish to target issues that front end developers struggle with the most.

Responses (5)