Skip to content

Add example for Authorization Code grant. #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions authorization-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Authorization Code Grant Example

## Architecture

The authorization code workflow is described in
[RFC 6749, section 4.1](https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1):

```
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
```

### Provider dependencies

- @node-oauth/express-oauth-server (uses @node-oauth/oauth2-server)
- express
- body-parser

### Client dependencies

- express
- ejs

## Installation and usage

Install dependencies in both provider and client directories:

```shell
$ cd provider && npm install
$ cd ../client && npm install
```

Create a `.env` file in the authorization-code directory:

```
CLIENT_ID=testclient
CLIENT_SECRET=testsecret
REDIRECT_URI=http://localhost:3000/callback
USER_ID=user1
USERNAME=demo
PASSWORD=demo
```

Start the provider (authorization server + resource server):

```shell
$ cd provider && npm start
```

Start the client application:

```shell
$ cd client && npm start
```

Visit http://localhost:3000 to start the authorization code flow.

## About This Example

This example demonstrates a clear separation between the OAuth2 provider (authorization server + resource server) and the client application. Unlike other examples that might combine both roles in a single application, this example shows:

- **Provider** (port 8080): Acts as both authorization server and resource server
- **Client** (port 3000): A separate web application that consumes OAuth2 services

This separation makes it easier to understand what the framework supports and what it doesn't.

## Flow

1. User visits the client application at http://localhost:3000
2. User clicks "Login" to start the authorization flow
3. User is redirected to the provider's authorization page
4. User enters credentials and grants authorization
5. User is redirected back to the client with an authorization code
6. Client exchanges the code for an access token
7. Client can now access protected resources using the access token
76 changes: 76 additions & 0 deletions authorization-code/client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require("dotenv").config({ path: "../.env" });
const express = require("express");
const crypto = require("crypto");

const app = express();
const states = new Map();

app.set("view engine", "ejs");
app.set("views", "./views");
app.use(express.static("public"));

const authServer = "http://localhost:8080";
const clientId = process.env.CLIENT_ID || "testclient";
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if the sample worked just as is without any envs. So I added a default, but we can discuss the options.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think envs are fine, you can also use .env if you like, it's a good common practice

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did add both options. Use the values from the defaults if the env does not exist. But I am happy to remove the defaults if you prefer it to come only from the env.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should we commit a .env file with the defaults? Not like it contains secretive stuff... The intention being the samples work with as minimal effort as possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common practice is to gitignore .env but provide a .env.example that users can copy. This avoids ever checking in real .env (in case users build their project upon the example)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or do not check in a .env at all but tell users to create one with example data. My concerns are only regarding users cloning and continuing to use it until production.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a .env.example file. Seems to be a good balance incase they use it till production.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jankapunkt FYI I got the example to a runnable state. Would you be able to run this when you have some availability?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shrihari-prakash yes I will test it and leave comments/review if needed

const clientSecret = process.env.CLIENT_SECRET || "testsecret";
const redirectUri =
process.env.REDIRECT_URI || "http://localhost:3000/callback";

function generateState() {
return crypto.randomBytes(16).toString("hex");
}

app.use(express.urlencoded({ extended: false }));
app.use(express.json());

app.get("/", (req, res) => {
res.render("index", {
authServer: authServer,
});
});

app.get("/login", (req, res) => {
const state = generateState();
states.set(state, { created: Date.now() });

res.render("authorize", {
client: { id: clientId },
redirectUri: redirectUri,
scope: "read write",
state: state,
authServer: authServer,
});
});

app.get("/callback", (req, res) => {
const { code, state, error } = req.query;

if (error) {
return res.render("error", {
message: `Authorization Error: ${error}`,
});
}

if (!states.has(state)) {
return res.render("error", {
message: "Invalid State: State parameter mismatch",
});
}

states.delete(state);

res.render("callback", {
code: code,
state: state,
authServer: authServer,
clientId: clientId,
clientSecret: clientSecret,
redirectUri: redirectUri,
});
});

app.get("/logout", (req, res) => {
res.redirect("/");
});

app.listen(3000);
console.debug("[Client]: listens to http://localhost:3000");
Loading