Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Authentication Plugin #1314

Merged
merged 15 commits into from Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
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 @@ -359,6 +360,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
Expand Up @@ -148,6 +148,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
44 changes: 39 additions & 5 deletions src/events/http/HttpServer.js
Expand Up @@ -412,6 +412,39 @@ export default class HttpServer {
return authStrategyName
}

_setAuthorizationStrategy(endpoint, functionKey, method, path) {
medikoo marked this conversation as resolved.
Show resolved Hide resolved
/*
* 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
) {
// eslint-disable-next-line import/no-dynamic-require, global-require
const provider = require(customizations.offline
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@medikoo - Based on your earlier feedback on moving this away from a serverless plugin, let me know if you see a better way to inject this. My desire would be that in serverless.yml you'd be able to add just a regular import statement in
custom: offline: customAuthenticationProvider:
this makes for an inline 'require', unless you know of a better way?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This require will search in context of a directory in which this HttpServer.js file is placed. While ideally we should search against service directory (it's also how we load service local plugins).

For that you can rely on https://nodejs.org/api/module.html#modulecreaterequirefilename, as we do indirectly in Framework here: https://github.com/serverless/serverless/blob/480d74ec58c78466d2d055e5c2d73c2f4dde0b42/lib/classes/plugin-manager.js#L159

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@medikoo - thanks for the feedback. I knew that the search would be relative to the HttpServer and wasn't thrilled with the confusion this could create but I didn't know this could be modified via createRequire.

I think this matches your suggestions, let me know if it doesn't.

.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 +501,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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,24 @@
service: integration-tests

custom:
offline:
customAuthenticationProvider: '../../../tests/integration/custom-authentication/authenticationProvider.js'

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