npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

permis

v1.0.7

Published

Library to implement OAuth2 server using Node and TypeScript

Downloads

1

Readme

permis

Library to implement OAuth2 server using Node and TypeScript

Table of Contents

To simplify reading this document, let's define some placeholders:

[RESOURCE-HOST]: "https://api.example.com"
[RESOURCE-URI]:  "[RESOURCE-HOST]/billing-api/v1/invoices/*"
[IDP-HOST]:      "https://idp.example.com"
[OAUTH2-HOST]:   "https://auth.example.com"
[REDIRECT-URI]:  "https://client-app.local/auth/finish" It must not include any **parameters**.

A. Resources

We create some APIs i.e. Resources that can be accessed/managed together with Scopes

Example resource:

[RESOURCE-HOST]/billing-api/v1/invoices

Examples for scopes:

invoices:read
invoices:write

We can have an admin app to manage scopes:

POST [OAUTH2-HOST]/scopes

Sample DTO:

interface IScopeDto {
  id:         IdType; // unique e.g.: 'invoices:read'
  name:       string; // e.g. 'Read invoices'
  created_at: RawDateType;
  updated_at: RawDateType;
}

B. Identity Provider

All users should register:

  • Resource Owners
  • API Consumers

Example:

POST [IDP-HOST]/v1/users

Sample DTO:

interface IUserDto {
  id:            IdType; // unique
  username:      string; // unique
  password_hash: string;
  name?:         string;
  email?:        string;
  status:        string; // e.g. 'PENDING', 'ACTIVE', 'INACTIVE'
  created_at:    RawDateType;
  updated_at:    RawDateType;
  // other properties like settings
}

C. Consumers

API Consumer must register.

  • Payment may be required.
  • We may need to approve this or activate the account.
POST [OAUTH2-HOST]/consumers

Sample DTO:

interface IConsumerDto {
  id:            IdType; // unique
  name:          string;
  user_id?:      IdType; // ref to IdP users
  status:        string; // e.g. 'PENDING', 'ACTIVE', 'ON_HOLD', 'CLOSED', 'ARCHIVED'
  created_at:    RawDateType;
  updated_at:    RawDateType;
  // other properties like settings
}

D. Clients

API Consumer registers a Client App:

POST [OAUTH2-HOST]/clients

Sample DTO:

interface IClientDto {
  // unique - used to verify client_id
  id: IdType;

  // 'My Web App'
  name: string;

  // ref to consumers
  consumer_id: IdType;

  // used to verify client_secret
  secret_hash: string;

  // used to verify redirect_uri - separated by line-breaks
  redirect_uris: string;

  // e.g. 'http://app.example.com'
  url?: string;

  // an introductory paragraph would be useful
  description?: string;

  // e.g. 'http://app.example.com/images/icon.png'
  icon_url?: string;

  // e.g. 'WEB', 'MOBILE', 'SERVER'; we can require secret on most calls if it is not a 'WEB' app
  kind?: string;

  status:        string; // e.g. 'ACTIVE', 'INACTIVE', 'ARCHIVED'
  created_at:    RawDateType;
  updated_at:    RawDateType;
  // other properties like settings
  // white-listed IP addresses or regular expressions for better security - esp. if kind is 'SERVER'
}

E. Consents

E.1. Start Authorization

Client App initiates a consent request - redirects the user:

GET [OAUTH2-HOST]/authorize?response_type=RESPONSE-TYPE&client_id=CLIENT-ID&scope=SCOPE&state=CLIENT-CROSS-REF&redirect_uri=[REDIRECT-URI]
  • RESPONSE-TYPE can be code (default) or token
  • CLIENT-ID must be ID of an existing client
  • SCOPE must be a list of scopes using existing scope IDs - separated by spaces e.g. invoices:read invoices:write
  • STATE should be a reference generated by the client app
  • REDIRECT-URI must be one of the URIs registered with the given client - ideally URI-encoded

All inputs are verified.

E.1.a Create Consent

We need to track consents. At this step, we create a record based on URL query parameters with status PENDING. Then, we will include consent_id in the process.

Sample DTO:

interface IConsentDto {
  id:            IdType; // unique
  client_id:     IdType; // ref to clients
  redirect_uri:  string;
  scope:         string;
  state:         string;
  user_id:       IdType | null;
  status:        string; // e.g. 'PENDING', 'ALLOWED', 'REJECTED', 'REVOKED'
  expires_at:    RawDateType; // e.g. within 15 minutes it should be completed
  created_at:    RawDateType;
  updated_at:    RawDateType;
}

E.1.b. Show Consent Form

We have 2 choices:

  • Return HTML and show consent form
  • Redirect user to IdP App - it should contain return_url so that after sign up/in, the user can continue with consent user-journey.

E.1.c. Sign Up or Sign In

Possible user journeys:

[Sign In] --(redirect)-->                       /--> Allow
                         \                     /
                          X => [Consent form] X
                         /                     \
[Sign Up] --(redirect)-->                       \--> Reject

E.1.d. Show Consent Form

Consent form, that's managed by us, should be able to retrieve information about consumer, client and consent requested:

GET [OAUTH2-HOST]/consents/:id
GET [OAUTH2-HOST]/clients/:id
GET [OAUTH2-HOST]/consumers/:id

Otherwise, HTML could embed the information e.g. Handlebars could be used to render HTML with relevant context variable(s).

E.2. Update Consent

Based on Resource Owner's decision, we need to update consent record.

Note on Security: we need a session token to verify a user has signed in or not.

E.2.a. Reject

Update consent:

PATCH [OAUTH2-HOST]/consents/:id
{
  "user_id": "USER-ID",
  "status": "REJECTED"
}

E.2.b. Allow

Update consent:

PATCH [OAUTH2-HOST]/consents/:id

Change:

{
  "user_id": "USER-ID",
  "status": "ALLOWED"
}

E.2.b.i. Revoke

At a later date, a user may choose to revoke access to a client app.

Either we can update consent:

PATCH [OAUTH2-HOST]/consents/:id

Change:

{
  "status": "REVOKED"
}

Or delete it:

DELETE [OAUTH2-HOST]/consents/:id

F. Authorization Codes

When consent is given (e.g. user allowed app to read invoices), we should create an authorization code.

F.1. Finish Authorization

Consent form should inform OAuth2 server:

POST [OAUTH2-HOST]/authorize?response_type=RESPONSE-TYPE&client_id=CLIENT-ID&scope=SCOPE&state=CLIENT-CROSS-REF&redirect_uri=REDIRECT-URI&consent_id=CONSENT-ID&allowed=1

F.1.a. Error

Redirect to client app with error:

GET REDIRECT-URI?error=access_denied&state=STATE

F.1.b. Success

Sample DTO:

interface IAuthCodeDto {
  id:            IdType; // unique
  code:          string; // unique
  consent_id:    IdType; // ref to consents
  status:        string; // e.g. 'PENDING', 'USED', 'EXPIRED'
  expires_at:    RawDateType; // e.g. it should be used within 5 minutes
  created_at:    RawDateType;
  updated_at:    RawDateType;
}

Authorization code will be returned when response type is code.

GET REDIRECT-URI?code=AUTH-CODE&state=STATE

G. Tokens

G.1. Exchange Auth Code

Client is expected to send authorization code in order to have an access token that can be used to access Resources (APIs) - according to allowed scope(s).

POST [OAUTH2-HOST]/tokens

Input:

{
  "client_id": "CLIENT-ID",
  "grant_type": "authorization_code",
  "code": "AUTH-CODE"
}

G.1.a. Update Auth Code

Update Auth Code; it is used. Change:

{
  "status": "USED"
}

G.1.b. Create Access Token

Sample DTO:

interface IAccessTokenDto {
  id:        IdType; // unique
  client_id: IdType; // ref to clients
  user_id:   IdType; // ref to users

  // multiple values separated by space - each ref to scopes
  scope: string;

  // unique - JWT can be used
  access_token: string;
  // e.g. it is valid for 30 days
  access_token_expires_at: RawDateType;

  // unique - JWT can be used
  refresh_token: string;
  // e.g. it is valid for 3 months
  refresh_token_expires_at: RawDateType;

  status:     string; // 'ACTIVE', 'INACTIVE'
  created_at: RawDateType;
  updated_at: RawDateType;
}
G.1.b.i. Exchange Refresh Token

Client can use refresh token (before it expires) in order to create a new access token and refresh token. It can be used only once.

POST [OAUTH2-HOST]/tokens

Input:

{
  "client_id": "CLIENT-ID",
  "grant_type": "refresh_token",
  "refresh_token": "REFRESH-TOKEN"
}

We need to find by using unique refresh token and update existing token record; change:

{
  "status": "INACTIVE"
}

Then, we can "clone" old token record, create a new token record (with new access token and refresh token) and send it to client which needs to store access token and refresh token as usual. The old access token and refresh token cannot be used.

G.2. Access Resources

Client can use access token to make calls to APIs/Resources, using authorization header containing token.

GET [RESOURCE-HOST]/billing-api/v1/invoices/:id

It needs to communicate with OAuth2 server and verify token, using authorization header containing token!

GET [OAUTH2-HOST]/authenticate

Resource service needs to be passed: user_id and scope(list e.g. invoices:read). Then, it can decide whether that user can retrieve the details of that particular invoice or not! It could still use other info to support that decision: like user roles and permissions, type of a record, status of a record etc.

Appendix

Shared types:

type IdType      = string | number;
type RawDateType = string | number; // used for new Date(*)

// this can be extended by other DTOs
interface IBaseDto {
  id:          IdType;
  name?:       string;
  created_at?: RawDateType;
  updated_at?: RawDateType;
}