permis
v1.0.7
Published
Library to implement OAuth2 server using Node and TypeScript
Downloads
1
Maintainers
Readme
permis
Library to implement OAuth2 server using Node and TypeScript
Table of Contents
- A. Resources
- B. Identity Provider
- C. Consumers
- D. Clients
- E. Consents
- F. Authorization Codes
- G. Tokens
- Appendix
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 becode
(default) ortoken
CLIENT-ID
must be ID of an existing clientSCOPE
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 appREDIRECT-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;
}