OAuth 2.0 Server
Free — Open — Simple

1. About

This OAuth 2.0 server is based on Jared Hanson's Passport and Oauth2orize modules. Its focus is to provide existing websites with a straight forward way to become OAuth providers, allowing partner sites to consume their resources easily and with a moderate level of security.

As a provider, the existing website is given control over not only the whitelist of valid partner sites but also the OAuth patterns each partner is allowed to use (i.e. Authorization Code Grant vs. Implicit Grant.)

In OAuth terms, the existing website fills the Authorization Server and Resource Server roles and the partner sites take the Client role. This software provides the Authorization Server functionality and is generally able to integrate without any mandatory software changes to the Resource Server.

Reference documentation is provided for Client integration.

This project's source code is freely available under the GNU General Public License.

2. Abstract Protocol Flow

RFC 6749 defines the abstract protocol flow as:

OAuth2.0 - Abstract Protocol Flow

The participants are:

  • Resource Owner: The end user.
  • Client: The third party site you're exposing your protected resources to.
  • Resource Server: The server hosting the protected resources.
  • Authorization Server: This application.

As a concrete example, if you allow people (i.e. Resource Owners) to log in to your site via Twitter, then you take on the role of Client, Twitter's core is the Resource Server and Twitter's OAuth provider is the Authorization Server.

The point of all of this is:

  1. Users get to make explicit decisions about what information they share, and with who
  2. Resource Servers can integrate with third parties without exposing Resource Owner credentials.

3. Getting Started

Get Node
 > sudo add-apt-repository ppa:chris-lea/node.js
 > sudo apt-get update
 > sudo apt-get install nodejs
Get the source
 > git clone https://github.com/jlabusch/oauth2-server.git
 > cd oauth2-server
Make
 > npm install
Run
 > npm test

See also npm [stop|start|restart].

4. Components

In its role as Authorization Server, this application acts as a broker for the interactions between the other participants. It maintains a registry of accepted Client sites, authenticates users against the Resource Server and manages the authorization granted by those users for Client access to their data.

Server components

5.0. Auth Server

The heart of the application is the Express / Passport / Oauth2orize stack that defines the framework in which the OAuth interactions take place.

The basic interactions are:

GET /login

Prompts the Resource Owner for his credentials using an EJS template configured in auth_server.views.login

POST /login

Authenticates the Resource Owner against the Resource Server using a custom function that you'll need to define.

GET /authorize

Prompts the user for authorization to share their protected resources with the Client site. Uses the auth_server.views.dialog EJS template

POST /authorize

Handles the result of the user's decision. Depending on the OAuth pattern chosen by the Client, this can result in an access token or an authorization code (which the Client then redeems for an access token.)

POST /token

Exchanges an authorization code (or a refresh token) for an access token.

GET /review

Allows the user to review existing access token/refresh token grants and revoke any that should no longer apply. Uses the auth_server.views.review EJS template

POST /review

Revokes tokens as directed by the user.

GET /api/*

Services allowing access to protected resources based on access tokens. All /api/ services will be highly specific to your Resource Server.

5.1 Auth Server Config

"auth_server": {
  "url": "http://127.0.0.1:8081",
  "port": 8081,
  "ssl": false,
  "ssl_cert": null,
  "ssl_key": null,
  "session_secret": "REPLACE ME",
  "num_workers": "auto",
  "views": {
    "layout": "layout",
    "login": "login",
    "dialog": "dialog",
    "review": "review"
  }
}

The Authentication Server portion controls the operation of the core Express webserver.

  • url: Used for user agent redirection between endpoints, e.g. from /authorize to /login, and by automated tests. If the Auth Server is deployed behind a reverse proxy, this should be the external, public-facing URL
  • port: Express webserver listen port
  • ssl: Flag to toggle between HTTP and HTTPS
  • ssl_cert: SSL certificate to use (implies ssl = true)
  • ssl_key: SSL key file to use (implies ssl = true)
  • session_secret: Value used to sign session cookies
  • num_workers: Number of worker processes to spawn (auto means one per "CPU")

For the EJS template files, any name $X is translated to the file ./views/$X.ejs.

  • views.layout: Page foundation
  • views.login: Body of authentication page
  • views.dialog: Body of authorization page
  • views.review: Body of review page

6.0 Resource Server

The easiest way to integrate with an existing website is to provide an integration layer that proxies OAuth requests to existing resource server APIs. For example, suppose your existing site supports

  • POST /login, used by your login form
  • GET /user/info, used by your client-side AJAX calls to get user profile information
  • POST /user/avatar, which your user settings page can hit to update a user's profile picture.

In ./lib/resource_server.js, you might define:

exports.MyResourceServerInterface = {
  login: function(username, password, next){ /* proxy to yoursite/login */ },
  api: {
    scopes: ['read-only', 'read-write'],
    update_avatar: {
      scope: 'read-write',
      fn: function(req, res){ /* proxy to yoursite/user/avatar */ }
    },
    profile: {
      scope: 'read-only',
      fn: function(req, res){ /* proxy to yoursite/user/info */ }
    }
  }
}

The login() function should invoke next in the following ways:

  • Login success:
    next(null, {id: UID, site_token: T, ...})
  • Login failure:
    next(null, false, {message: '...'})
  • Internal error:
    next(err)

The api functions should use the site_token above (available as req.authInfo.site_token) to access the protected resources. The site token is typically the same kind of long-lived cookie or "remember me" token used by existing AJAX etc. services on your site.

API responses should be written using the HTTP/S response argument res, e.g. using res.json(...).

6.1 Resource Server Config

"resource_server": {
  "type": "dummy",
  "host": "127.0.0.1",
  "port": 8084,
  "basic_auth": "user:pass",
  "user_agent": "OAuth-IdP"
}
  • type: The name of an object exported from ./lib/resource_server.js

Additional options can be defined for your own implementation of the Resource Server interface.

See also the stub server in ./dummy-servers/resource_server.js, which is started automatically via npm start when the configured type is dummy.

For a real world interface example, see the stuff_nation type in ./lib/resource_server.js, which is compatible with Stuff.co.nz.

7.0 Storage

The application stores a few different kinds of state:

  • User sessions
  • Authorization codes
  • Access tokens
  • Refresh tokens
  • Client site metadata (JSON file; see ./clients.json.example)
  • User metadata (not exposed to Client sites)
  • Audit logs

This is an area you're very likely to want to customize based on your existing ecosystem and deployment environment. Two storage examples are included: Postgres, an interface to PostgreSQL, and Redis, an interface to redis-server.

As a starting point, you may want to consider putting anything that should expire in Redis and everything permanent, like audit logs, in Postgres.

7.1 Abstraction

As per the component diagram, it's useful to think about storage on three levels:

  • DB abstraction: ./db/*.js provides functions for accessing specific kinds of data in an implementation agnostic way
  • Storage interface: ./lib/store.js provides interfaces to "real" databases, e.g. redis-server. It's relatively easy to add support for additional storage types, and to switch between them by changing config on a per-table basis (i.e. users and tokens can have different kinds of storage.)
  • Actual storage: Likely to vary between projects based on your specific situation and requirements.

7.2 General Storage Config

General storage can be configured independently for the following logical groups:

  • session: User sessions
  • codes: Authorization codes
  • tokens: Access tokens
  • refresh_tokens: Refresh tokens
  • users: User metadata
  • audit: Token grant audit records

One final type, default, provides a catchall.

Postgres Config

"storage": {
  "audit": {
    "type": "Postgres",
    "options": {
      "user": "postgres",
      "password": "",
      "database": "oauth2",
      "host": "127.0.0.1",
      "port": "5432",
      "ssl": true
    }
  },
}

Postgres storage depends on the postgresql database package (tested with 9.3.) and the Node pg module.

  • type: "Postgres" corresponds to the name of an object exported from ./lib/store.js
  • options.user: Postgres username
  • options.password: Postgres user password
  • options.database: Database name
  • options.host: Postgres server hostname
  • options.port: Postgres server port
  • options.ssl: Whether to connect via SSL

Redis Config

"storage": {
  "default": {
    "type": "Redis",
    "db": 0,
    "host": "127.0.0.1",
    "port": 6379,
    "ttl": 900,
    "options": {}
  }
}

Redis storage depends on the redis-server package (tested with 2.6.) and the Node redis and hiredis modules.

  • type: "Redis" corresponds to the name of an object exported from ./lib/store.js
  • db: Specific database instance
  • host: Redis server hostname
  • port: Redis server port
  • ttl: Expiry time of records (SETEX), or 0 for no expiry (SET)
  • options: Additional options to be passed to redis.createClient

7.3 Client Config

The list of accepted clients is stored in a JSON file on disk.

"client_credentials": {
  "file": "./clients.json"
}

Entries in the file define client credentials, redirect URIs and allowed grant types.

{
  "id": "2",
  "name": "Automated tests",
  "client_id": "test",
  "client_secret": "2aa0c27d4a452d6bbb87e1b175f8e67ce75c000f",
  "client_salt": "$4$3SByu9lP$nEyg3Ezxj+5BDsi8uAdwtTeU4Is$",
  "allow_code_grant": true,
  "allow_implicit_grant": true,
  "valid_redirects": [
    "http://localhost:8080/"
  ]
}
  • id: Unique internal client ID, which should never change
  • name: Name suitable for display to user during authorization step
  • client_id: Client ID as shared with the client site
  • client_secret: SHA-1 hash of client_id:<secret>:client_salt. We never store the clear text client secret
  • client_salt: Any random-ish string
  • allow_code_grant: true if Authorization Code Grants are allowed for this client
  • allow_implicit_grant: true if Implicit Grants are allowed for this client
  • valid_redirects: A list of the valid redirect URIs for a client. Locking clients down by redirect URI is a vital layer of protection against abuse.

8.0 Config System

The application's configuration system was designed with a couple of goals in mind:

  • It should support runtime reloading for at least some options (e.g. log levels)
  • It should be easy to override options based on your environment (e.g. dev/test/prod)

Environment Variables

export NODE_CONFIG_DIR=${NODE_CONFIG_DIR:=config}

Use NODE_CONFIG_DIR to change where the application looks for its configuration files. For production use this should probably be somewhere in /etc/.

export NODE_ENV=${NODE_ENV:=development}

Use NODE_ENV to load configuration overrides for particular hosts or platforms.

Loading

Configuration is loaded from (in order)

$NODE_CONFIG_DIR
  |-- default.json
  |-- $NODE_ENV.json
  `-- runtime.json

default.json should contain the bulk of your configuration, with host/environment specific overrides in $NODE_ENV.json. By convention runtime.json should only be used for overrides that you've applied manually, i.e. outside of a proper deployment cycle, but in reality a config reload will parse all three files, not just the latter.

Configuration can be reloaded at runtime by sending a SIGHUP to the parent process. After the configuration is reloaded it emits a loaded event.

In general configuration is either used once on startup and never again (e.g. webserver listen port) or accessed at runtime through config.get(), which always accesses the most recently loaded value (e.g. log levels).

9.0 Pattern: Auth Code Grant

The Authorization Code Grant is the preferred OAuth 2.0 pattern. It completely hides the access tokens from the Resource Owner (and User Agent), side-stepping many of the security issues inherent in Implicit Grants.

On the down side, Clients require server-side support for the back channel token exchange, meaning it can't be implemented entirely in-browser in JavaScript.

9.1 Obtaining Authorization

 

OAuth2.0 - Authorization Code Grant

See RFC 6749 for more

(A) Client site adds a "Log in with <X>" link

The client initiates the flow by directing the resource owner's user-agent to the authorization endpoint. The client includes its client identifier, requested scope, local state, and a redirection URI to which the authorization server will send the user-agent back once access is granted (or denied).

Link example:

https://oauth.example.com/authorize?
    response_type=code&
    client_id=MySite&
    redirect_uri=http%3A%2F%2Fmysite.com%2F&
    scope=profile&
    state=af87ed7f6b22
  • response_type: Must be code - you're asking it to grant you an authorization code, not an access token.
  • client_id: The client ID portion of the credentials issued by the Authorization Server. The client secret isn't used until you exchange the code for an actual access token in the back channel POST to /token.
  • redirect_uri: The location on your site where the user should land after the grant process completes. This URI must not include a URI fragment. All of the redirection URIs you intend to use must be pre-registered with the Authorization Server.
  • scope: Optional; defined by the Resource Server.
  • state: Recommended; an opaque value used by the Client site to maintain state between the request and callback. This is an important layer of defense against CSRF attacks.

 

(B) User authentication and consent

The authorization server authenticates the resource owner (via the user-agent) and establishes whether the resource owner grants or denies the client's access request.

 

(C) Code granted

Assuming the resource owner grants access, the authorization server redirects the user-agent back to the client using the redirection URI provided earlier (in the request or during client registration). The redirection URI includes an authorization code and any local state provided by the client earlier.

Redirection example:

http://mysite.com/?code=wglXO28xcX91Pthn&state=af87ed7f6b22
  • code: A one time code that the Client, mysite.com, can use to redeem an access token.
  • state: Any optional state that was passed to the original request.

 

(D) Access token request

The client requests an access token from the authorization server's token endpoint by including the authorization code received in the previous step. When making the request, the client authenticates with the authorization server. The client includes the redirection URI used to obtain the authorization code for verification.

POST example:

POST /token HTTP/1.1
Host: oauth.example.com
Accept-Encoding: gzip, deflate
Cookie:
User-Agent: node-superagent/0.18.0
Authorization: Basic dGVzdDpodW50ZXIy
Content-Type: application/json
Content-Length: 101
Connection: close

{"grant_type":"authorization_code","code":"wglXO28xcX91Pthn","redirect_uri":"http://mysite.com/"}
  • grant_type: Must be authorization_code.
  • code: The authorization code returned in (C).
  • redirect_uri: A valid URI as in (A).

The preferred means of Client authentication is through the HTTP Basic Authentication header as above (i.e. base64 <client_id>:<client_secret>), but it's also possible to include those credentials in the body of the POST:

POST /token HTTP/1.1
Host: oauth.example.com
Accept-Encoding: gzip, deflate
Cookie: 
User-Agent: node-superagent/0.18.0
Content-Type: application/json
Content-Length: 146
Connection: close

{"grant_type":"authorization_code","code":"wglXO28xcX91Pthn","redirect_uri":"http://mysite.com/","client_id":"MySite","client_secret":"hunter2"}

 

(E) Access token response

The authorization server authenticates the client, validates the authorization code, and ensures that the redirection URI received matches the URI used to redirect the client in step (C). If valid, the authorization server responds back with an access token and, optionally, a refresh token.
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
Date: Sun, 01 Jun 2014 04:11:19 GMT
Connection: keep-alive
Transfer-Encoding: chunked

{"access_token":"emnziC15S33dffGUoJsPqxc0C5GGW4XT2Hd18xINwoZW0og1TtTolpFA09O7YCqWaYC8pKDH38QZlqR0q3MgEsyj8O8A6dYqaMLSHBh2lWTVfghkd5BdaVxqCszwjlJX5Cm5IcXJfIErq75JWmVDgXcWij7NJ1eyRFG4mGmDNHjJ4gdi1jIxbcml8jCNRlAyx9wY81KB6hSsdCoaWpPqyIvXF98AyBEM0cPnhmYotrvCtKF2Zr1ge6CO8IS7Vevg","refresh_token":"cMaXWsvgoNzgqWfchTPyWJq3OvbGKDMJcsIzyg8nxQ5qQfasYvMfahfIGWMnCpt8FMC2xW26ALikq3JOSRB2ZVZ3gTdqkPJ1zramKc9srbkaMPA0yCTy8YASZYYO85wR0tArTYXBXOxJ4iytGCLbee7HIXUr4yvKdmvvhro08lXlOIIHoF3cIywHCHK7URZPCUtqnQZuSAZBXUztdeYM8MwAYXjR2RjShXUADvOlec2hbakYBjO2FOoh4d7KtoTc","expires_in":7200,"token_type":"Bearer"}
  • access_token: A short lived token (e.g. 2 hours) that can be used to access protected resources.
  • refresh_token: A longer lived token (e.g. 90 days) that can be exchanged for a new access_token/refresh_token pair.
  • expires_in: The number of seconds until the access token expires.
  • token_type: Always Bearer.

9.2 Using Refresh Tokens

 

OAuth2.0 - Authorization Code Grant

See RFC 6749 for more

(A) & (B) as before

(C) Client accesses protected resource

The client makes a protected resource request to the resource server by presenting the access token.

The access token appears in the HTTP Bearer Auth header.

GET /api/profile HTTP/1.1
Host: oauth.example.com
Accept-Encoding: gzip, deflate
Cookie: 
User-Agent: node-superagent/0.18.0
Authorization: Bearer emnziC15S33dffGUoJsPqxc0C5GGW4XT2Hd18xINwoZW0og1TtTolpFA09O7YCqWaYC8pKDH38QZlqR0q3MgEsyj8O8A6dYqaMLSHBh2lWTVfghkd5BdaVxqCszwjlJX5Cm5IcXJfIErq75JWmVDgXcWij7NJ1eyRFG4mGmDNHjJ4gdi1jIxbcml8jCNRlAyx9wY81KB6hSsdCoaWpPqyIvXF98AyBEM0cPnhmYotrvCtKF2Zr1ge6CO8IS7Vevg
Connection: close

 

(D) Protected resource response

The resource server validates the access token, and if valid, serves the request.

Note that in order to minimise the changes required to the Resource Server, the provider has the option of proxying these access requests through the Authorization Server, isolating the token validation logic from the Resource Server itself.

HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 52
Date: Tue, 03 Jun 2014 09:18:25 GMT
Connection: close

{"id":"f1d2d2f9", "first_name":"Bob", "last_name":"Smith", "email":"bob@example.com"}

Contact your specific provider for a specification of the available API endpoints and their associated response attributes.

 

(E) Subsequent resource request

Steps (C) and (D) repeat until the access token expires. If the client knows the access token expired, it skips to step (G); otherwise, it makes another protected resource request.

 

(F) Failure response

Since the access token is invalid, the resource server returns an invalid token error.
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Access-Control-Allow-Origin: *
WWW-Authenticate: Bearer realm="Users", error="invalid_token"
Date: Tue, 03 Jun 2014 09:18:25 GMT
Connection: close
Transfer-Encoding: chunked

Unauthorized

 

(G) Refresh token request

The client requests a new access token by authenticating with the authorization server and presenting the refresh token. The client authentication requirements are based on the client type and on the authorization server policies.

Note the presence of the Client's HTTP Basic Auth header.

POST /token HTTP/1.1
Host: oauth.example.com
Accept-Encoding: gzip, deflate
Cookie: 
User-Agent: node-superagent/0.18.0
Authorization: Basic dGVzdDpodW50ZXIy
Content-Type: application/json
Content-Length: 305
Connection: close

{"grant_type":"refresh_token","refresh_token":"cMaXWsvgoNzgqWfchTPyWJq3OvbGKDMJcsIzyg8nxQ5qQfasYvMfahfIGWMnCpt8FMC2xW26ALikq3JOSRB2ZVZ3gTdqkPJ1zramKc9srbkaMPA0yCTy8YASZYYO85wR0tArTYXBXOxJ4iytGCLbee7HIXUr4yvKdmvvhro08lXlOIIHoF3cIywHCHK7URZPCUtqnQZuSAZBXUztdeYM8MwAYXjR2RjShXUADvOlec2hbakYBjO2FOoh4d7KtoTc"}
  • grant_type: Must be refresh_token.
  • refresh_token: The token returned on a previous call to /token.

 

(H) New access token response

The authorization server authenticates the client and validates the refresh token, and if valid, issues a new access token (and, optionally, a new refresh token).
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
Date: Tue, 03 Jun 2014 09:18:25 GMT
Connection: close
Transfer-Encoding: chunked

{"access_token":"wPxG4ZEgXZmBG3rMRcb7tSE4Wp32f1hHp0TiVvB9RubhZ0KMLFXpiAMdtdW5mtx58lax29PKoCvwd7KYOf6pmWD70kNVDebCXeon4iW5RsrQkxKCpB5qOdtqoweHysAwettDBlVLWREYYiwYkrPqpd92cvFS9Q8SQwptj3iYeUgkFtT26YtrmivQq7lLkfICJsGApYVzA9PANyXps4Qq9I57HgpnryeS3yGkBIt3HCM6ZF2TcLgNBdMAg1sJMrRI","refresh_token":"cntSZunRNWGvq7R2Xyx8RJE5C9uRhUObCBIcWQxud1xtEWnWrrPSKwp2Xn1TH1HSdIdjXMWjR56S8rP4kh8inpRFGW3bbWNRgloej7uorIpp310KaxcIB8X5lPix8TuAHzwgAZRFpHkezQrKzSwTRLgCLuHszzw6QZIbhEYW6aA5e48BqRJbZ1B2YjRcCw9TykhBBp0eAusR3dbGKFrm8qvF53ZqhmWPjwtWsdNxqJTqyJ3IvfzpMCAUGiEP5p2D","expires_in":7200,"token_type":"Bearer"}