├── LICENSE ├── README.md ├── grant.d.ts ├── index.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present, Simeon Velichkov (https://github.com/simov/grant) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # fastify-grant 3 | 4 | > _Fastify plugin for **[Grant][grant]** OAuth Proxy_ 5 | 6 | ```js 7 | var fastify = require('fastify') 8 | var cookie = require('fastify-cookie') 9 | var session = require('fastify-session') 10 | var grant = require('fastify-grant') 11 | 12 | fastify() 13 | .register(cookie) 14 | .register(session, {secret: 'grant', cookie: {secure: false}}) 15 | .register(grant({/*configuration - see below*/})) 16 | .listen(3000) 17 | ``` 18 | 19 | > _[Examples](https://github.com/simov/grant/tree/master/examples)_ 20 | 21 | ## ES Modules and TypeScript 22 | 23 | Import Grant in your `.mjs` files: 24 | 25 | ```js 26 | import fastify from 'fastify' 27 | import cookie from 'fastify-cookie' 28 | import session from 'fastify-session' 29 | import grant from 'fastify-grant' 30 | 31 | fastify() 32 | .register(cookie) 33 | .register(session, {secret: 'grant', cookie: {secure: false}}) 34 | .register(grant({/*configuration - see below*/})) 35 | .listen(3000) 36 | ``` 37 | 38 | Grant ships with extensive [type definitions][type-definitions] for TypeScript. However, a few additional type definitions that extend the typings of your HTTP framework of choice can be found [here][grant-types]. 39 | 40 | # Configuration 41 | 42 | 43 | ## Configuration: Basics 44 | 45 | ```json 46 | { 47 | "defaults": { 48 | "origin": "http://localhost:3000", 49 | "transport": "session", 50 | "state": true 51 | }, 52 | "google": { 53 | "key": "...", 54 | "secret": "...", 55 | "scope": ["openid"], 56 | "nonce": true, 57 | "custom_params": {"access_type": "offline"}, 58 | "callback": "/hello" 59 | }, 60 | "twitter": { 61 | "key": "...", 62 | "secret": "...", 63 | "callback": "/hi" 64 | } 65 | } 66 | ``` 67 | 68 | - **defaults** - default configuration for all providers 69 | - **origin** - where your client server can be reached `http://localhost:3000` | `https://site.com` ... 70 | - **transport** - a [transport](#callback-transport) used to deliver the [response data](#callback-response) in your `callback` route 71 | - **state** - generate random state string 72 | - **provider** - any [supported provider](#grant) `google` | `twitter` ... 73 | - **key** - `consumer_key` or `client_id` of your OAuth app 74 | - **secret** - `consumer_secret` or `client_secret` of your OAuth app 75 | - **scope** - array of OAuth scopes to request 76 | - **nonce** - generate random nonce string ([OpenID Connect](#connect-openid-connect) only) 77 | - **custom_params** - custom [authorization parameters](#connect-custom-parameters) 78 | - **callback** - relative route or absolute URL to receive the response data `/hello` | `https://site.com/hey` ... 79 | 80 | 81 | ## Configuration: Description 82 | 83 | Key | Location | Description 84 | :-| :-: | :- 85 | ***Authorization Server*** | 86 | **`request_url`** | [oauth.json] | OAuth 1.0a only, first step 87 | **`authorize_url`** | [oauth.json] | OAuth 2.0 first step, OAuth 1.0a second step 88 | **`access_url`** | [oauth.json] | OAuth 2.0 second step, OAuth 1.0a third step 89 | **`oauth`** | [oauth.json] | OAuth version number 90 | **`scope_delimiter`** | [oauth.json] | String delimiter used for concatenating multiple scopes 91 | **`token_endpoint_auth_method`** | `[provider]` | Authentication method for the token endpoint 92 | **`token_endpoint_auth_signing_alg`** | `[provider]` | Signing algorithm for the token endpoint 93 | ***Client Server*** | 94 | **`origin`** | `defaults` | Where your client server can be reached 95 | **`prefix`** | `defaults` | Path prefix for the Grant internal routes 96 | **`state`** | `defaults` | Random state string for OAuth 2.0 97 | **`nonce`** | `defaults` | Random nonce string for OpenID Connect 98 | **`pkce`** | `defaults` | Toggle PKCE support 99 | **`response`** | `defaults` | Response data to receive 100 | **`transport`** | `defaults` | A way to deliver the response data 101 | **`callback`** | `[provider]` | Relative or absolute URL to receive the response data 102 | **`overrides`** | `[provider]` | Static configuration overrides for a provider 103 | **`dynamic`** | `[provider]` | Configuration keys that can be overridden dynamically over HTTP 104 | ***Client App*** | 105 | **`key`** **`client_id`** **`consumer_key`** | `[provider]` | The `client_id` or `consumer_key` of your OAuth app 106 | **`secret`** **`client_secret`** **`consumer_secret`** | `[provider]` | The `client_secret` or `consumer_secret` of your OAuth app 107 | **`scope`** | `[provider]` | List of scopes to request 108 | **`custom_params`** | `[provider]` | Custom authorization parameters and their values 109 | **`subdomain`** | `[provider]` | String to embed into the authorization server URLs 110 | **`public_key`** | `[provider]` | Public PEM or JWK 111 | **`private_key`** | `[provider]` | Private PEM or JWK 112 | **`redirect_uri`** | `generated` | Absolute redirect URL of the OAuth app 113 | ***Grant*** | 114 | **`name`** | `generated` | Provider's [name](#grant) 115 | **`[provider]`** | `generated` | Provider's [name](#grant) as key 116 | **`profile_url`** | [profile.json] | User profile URL 117 | 118 | 119 | ## Configuration: Values 120 | 121 | Key | Location | Value 122 | :- | :-: | :-: 123 | ***Authorization Server*** | 124 | **`request_url`** | [oauth.json] | `'https://api.twitter.com/oauth/request_token'` 125 | **`authorize_url`** | [oauth.json] | `'https://api.twitter.com/oauth/authenticate'` 126 | **`access_url`** | [oauth.json] | `'https://api.twitter.com/oauth/access_token'` 127 | **`oauth`** | [oauth.json] | `2` `1` 128 | **`scope_delimiter`** | [oauth.json] | `','` `' '` 129 | **`token_endpoint_auth_method`** | `[provider]` | `'client_secret_post'` `'client_secret_basic'` `'private_key_jwt'` 130 | **`token_endpoint_auth_signing_alg`** | `[provider]` | `'RS256'` `'ES256'` `'PS256'` 131 | ***Client Server*** | 132 | **`origin`** | `defaults` | `'http://localhost:3000'` `https://site.com` 133 | **`prefix`** | `defaults` | `'/connect'` `/oauth` `''` 134 | **`state`** | `defaults` | `true` 135 | **`nonce`** | `defaults` | `true` 136 | **`pkce`** | `defaults` | `true` 137 | **`response`** | `defaults` | `['tokens', 'raw', 'jwt', 'profile']` 138 | **`transport`** | `defaults` | `'querystring'` `'session'` `'state'` 139 | **`callback`** | `[provider]` | `'/hello'` `'https://site.com/hi'` 140 | **`overrides`** | `[provider]` | `{something: {scope: ['..']}}` 141 | **`dynamic`** | `[provider]` | `['scope', 'subdomain']` 142 | ***Client App*** | 143 | **`key`** **`client_id`** **`consumer_key`** | `[provider]` | `'123'` 144 | **`secret`** **`client_secret`** **`consumer_secret`** | `[provider]` | `'123'` 145 | **`scope`** | `[provider]` | `['openid', '..']` 146 | **`custom_params`** | `[provider]` | `{access_type: 'offline'}` 147 | **`subdomain`** | `[provider]` | `'myorg'` 148 | **`public_key`** | `[provider]` | `'..PEM..'` `'{..JWK..}'` 149 | **`private_key`** | `[provider]` | `'..PEM..'` `'{..JWK..}'` 150 | **`redirect_uri`** |`generated` | `'http://localhost:3000/connect/twitter/callback'` 151 | ***Grant*** | 152 | **`name`** |`generated` | `name: 'twitter'` 153 | **`[provider]`** |`generated` | `twitter: true` 154 | **`profile_url`** | [profile.json] | `'https://api.twitter.com/1.1/users/show.json'` 155 | 156 | --- 157 | 158 | # Connect 159 | 160 | 161 | ## Connect: Origin 162 | 163 | ```json 164 | { 165 | "defaults": { 166 | "origin": "http://localhost:3000" 167 | } 168 | } 169 | ``` 170 | 171 | The `origin` is where your client server can be reached. 172 | 173 | You login by navigating to the `/connect/:provider` route where `:provider` is a key in your configuration, usually one of the [officially supported](#grant) ones, but you can define [your own](#misc-custom-providers) as well. Additionally you can login through a [static override](#connect-static-overrides) defined for that provider by navigating to the `/connect/:provider/:override?` route. 174 | 175 | ## Connect: Prefix 176 | 177 | By default Grant operates on the following two routes: 178 | 179 | ``` 180 | /connect/:provider/:override? 181 | /connect/:provider/callback 182 | ``` 183 | 184 | However, the default `/connect` prefix can be configured: 185 | 186 | ```json 187 | { 188 | "defaults": { 189 | "origin": "http://localhost:3000", 190 | "prefix": "/oauth" 191 | } 192 | } 193 | ``` 194 | 195 | 196 | ## Connect: Redirect URI 197 | 198 | The [`redirect_uri`](#misc-redirect-uri) of your OAuth app should follow this format: 199 | 200 | ``` 201 | [origin][prefix]/[provider]/callback 202 | ``` 203 | 204 | Where [`origin`](#connect-origin) and [`prefix`](#connect-prefix) have to match the ones set in your configuration, and [`provider`](#grant) is a provider key found in your configuration. 205 | 206 | For example: `http://localhost:3000/connect/google/callback` 207 | 208 | This redirect URI is used internally by Grant. Depending on the [`transport`](#callback-transport) being used you will receive the response data in the [`callback`](#callback-data) route or absolute URL configured for that provider. 209 | 210 | 211 | ## Connect: Custom Parameters 212 | 213 | Some providers may employ custom authorization parameters that you can configure using the `custom_params` key: 214 | 215 | ```json 216 | { 217 | "google": { 218 | "custom_params": {"access_type": "offline", "prompt": "consent"} 219 | }, 220 | "reddit": { 221 | "custom_params": {"duration": "permanent"} 222 | }, 223 | "trello": { 224 | "custom_params": {"name": "my app", "expiration": "never"} 225 | } 226 | } 227 | ``` 228 | 229 | 230 | ## Connect: OpenID Connect 231 | 232 | The `openid` scope is required, and generating a random `nonce` string is optional but recommended: 233 | 234 | ```json 235 | { 236 | "google": { 237 | "scope": ["openid"], 238 | "nonce": true 239 | } 240 | } 241 | ``` 242 | 243 | Grant **does not** verify the signature of the returned `id_token` by default. 244 | 245 | However, the following two claims of the `id_token` are being validated: 246 | 247 | 1. `aud` - is the token intended for my OAuth app? 248 | 2. `nonce` - does it tie to a request of my own? 249 | 250 | 251 | ## Connect: PKCE 252 | 253 | PKCE can be enabled for all providers or for a specific provider only: 254 | 255 | ```json 256 | { 257 | "google": { 258 | "pkce": true 259 | } 260 | } 261 | ``` 262 | 263 | Providers that do not support PKCE will ignore the additional parameters being sent. 264 | 265 | 266 | ## Connect: Static Overrides 267 | 268 | Provider sub configurations can be configured using the `overrides` key: 269 | 270 | ```json 271 | { 272 | "github": { 273 | "key": "...", "secret": "...", 274 | "scope": ["public_repo"], 275 | "callback": "/hello", 276 | "overrides": { 277 | "notifications": { 278 | "key": "...", "secret": "...", 279 | "scope": ["notifications"] 280 | }, 281 | "all": { 282 | "scope": ["repo", "gist", "user"], 283 | "callback": "/hey" 284 | } 285 | } 286 | } 287 | } 288 | ``` 289 | 290 | Navigate to: 291 | 292 | - `/connect/github` to request the public_repo `scope` 293 | - `/connect/github/notifications` to request the notifications `scope` using another OAuth App (`key` and `secret`) 294 | - `/connect/github/all` to request a bunch of `scope`s and also receive the response data in another `callback` route 295 | 296 | --- 297 | 298 | # Callback 299 | 300 | 301 | ## Callback: Data 302 | 303 | By default the response data will be returned in your `callback` route or absolute URL encoded as querystring. 304 | 305 | Depending on the [`transport`](#callback-transport) being used the response data can be returned in the `session` or in the `state` object instead. 306 | 307 | The amount of the returned data can be controlled through the [`response`](#callback-response) configuration. 308 | 309 | ### OAuth 2.0 310 | 311 | ```js 312 | { 313 | id_token: '...', 314 | access_token: '...', 315 | refresh_token: '...', 316 | raw: { 317 | id_token: '...', 318 | access_token: '...', 319 | refresh_token: '...', 320 | some: 'other data' 321 | } 322 | } 323 | ``` 324 | 325 | The `refresh_token` is optional. The `id_token` is returned only for [OpenID Connect](#connect-openid-connect) providers requesting the `openid` scope. 326 | 327 | 328 | ### OAuth 1.0a 329 | 330 | ```js 331 | { 332 | access_token: '...', 333 | access_secret: '...', 334 | raw: { 335 | oauth_token: '...', 336 | oauth_token_secret: '...', 337 | some: 'other data' 338 | } 339 | } 340 | ``` 341 | 342 | 343 | ### Error 344 | 345 | ```js 346 | { 347 | error: { 348 | some: 'error data' 349 | } 350 | } 351 | ``` 352 | 353 | 354 | ## Callback: Transport 355 | 356 | ### querystring 357 | 358 | By default Grant will encode the OAuth [response data](#callback-data) as `querystring` in your `callback` route or absolute URL: 359 | 360 | ```json 361 | { 362 | "github": { 363 | "callback": "https://site.com/hello" 364 | } 365 | } 366 | ``` 367 | 368 | This is useful when using Grant as [OAuth Proxy](#dynamic-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. 369 | 370 | ### session 371 | 372 | For local `callback` routes the session `transport` is recommended: 373 | 374 | ```json 375 | { 376 | "defaults": { 377 | "transport": "session" 378 | }, 379 | "github": { 380 | "callback": "/hello" 381 | } 382 | } 383 | ``` 384 | 385 | This will make the OAuth [response data](#callback-data) available in the `session` object instead: 386 | 387 | ```js 388 | req.session.grant.response // Fastify 389 | ``` 390 | 391 | ### state 392 | 393 | The request/response lifecycle `state` can be used as well: 394 | 395 | ```json 396 | { 397 | "defaults": { 398 | "transport": "state" 399 | } 400 | } 401 | ``` 402 | 403 | 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: 404 | 405 | ```js 406 | res.grant.response // Fastify 407 | ``` 408 | 409 | ## Callback: Response 410 | 411 | By default Grant returns all of the available tokens and the `raw` response data returned by the Authorization server: 412 | 413 | ```js 414 | { 415 | id_token: '...', 416 | access_token: '...', 417 | refresh_token: '...', 418 | raw: { 419 | id_token: '...', 420 | access_token: '...', 421 | refresh_token: '...', 422 | some: 'other data' 423 | } 424 | } 425 | ``` 426 | 427 | ### querystring 428 | 429 | When using the querystring [`transport`](#callback-transport) it might be a good idea to limit the response data: 430 | 431 | ```json 432 | { 433 | "defaults": { 434 | "response": ["tokens"] 435 | } 436 | } 437 | ``` 438 | 439 | This will return only the tokens available, without the `raw` response data. 440 | 441 | This is useful when using Grant as [OAuth Proxy](#dynamic-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. 442 | 443 | ### session 444 | 445 | Using the session [`transport`](#callback-transport) is generally safer, but it also depends on the implementation of your session store. 446 | 447 | 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. 448 | 449 | ```json 450 | { 451 | "google": { 452 | "response": ["tokens"] 453 | } 454 | } 455 | ``` 456 | 457 | This will return only the tokens available, without the `raw` response data. 458 | 459 | ### jwt 460 | 461 | Grant can also return even larger [response data](#callback-data) by including the decoded JWT for [OpenID Connect](#connect-openid-connect) providers that return `id_token`: 462 | 463 | ```json 464 | { 465 | "google": { 466 | "response": ["tokens", "raw", "jwt"] 467 | } 468 | } 469 | ``` 470 | 471 | This will make the decoded JWT available in the response data: 472 | 473 | ```js 474 | { 475 | id_token: '...', 476 | access_token: '...', 477 | refresh_token: '...', 478 | raw: { 479 | id_token: '...', 480 | access_token: '...', 481 | refresh_token: '...', 482 | some: 'other data' 483 | }, 484 | jwt: {id_token: {header: {}, payload: {}, signature: '...'}} 485 | } 486 | ``` 487 | 488 | Make sure you include all of the response keys that you want to be returned when configuring the `response` data explicitly. 489 | 490 | 491 | ### profile 492 | 493 | Outside of the regular OAuth flow, Grant can request the user profile as well: 494 | 495 | ```json 496 | { 497 | "google": { 498 | "response": ["tokens", "profile"] 499 | } 500 | } 501 | ``` 502 | 503 | Additionaly a `profile` key will be available in the response data: 504 | 505 | ```js 506 | { 507 | access_token: '...', 508 | refresh_token: '...', 509 | profile: {some: 'user data'} 510 | } 511 | ``` 512 | 513 | The `profile` key contains either the raw response data returned by the user profile endpoint or an error message. 514 | 515 | 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 `scope`s were requested. 516 | 517 | --- 518 | 519 | # Dynamic Configuration 520 | 521 | 522 | ## Dynamic: Instance 523 | 524 | Every Grant instance have a `config` property attached to it: 525 | 526 | ```js 527 | var grant = Grant(require('./config')) 528 | console.log(grant.config) 529 | ``` 530 | 531 | You can use the `config` property to alter the Grant's behavior during runtime without having to restart your server. 532 | 533 | This property contains the **generated** configuration used internally by Grant, and changes made to that configuration affects the **entire** Grant instance! 534 | 535 | 536 | ## Dynamic: State 537 | 538 | The request/response lifecycle state can be used to alter configuration on every request: 539 | 540 | ```js 541 | req.grant = {dynamic: {subdomain: 'usershop'}} // Fastify 542 | ``` 543 | 544 | 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. 545 | 546 | The request/response lifecycle state is not controlled by the [`dynamic`](#dynamic-http) configuration, meaning that you can override any configuration key. 547 | 548 | Any allowed [`dynamic`](#dynamic-http) configuration key sent through HTTP GET/POST request will override the identical one set using a state override. 549 | 550 | ## Dynamic: HTTP 551 | 552 | The `dynamic` configuration allows certain configuration keys to be set dynamically over HTTP GET/POST request. 553 | 554 | For example `shopify` requires your shop name to be embedded into the OAuth URLs, so it makes sense to allow the [`subdomain`](#subdomain-urls) configuration key to be set dynamically: 555 | 556 | ```json 557 | { 558 | "shopify": { 559 | "dynamic": ["subdomain"] 560 | } 561 | } 562 | ``` 563 | 564 | Then you can have a web form on your website allowing the user to specify the shop name: 565 | 566 | ```html 567 |
568 | 569 | 570 |
571 | ``` 572 | 573 | When making a `POST` request to the `/connect/:provider/:override?` route you have to mount a form body parser middleware before mounting Grant: 574 | 575 | ```js 576 | // fastify 577 | var parser = require('fastify-formbody') 578 | .register(parser) 579 | .register(grant(config)) 580 | ``` 581 | 582 | Alternatively you can make a `GET` request to the `/connect/:provider/:override?` route: 583 | 584 | ``` 585 | https://awesome.com/connect/shopify?subdomain=usershop 586 | ``` 587 | 588 | Any `dynamic` configuration sent over HTTP GET/POST request overrides any other configuration. 589 | 590 | 591 | [grant]: https://github.com/simov/grant 592 | [grant-oauth]: https://grant.outofindex.com 593 | 594 | [oauth.json]: https://github.com/simov/grant/blob/master/config/oauth.json 595 | [profile.json]: https://github.com/simov/grant/blob/master/config/profile.json 596 | [reserved-keys]: https://github.com/simov/grant/blob/master/config/reserved.json 597 | [examples]: https://github.com/simov/grant/tree/master/examples 598 | 599 | [grant-types]: https://github.com/simov/grant-types 600 | [type-definitions]: https://github.com/simov/grant/blob/master/grant.d.ts 601 | -------------------------------------------------------------------------------- /grant.d.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | GrantConfig, 4 | GrantOptions, 5 | FastifyMiddleware, 6 | GrantInstance 7 | } from 'grant' 8 | 9 | /** 10 | * Fastify handler 11 | */ 12 | export default function (config: GrantConfig | GrantOptions): FastifyMiddleware & GrantInstance 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('grant').fastify() 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-grant", 3 | "version": "5.4.8", 4 | "description": "Grant OAuth Proxy plugin for Fastify", 5 | "keywords": [ 6 | "oauth", 7 | "oauth2", 8 | "openid", 9 | "openid-connect", 10 | "authentication", 11 | "authorization", 12 | "proxy", 13 | "middleware", 14 | "plugin", 15 | "fastify" 16 | ], 17 | "license": "MIT", 18 | "homepage": "https://github.com/simov/fastify-grant", 19 | "author": "Simeon Velichkov (https://simov.github.io)", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/simov/fastify-grant.git" 23 | }, 24 | "dependencies": { 25 | "grant": "^5.4.8" 26 | }, 27 | "peerDependencies": { 28 | "fastify": ">=3.0.0" 29 | }, 30 | "main": "index.js", 31 | "type": "commonjs", 32 | "types": "grant.d.ts", 33 | "engines": { 34 | "node": ">=8.0.0" 35 | } 36 | } 37 | --------------------------------------------------------------------------------