fastify-grant
fastify-grant copied to clipboard
Fastify plugin for Grant OAuth Proxy
fastify-grant
Fastify plugin for Grant OAuth Proxy
var fastify = require('fastify')
var cookie = require('fastify-cookie')
var session = require('fastify-session')
var grant = require('fastify-grant')
fastify()
.register(cookie)
.register(session, {secret: 'grant', cookie: {secure: false}})
.register(grant({/*configuration - see below*/}))
.listen(3000)
ES Modules and TypeScript
Import Grant in your .mjs files:
import fastify from 'fastify'
import cookie from 'fastify-cookie'
import session from 'fastify-session'
import grant from 'fastify-grant'
fastify()
.register(cookie)
.register(session, {secret: 'grant', cookie: {secure: false}})
.register(grant({/*configuration - see below*/}))
.listen(3000)
Grant ships with extensive type definitions for TypeScript. However, a few additional type definitions that extend the typings of your HTTP framework of choice can be found here.
Configuration
Configuration: Basics
{
"defaults": {
"origin": "http://localhost:3000",
"transport": "session",
"state": true
},
"google": {
"key": "...",
"secret": "...",
"scope": ["openid"],
"nonce": true,
"custom_params": {"access_type": "offline"},
"callback": "/hello"
},
"twitter": {
"key": "...",
"secret": "...",
"callback": "/hi"
}
}
- defaults - default configuration for all providers
- origin - where your client server can be reached
http://localhost:3000|https://site.com... - transport - a transport used to deliver the response data in your
callbackroute - state - generate random state string
- origin - where your client server can be reached
- provider - any supported provider
google|twitter...- key -
consumer_keyorclient_idof your OAuth app - secret -
consumer_secretorclient_secretof your OAuth app - scope - array of OAuth scopes to request
- nonce - generate random nonce string (OpenID Connect only)
- custom_params - custom authorization parameters
- callback - relative route or absolute URL to receive the response data
/hello|https://site.com/hey...
- key -
Configuration: Description
| Key | Location | Description |
|---|---|---|
| Authorization Server | ||
request_url |
oauth.json | OAuth 1.0a only, first step |
authorize_url |
oauth.json | OAuth 2.0 first step, OAuth 1.0a second step |
access_url |
oauth.json | OAuth 2.0 second step, OAuth 1.0a third step |
oauth |
oauth.json | OAuth version number |
scope_delimiter |
oauth.json | String delimiter used for concatenating multiple scopes |
token_endpoint_auth_method |
[provider] |
Authentication method for the token endpoint |
token_endpoint_auth_signing_alg |
[provider] |
Signing algorithm for the token endpoint |
| Client Server | ||
origin |
defaults |
Where your client server can be reached |
prefix |
defaults |
Path prefix for the Grant internal routes |
state |
defaults |
Random state string for OAuth 2.0 |
nonce |
defaults |
Random nonce string for OpenID Connect |
pkce |
defaults |
Toggle PKCE support |
response |
defaults |
Response data to receive |
transport |
defaults |
A way to deliver the response data |
callback |
[provider] |
Relative or absolute URL to receive the response data |
overrides |
[provider] |
Static configuration overrides for a provider |
dynamic |
[provider] |
Configuration keys that can be overridden dynamically over HTTP |
| Client App | ||
key client_id consumer_key |
[provider] |
The client_id or consumer_key of your OAuth app |
secret client_secret consumer_secret |
[provider] |
The client_secret or consumer_secret of your OAuth app |
scope |
[provider] |
List of scopes to request |
custom_params |
[provider] |
Custom authorization parameters and their values |
subdomain |
[provider] |
String to embed into the authorization server URLs |
public_key |
[provider] |
Public PEM or JWK |
private_key |
[provider] |
Private PEM or JWK |
redirect_uri |
generated |
Absolute redirect URL of the OAuth app |
| Grant | ||
name |
generated |
Provider's name |
[provider] |
generated |
Provider's name as key |
profile_url |
profile.json | User profile URL |
Configuration: Values
| Key | Location | Value |
|---|---|---|
| Authorization Server | ||
request_url |
oauth.json | 'https://api.twitter.com/oauth/request_token' |
authorize_url |
oauth.json | 'https://api.twitter.com/oauth/authenticate' |
access_url |
oauth.json | 'https://api.twitter.com/oauth/access_token' |
oauth |
oauth.json | 2 1 |
scope_delimiter |
oauth.json | ',' ' ' |
token_endpoint_auth_method |
[provider] |
'client_secret_post' 'client_secret_basic' 'private_key_jwt' |
token_endpoint_auth_signing_alg |
[provider] |
'RS256' 'ES256' 'PS256' |
| Client Server | ||
origin |
defaults |
'http://localhost:3000' https://site.com |
prefix |
defaults |
'/connect' /oauth '' |
state |
defaults |
true |
nonce |
defaults |
true |
pkce |
defaults |
true |
response |
defaults |
['tokens', 'raw', 'jwt', 'profile'] |
transport |
defaults |
'querystring' 'session' 'state' |
callback |
[provider] |
'/hello' 'https://site.com/hi' |
overrides |
[provider] |
{something: {scope: ['..']}} |
dynamic |
[provider] |
['scope', 'subdomain'] |
| Client App | ||
key client_id consumer_key |
[provider] |
'123' |
secret client_secret consumer_secret |
[provider] |
'123' |
scope |
[provider] |
['openid', '..'] |
custom_params |
[provider] |
{access_type: 'offline'} |
subdomain |
[provider] |
'myorg' |
public_key |
[provider] |
'..PEM..' '{..JWK..}' |
private_key |
[provider] |
'..PEM..' '{..JWK..}' |
redirect_uri |
generated |
'http://localhost:3000/connect/twitter/callback' |
| Grant | ||
name |
generated |
name: 'twitter' |
[provider] |
generated |
twitter: true |
profile_url |
profile.json | 'https://api.twitter.com/1.1/users/show.json' |
Connect
Connect: Origin
{
"defaults": {
"origin": "http://localhost:3000"
}
}
The origin is where your client server can be reached.
You login by navigating to the /connect/:provider route where :provider is a key in your configuration, usually one of the officially supported ones, but you can define your own as well. Additionally you can login through a static override defined for that provider by navigating to the /connect/:provider/:override? route.
Connect: Prefix
By default Grant operates on the following two routes:
/connect/:provider/:override?
/connect/:provider/callback
However, the default /connect prefix can be configured:
{
"defaults": {
"origin": "http://localhost:3000",
"prefix": "/oauth"
}
}
Connect: Redirect URI
The redirect_uri of your OAuth app should follow this format:
[origin][prefix]/[provider]/callback
Where origin and prefix have to match the ones set in your configuration, and provider is a provider key found in your configuration.
For example: http://localhost:3000/connect/google/callback
This redirect URI is used internally by Grant. Depending on the transport being used you will receive the response data in the callback route or absolute URL configured for that provider.
Connect: Custom Parameters
Some providers may employ custom authorization parameters that you can configure using the custom_params key:
{
"google": {
"custom_params": {"access_type": "offline", "prompt": "consent"}
},
"reddit": {
"custom_params": {"duration": "permanent"}
},
"trello": {
"custom_params": {"name": "my app", "expiration": "never"}
}
}
Connect: OpenID Connect
The openid scope is required, and generating a random nonce string is optional but recommended:
{
"google": {
"scope": ["openid"],
"nonce": true
}
}
Grant does not verify the signature of the returned id_token by default.
However, the following two claims of the id_token are being validated:
aud- is the token intended for my OAuth app?nonce- does it tie to a request of my own?
Connect: PKCE
PKCE can be enabled for all providers or for a specific provider only:
{
"google": {
"pkce": true
}
}
Providers that do not support PKCE will ignore the additional parameters being sent.
Connect: Static Overrides
Provider sub configurations can be configured using the overrides key:
{
"github": {
"key": "...", "secret": "...",
"scope": ["public_repo"],
"callback": "/hello",
"overrides": {
"notifications": {
"key": "...", "secret": "...",
"scope": ["notifications"]
},
"all": {
"scope": ["repo", "gist", "user"],
"callback": "/hey"
}
}
}
}
Navigate to:
/connect/githubto request the public_reposcope/connect/github/notificationsto request the notificationsscopeusing another OAuth App (keyandsecret)/connect/github/allto request a bunch ofscopes and also receive the response data in anothercallbackroute
Callback
Callback: Data
By default the response data will be returned in your callback route or absolute URL encoded as querystring.
Depending on the transport being used the response data can be returned in the session or in the state object instead.
The amount of the returned data can be controlled through the response configuration.
OAuth 2.0
{
id_token: '...',
access_token: '...',
refresh_token: '...',
raw: {
id_token: '...',
access_token: '...',
refresh_token: '...',
some: 'other data'
}
}
The refresh_token is optional. The id_token is returned only for OpenID Connect providers requesting the openid scope.
OAuth 1.0a
{
access_token: '...',
access_secret: '...',
raw: {
oauth_token: '...',
oauth_token_secret: '...',
some: 'other data'
}
}
Error
{
error: {
some: 'error data'
}
}
Callback: Transport
querystring
By default Grant will encode the OAuth response data as querystring in your callback route or absolute URL:
{
"github": {
"callback": "https://site.com/hello"
}
}
This is useful when using Grant as OAuth Proxy. However this final https://site.com/hello?access_token=... redirect can potentially leak private data in your server logs, especially when sitting behind a reverse proxy.
session
For local callback routes the session transport is recommended:
{
"defaults": {
"transport": "session"
},
"github": {
"callback": "/hello"
}
}
This will make the OAuth response data available in the session object instead:
req.session.grant.response // Fastify
state
The request/response lifecycle state can be used as well:
{
"defaults": {
"transport": "state"
}
}
In this case a callback route is not needed, and it will be ignored if provided. The response data will be available in the request/response lifecycle state object instead:
res.grant.response // Fastify
Callback: Response
By default Grant returns all of the available tokens and the raw response data returned by the Authorization server:
{
id_token: '...',
access_token: '...',
refresh_token: '...',
raw: {
id_token: '...',
access_token: '...',
refresh_token: '...',
some: 'other data'
}
}
querystring
When using the querystring transport it might be a good idea to limit the response data:
{
"defaults": {
"response": ["tokens"]
}
}
This will return only the tokens available, without the raw response data.
This is useful when using Grant as OAuth Proxy. Encoding potentially large amounts of data as querystring can lead to incompatibility issues with some servers and browsers, and generally is considered a bad practice.
session
Using the session transport is generally safer, but it also depends on the implementation of your session store.
In case your session store encodes the entire session in a cookie, not just the session ID, some servers may reject the HTTP request because of HTTP headers size being too big.
{
"google": {
"response": ["tokens"]
}
}
This will return only the tokens available, without the raw response data.
jwt
Grant can also return even larger response data by including the decoded JWT for OpenID Connect providers that return id_token:
{
"google": {
"response": ["tokens", "raw", "jwt"]
}
}
This will make the decoded JWT available in the response data:
{
id_token: '...',
access_token: '...',
refresh_token: '...',
raw: {
id_token: '...',
access_token: '...',
refresh_token: '...',
some: 'other data'
},
jwt: {id_token: {header: {}, payload: {}, signature: '...'}}
}
Make sure you include all of the response keys that you want to be returned when configuring the response data explicitly.
profile
Outside of the regular OAuth flow, Grant can request the user profile as well:
{
"google": {
"response": ["tokens", "profile"]
}
}
Additionaly a profile key will be available in the response data:
{
access_token: '...',
refresh_token: '...',
profile: {some: 'user data'}
}
The profile key contains either the raw response data returned by the user profile endpoint or an error message.
Not all of the supported providers have their profile_url set, and some of them might require custom parameters. Usually the user profile endpoint is accessible only when certain scopes were requested.
Dynamic Configuration
Dynamic: Instance
Every Grant instance have a config property attached to it:
var grant = Grant(require('./config'))
console.log(grant.config)
You can use the config property to alter the Grant's behavior during runtime without having to restart your server.
This property contains the generated configuration used internally by Grant, and changes made to that configuration affects the entire Grant instance!
Dynamic: State
The request/response lifecycle state can be used to alter configuration on every request:
req.grant = {dynamic: {subdomain: 'usershop'}} // Fastify
This is useful in cases when you want to configure Grant dynamically with potentially sensitive data that you don't want to send over HTTP.
The request/response lifecycle state is not controlled by the dynamic configuration, meaning that you can override any configuration key.
Any allowed dynamic configuration key sent through HTTP GET/POST request will override the identical one set using a state override.
Dynamic: HTTP
The dynamic configuration allows certain configuration keys to be set dynamically over HTTP GET/POST request.
For example shopify requires your shop name to be embedded into the OAuth URLs, so it makes sense to allow the subdomain configuration key to be set dynamically:
{
"shopify": {
"dynamic": ["subdomain"]
}
}
Then you can have a web form on your website allowing the user to specify the shop name:
<form action="/connect/shopify" method="POST" accept-charset="utf-8">
<input type="text" name="subdomain" value="" />
<button>Login</button>
</form>
When making a POST request to the /connect/:provider/:override? route you have to mount a form body parser middleware before mounting Grant:
// fastify
var parser = require('fastify-formbody')
.register(parser)
.register(grant(config))
Alternatively you can make a GET request to the /connect/:provider/:override? route:
https://awesome.com/connect/shopify?subdomain=usershop
Any dynamic configuration sent over HTTP GET/POST request overrides any other configuration.