1.0.7 • Published 8 months ago

permis v1.0.7

Weekly downloads
-
License
SEE LICENSE IN LI...
Repository
github
Last release
8 months ago

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;
}