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.
RFC 6749 defines the abstract protocol flow as:
The participants are:
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:
> sudo add-apt-repository ppa:chris-lea/node.js > sudo apt-get update > sudo apt-get install nodejsGet the source
> git clone https://github.com/jlabusch/oauth2-server.git > cd oauth2-serverMake
> npm installRun
> npm test
See also npm [stop|start|restart]
.
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.
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:
auth_server.views.login
auth_server.views.dialog
EJS template
auth_server.views.review
EJS template
/api/
services will be highly specific to your
Resource Server.
"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 URLport
: Express webserver listen portssl
: Flag to toggle between HTTP and HTTPSssl_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 cookiesnum_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 foundationviews.login
: Body of authentication pageviews.dialog
: Body of authorization pageviews.review
: Body of review pageThe 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
/login
, used by your login form/user/info
, used by your client-side AJAX calls to get
user profile information/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:
next(null, {id: UID, site_token: T, ...})
next(null, false, {message: '...'})
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(...)
.
"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.
The application stores a few different kinds of state:
./clients.json.example
)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.
As per the component diagram, it's useful to think about storage on three levels:
./db/*.js
provides functions for accessing specific
kinds of data in an implementation agnostic way./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.)General storage can be configured independently for the following logical groups:
session
: User sessionscodes
: Authorization codestokens
: Access tokensrefresh_tokens
: Refresh tokensusers
: User metadataaudit
: Token grant audit recordsOne final type, default
, provides a catchall.
"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 usernameoptions.password
: Postgres user passwordoptions.database
: Database nameoptions.host
: Postgres server hostnameoptions.port
: Postgres server portoptions.ssl
: Whether to connect via SSL"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 instancehost
: Redis server hostnameport
: Redis server portttl
: Expiry time of records (SETEX), or 0 for no expiry (SET)options
: Additional options to be passed to redis.createClient
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 changename
: Name suitable for display to user during authorization stepclient_id
: Client ID as shared with the client siteclient_secret
: SHA-1 hash of client_id:<secret>:client_salt
.
We never store the clear text client secretclient_salt
: Any random-ish stringallow_code_grant
: true
if Authorization Code Grants are allowed for this clientallow_implicit_grant
: true
if Implicit Grants are allowed for this clientvalid_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.The application's configuration system was designed with a couple of goals in mind:
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.
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).
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.
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.
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.
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.
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"}
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
.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
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.
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.
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
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
.
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"}