Skip to content

Commit

Permalink
feat: Support injection of custom authentication strategy (#1314)
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-stclair authored Feb 11, 2022
1 parent a5158a4 commit febfe77
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 5 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ This plugin is updated by its users, I just do maintenance and ensure that PRs a
- [Custom authorizers](#custom-authorizers)
- [Remote authorizers](#remote-authorizers)
- [JWT authorizers](#jwt-authorizers)
- [Serverless plugin authorizers](#serverless-plugin-authorizers)
- [Custom headers](#custom-headers)
- [Environment variables](#environment-variables)
- [AWS API Gateway Features](#aws-api-gateway-features)
Expand Down Expand Up @@ -360,6 +361,27 @@ defined in the `serverless.yml` can be used to validate the token and scopes in
the signature of the JWT is not validated with the defined issuer. Since this is a security risk, this feature is
only enabled with the `--ignoreJWTSignature` flag. Make sure to only set this flag for local development work.

## Serverless plugin authorizers

If your authentication needs are custom and not satisfied by the existing capabilities of the Serverless offline project, you can inject your own authentication strategy. To inject a custom strategy for Lambda invocation, you define a custom variable under `serverless-offline` called `authenticationProvider` in the serverless.yml file. The value of the custom variable will be used to `require(your authenticationProvider value)` where the location is expected to return a function with the following signature.

```js
module.exports = function (endpoint, functionKey, method, path) {
return {
name: 'your strategy name',
scheme: 'your scheme name',

getAuthenticateFunction: () => ({
async authenticate(request, h) {
// your implementation
},
}),
}
}
```

A working example of injecting a custom authorization provider can be found in the projects integration tests under the folder `custom-authentication`.

## Custom headers

You are able to use some custom headers in your request to gain more control over the requestContext object.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"Stewart Gleadow (https://github.com/sgleadow)",
"Thales Minussi (https://github.com/tminussi)",
"Thang Minh Vu (https://github.com/ittus)",
"Tom St. Clair (https://github.com/tom-stclair)",
"Trevor Leach (https://github.com/trevor-leach)",
"Tuan Minh Huynh (https://github.com/tuanmh)",
"Utku Turunc (https://github.com/utkuturunc)",
Expand Down
53 changes: 48 additions & 5 deletions src/events/http/HttpServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { readFileSync } from 'fs'
import { join, resolve } from 'path'
import h2o2 from '@hapi/h2o2'
import { Server } from '@hapi/hapi'
import { createRequire } from 'module'
import * as pathUtils from 'path'
import authFunctionNameExtractor from './authFunctionNameExtractor.js'
import authJWTSettingsExtractor from './authJWTSettingsExtractor.js'
import createAuthScheme from './createAuthScheme.js'
Expand Down Expand Up @@ -412,6 +414,46 @@ export default class HttpServer {
return authStrategyName
}

_setAuthorizationStrategy(endpoint, functionKey, method, path) {
/*
* The authentication strategy can be provided outside of this project
* by injecting the provider through a custom variable in the serverless.yml.
*
* see the example in the tests for more details
* /tests/integration/custom-authentication
*/
const customizations = this.#serverless.service.custom
if (
customizations &&
customizations.offline?.customAuthenticationProvider
) {
const root = pathUtils.resolve(
this.#serverless.serviceDir,
'require-resolver',
)
const customRequire = createRequire(root)

const provider = customRequire(
customizations.offline.customAuthenticationProvider,
)

const strategy = provider(endpoint, functionKey, method, path)
this.#server.auth.scheme(
strategy.scheme,
strategy.getAuthenticateFunction,
)
this.#server.auth.strategy(strategy.name, strategy.scheme)
return strategy.name
}

// If the endpoint has an authorization function, create an authStrategy for the route
const authStrategyName = this.#options.noAuth
? null
: this._configureJWTAuthorization(endpoint, functionKey, method, path) ||
this._configureAuthorization(endpoint, functionKey, method, path)
return authStrategyName
}

createRoutes(functionKey, httpEvent, handler) {
const [handlerPath] = splitHandlerPathAndName(handler)

Expand Down Expand Up @@ -468,11 +510,12 @@ export default class HttpServer {
invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
})

// If the endpoint has an authorization function, create an authStrategy for the route
const authStrategyName = this.#options.noAuth
? null
: this._configureJWTAuthorization(endpoint, functionKey, method, path) ||
this._configureAuthorization(endpoint, functionKey, method, path)
const authStrategyName = this._setAuthorizationStrategy(
endpoint,
functionKey,
method,
path,
)

let cors = null
if (endpoint.cors) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import fetch from 'node-fetch'
import { resolve } from 'path'
import { joinUrl, setup, teardown } from '../_testHelpers/index.js'

jest.setTimeout(30000)

describe('custom authentication serverless-offline variable tests', () => {
// init
beforeAll(() =>
setup({
servicePath: resolve(__dirname),
}),
)

// cleanup
afterAll(() => teardown())

//
;[
{
description:
'should return custom payload from injected authentication provider',
path: '/echo',
status: 200,
},
].forEach(({ description, path, status }) => {
test(description, async () => {
const url = joinUrl(TEST_BASE_URL, path)

const response = await fetch(url)
expect(response.status).toEqual(status)

const json = await response.json()
expect(json.event.requestContext.authorizer.expected).toEqual('it works')
})
})
})
18 changes: 18 additions & 0 deletions tests/integration/custom-authentication/authenticationProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// eslint-disable-next-line no-unused-vars
module.exports = (endpoint, functionKey, method, path) => {
return {
name: 'strategy-name',
scheme: 'scheme',

getAuthenticateFunction: () => ({
async authenticate(request, h) {
const context = { expected: 'it works' }
return h.authenticated({
credentials: {
context,
},
})
},
}),
}
}
8 changes: 8 additions & 0 deletions tests/integration/custom-authentication/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict'

const { stringify } = JSON

exports.echo = async function echo(event, context) {
const data = { event, context }
return stringify(data)
}
24 changes: 24 additions & 0 deletions tests/integration/custom-authentication/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
service: integration-tests

custom:
offline:
customAuthenticationProvider: './authenticationProvider'

plugins:
- ../../../

provider:
memorySize: 128
name: aws
region: us-east-1 # default
runtime: nodejs12.x
stage: dev
versionFunctions: false

functions:
echo:
events:
- httpApi:
method: get
path: echo
handler: handler.echo

0 comments on commit febfe77

Please sign in to comment.