@celastrina/http
Advanced tools
Comparing version 1.1.1-0.1beta to 1.1.1-0.20beta
@@ -263,7 +263,6 @@ /* | ||
* @param {string} name | ||
* @param {boolean} secure | ||
* @param {null|string} defaultValue | ||
*/ | ||
constructor(name, secure = false, defaultValue = null) { | ||
super(name, secure, defaultValue); | ||
constructor(name, defaultValue = null) { | ||
super(name, defaultValue); | ||
} | ||
@@ -434,7 +433,6 @@ | ||
* @param {string} name | ||
* @param {boolean} secure | ||
* @param {null|string} defaultValue | ||
*/ | ||
constructor(name, secure = false, defaultValue = null) { | ||
super(name, secure, defaultValue); | ||
constructor(name, defaultValue = null) { | ||
super(name, defaultValue); | ||
} | ||
@@ -1127,3 +1125,3 @@ | ||
/** | ||
* @type {CookieRoleResolver} | ||
* @type {CookieSessionResolver} | ||
*/ | ||
@@ -1167,7 +1165,6 @@ class SecureCookieSessionResolver extends CookieSessionResolver { | ||
* @param {string} name | ||
* @param {boolean} secure | ||
* @param {null|string} defaultValue | ||
*/ | ||
constructor(name, secure = false, defaultValue = null) { | ||
super(name, secure, defaultValue); | ||
constructor(name, defaultValue = null) { | ||
super(name, defaultValue); | ||
} | ||
@@ -1203,7 +1200,6 @@ | ||
* @param {string} name | ||
* @param {boolean} secure | ||
* @param {null|string} defaultValue | ||
*/ | ||
constructor(name, secure = false, defaultValue = null) { | ||
super(name, secure, defaultValue); | ||
constructor(name, defaultValue = null) { | ||
super(name, defaultValue); | ||
} | ||
@@ -1210,0 +1206,0 @@ |
{ | ||
"name": "@celastrina/http", | ||
"version": "1.1.10.1beta", | ||
"version": "1.1.10.20beta", | ||
"description": "HTTP Function Package for Celastrina", | ||
@@ -31,3 +31,3 @@ "main": "index.js", | ||
"dependencies": { | ||
"@celastrina/core": "^1.1.10beta", | ||
"@celastrina/core": "^1.1.10.20beta", | ||
"adal-node": "^0.2.1", | ||
@@ -34,0 +34,0 @@ "axios": "^0.19.2", |
684
README.md
@@ -1,5 +0,5 @@ | ||
# celastrina | ||
#celastrina | ||
Javascript Framework for simplifying Microsoft Azure Functions and supporting resources. | ||
## Who should use celastrina.js? | ||
##Who should use celastrina.js? | ||
Right now, to receive the highest time-to-value, celastrina.js should be used in green-field application development. | ||
@@ -11,9 +11,15 @@ While stable and well-supported, Celastrina.js has not been out in the wild very long; because of this, configurations | ||
## Prerequisite | ||
##Prerequisite | ||
1. You gotta know Azure, at least a little. | ||
2. You gotta know Javascript and Node.js, duh. | ||
## Quick-start | ||
To use Celastring.js simply deploy the following to your Microsoft Azure HTTP Trigger function: | ||
##Recent Changes | ||
1. Removed `managed` mode from the `Configuration`. Adopting Azure Key Vault and App Configuration changed how to | ||
managed resources are handled internally and now no longer required indicating the system uses managed services like | ||
Vault. | ||
2. Support for Application Settings, App Configurations, and a hybrid Application Settings and Vault. | ||
##Quick-start | ||
To use Celastrina.js simply deploy the following to your Microsoft Azure HTTP Trigger function: | ||
``` | ||
@@ -25,4 +31,3 @@ "use strict"; | ||
const config = new Configuration(new StringProperty("function.name"), | ||
new BooleanProperty("function.managed")); | ||
const config = new Configuration(new StringProperty("function.name")); | ||
@@ -32,3 +37,3 @@ class MyNewHTTPTriggerFunction extends JSONHTTPFunction { | ||
return new Promise((resolve, reject) => { | ||
context.send({"message": "_get invoked."}); | ||
context.send({message: "_get invoked."}); | ||
resolve(); | ||
@@ -40,3 +45,3 @@ }); | ||
return new Promise((resolve, reject) => { | ||
context.send({"message": "_post invoked."}); | ||
context.send({message: "_post invoked."}); | ||
resolve(); | ||
@@ -51,5 +56,8 @@ }); | ||
### function.json | ||
If you are feeling brave, or just here for a refresher, you can skip down to [Putting it all together](#Putting-it-all-together) | ||
and dive right in. | ||
###function.json | ||
Update you r Microsoft Azure function.json file with the following directive "entryPoint": "execute". Your in and out | ||
bdinges should be named "req" and "res" respectively. Your function.json should look something like this: | ||
bdinges should be named `req` and `res` respectively. Your `function.json` should look something like this: | ||
@@ -79,3 +87,655 @@ ``` | ||
## Detailed Help & Documentation | ||
##Detailed Help & Documentation | ||
Please visit https://www.celastrinajs.com for complete documentation on using Celastrina.js. | ||
###Environment Properties | ||
Celastrina.js leverages Microsoft Azure Function application settings a little differently. Celastrina.js uses a | ||
confiuration object (`Configuration`) that is linked through `Property` instances. A property represents a key=value pair in the | ||
`process.env`. The `Property` paradigm is a little different as it enfources some degree of type-safety as well as a | ||
way to secure key=value pairs using Microsoft Azure Vault. | ||
Celastrina.js core comes with 4 out-of-the-box `Property` types: | ||
1. `NumericProperty`: `key=value` pair representing a Numberic data-type. | ||
2. `BooleanProperty`: `key=value` pair representing a true/false data type. The `BooleanProperty` simple looks for | ||
`string === "true"` for `true`, `false` otherwise. | ||
3. `StringProperty`: `key=value` pair for string, duh. | ||
4. `JsonProperty`: A serialized JavaScript object parsed using `JSON.parse()`. | ||
This HTTP package also includes specialized property types will discuss later in this document. Celastrina.js takes | ||
advantage of varient-type nature of JavaScript by allowing ANY configuration element to be a primitive type, or a | ||
`Property` instance. All `Property` instances are converted to thier respective types during the bootstrp life-cycle of | ||
the Celastrina.js function. More on life-cycle later. | ||
A `Property` instance has the following constructor: | ||
``` | ||
constructor(name, defaultValue = null) | ||
``` | ||
- `name` {`string`}: The name, or KEY, of the property in the process.env. This paramter is required and cannot be | ||
undefined. | ||
- `defaultValue` {`*`}: The default value to use if the entry in process.env is `null` or `unddefined. The super | ||
class `Property` will accept Any (`*`) value but implentations such as `StringProperty` or `BooleanProperty` will enforce thier | ||
respective types. This parameter is optional and will default to `null`. | ||
####Using a Property | ||
To use a `Property`, add to your function application settings through the Azure portal at _All services > Function App > | ||
\[Your function App\] > Configuration_ and in your local.settings.json. | ||
``` | ||
{ | ||
"IsEncrypted": false, | ||
"Values": { | ||
"function.name": "Your Function Name" | ||
} | ||
} | ||
``` | ||
Next, you need create a property instance and add it to a configuration. A `Configuration` has 1 required parameter passed | ||
in the constructor: | ||
- `name` {`string`}: The firendly name of this function. This name is used for diagnostics, insights, and logging and | ||
should not be seen by the caller. | ||
As stated you may use the primitive types or corrosponding `Property` instances. | ||
``` | ||
const config = new Configuration("Your Function Name"); | ||
/* OR */ | ||
const config = new Configuration(new StringProperty("function.name")); | ||
``` | ||
Let's unpack this real quick. The name parameter in the later example uses a `StringProperty` with a key that points to | ||
"function.name" in process.env, equivelent to calling `process.env["function.name"]`: | ||
``` | ||
new StringProperty("function.name") | ||
``` | ||
At runtime, when the `execute` function of the class is invoked, this `StringProperty` will be converted to a `string` from | ||
the `process.env`. | ||
Pretty straight forward,eh? The core `Configuration` of Celastrina.js comes with numerous out-of-the-box attributes | ||
including `name` and `managed`. You can set these attributes using the `Configuration.addValue` method or using explicit | ||
setter methods. The following explicit attributes are supported: | ||
- `addApplicationAuthorization` {`ApplicationAuthorization`} | ||
- `addResourceAuthorization` {`string`} | ||
- `addFunctionRole` {`FunctionRole`} | ||
More on what these are later... The `Configuration` is only available during the `bootstrap` life-cycle. After that, any | ||
environment properties can be accessed from the `BaseContext` object. | ||
#### The Celastrina.js Life-Cycle | ||
Celastrina.js follows a fairly straight forward life-cycle of promises to execute work. The basic life-cycle executed | ||
when the 'execute' method is invoked in BaseFunction is, in order, as follows: | ||
1. `bootstrap`: This is a critical life-cycle phase. This phase creates objects and dependencies necessary for the | ||
function to work. This is the only phase, that if overridden, should always call the super method on. | ||
2. `intialize`: Implementors should override this function to manage dependencies. | ||
3. `authenticate`: Phase used to authenticate requests. The base implementation forwards this call to the `BaseSentry`. | ||
4. `authorize`: Authorizes (role verification) requests. The base implementation forwards this call to the `BaseSentry`. | ||
5. `validate`: Phase used to validate any request input like headers, query params, request body. | ||
6. `load`: Load from a data store any object you might need to fulfill this request. I use it all the | ||
time to pull documents from Azure Cosmos DB from a URI parameter and place an object into the context. | ||
7. Processing: Depends on whether this request is a monitor (HTTP trace) request. If its a monitor request then | ||
`monitor` is invoked, otherwise `process` is invoked. This phase is where you performs all those precious busine$$ | ||
rules you are getting paid for. | ||
8. `save`: Inverse of load! LOL Commit back to your data store all the preciou$ information you gathered from your user | ||
base. | ||
9. `terminate`: Clean up anything you might have created during `initialize`. | ||
There is a special phase that can happen anywhere from `initialize` to `terminate`, and that is the `exception` phase. | ||
This is invoked when any of the life-cycle promises are rejected. If the `exception` life-cycle is invoked prior to | ||
`terminate` then terminate will still be invoked. If an exception happens during `createContext` then `terminate` will | ||
**not** be invoked as the context might not have been safely instantiated. If an exception happens within `exception` or | ||
`terminate` then an "unhandled" condition is met, and an error is returned via the Azure `context.done()` | ||
In the processing phase, the HTTP package looks at the HTTP method in the request and invokes the `_[method]` function | ||
in the class. For example, if the HTTP method is GET, then `_get(context)` is invoked. A typical HTTP Trigger function | ||
might look like: | ||
``` | ||
class MyNewHTTPTriggerFunction extends JSONHTTPFunction { | ||
async initialize(context) { | ||
return new Promise((resolve, reject) => { | ||
// Do some initialization stuff | ||
resolve(); | ||
}); | ||
} | ||
async load(context) { | ||
return new Promise((resolve, reject) => { | ||
// Load some objects from your data store | ||
resolve(); | ||
}); | ||
} | ||
async _get(context) { | ||
return new Promise((resolve, reject) => { | ||
context.send({message: "_get invoked."}); | ||
resolve(); | ||
}); | ||
} | ||
async _post(context) { | ||
return new Promise((resolve, reject) => { | ||
context.send({message: "_post invoked."}); | ||
resolve(); | ||
}); | ||
} | ||
async save(context) { | ||
return new Promise((resolve, reject) => { | ||
// Save some objects to your data store | ||
resolve(); | ||
}); | ||
} | ||
async terminate(context) { | ||
return new Promise((resolve, reject) => { | ||
// Do your cleanup. | ||
resolve(); | ||
}); | ||
} | ||
} | ||
module.exports = new MyNewHTTPTriggerFunction(config); | ||
``` | ||
###So, How do I use Azure Key Vault or App Configuration Service? | ||
Glad you asked! | ||
**COMING SOON** | ||
####Back to Managed Resources | ||
You can add authorizations for any other supported managed resource using the `Configration`. For a list of supported | ||
service please visit [https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/services-support-managed-identities](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/services-support-managed-identities). | ||
To add a resource authorization: | ||
``` | ||
const config = new Configuration(new StringProperty("function.name"), | ||
new BooleanProperty("function.managed")); | ||
config.config.addResourceAuthorization("https://datalake.azure.net/"); | ||
// OR | ||
config.config.addResourceAuthorization(new StringProperty("YOUR_RESOURCE_AUTHORIZATION")); | ||
``` | ||
More about actually using a resource authorization later. | ||
####Wait, what about Applications Registrations? | ||
Awesome! So you finally realized that roll'n your own user management system is a bad idea and have set up Azure AD B2C! | ||
I'm proud of you, that's a big step! If I was wrong and you haven't, I choose not to help you. Just don't. If you're gonna | ||
use Azure, use Azure AD. If you are an enterprise and writing an application for employees, you're almost there. If not, | ||
use Azure AD B2C for your customers. Its basically free for most small to medium application so there is really no reason | ||
not to. Please, don't fight this. BTW, heres a good link on configuring Azure AD B2C [https://docs.microsoft.com/en-us/azure/active-directory-b2c/](https://docs.microsoft.com/en-us/azure/active-directory-b2c/) | ||
If you've made it this far, you probably have Azure AD, or Azure AD B2C and want to access resources not as a function | ||
managed identity, but as an application. No problem, Celastrina.js makes this pretty easy to do as well, enter | ||
`ApplicationAuthorization`. | ||
The `ApplicationAuthorization` constructor takes the following information: | ||
-`authority` {`string`}: This is the authorizing URL for the directory. For most Azure AD B2C deployments this will be | ||
`https://login.microsoftonline.com`. | ||
- `tenant` {`string`}: This is the UUID for your tenant. You can get this by navigating to Home > Azure AD B2C in the | ||
Azure portal and clicking on the "Resource name" in the "Overview" page. There will be a "Tenant ID" you can copy from | ||
there. | ||
- `id` {`string`}: This is the Application ID of the registered application. You can get the application ID in Azure | ||
portal at Home > Azure AD B2C | App registrations (Preview) > \[Your Application\]. | ||
- `secret` {`string`}: This is the credential the application uses. | ||
- `resources` {`Array.<string>`}: An array of resources to register for the application. | ||
Using the `ApplicationAuthorization` in code: | ||
``` | ||
const config = new Configuration(new StringProperty("function.name"), | ||
new BooleanProperty("function.managed")); | ||
config.addApplicationAuthorization(new ApplicationAuthorization("https://login.microsoftonline.com", | ||
"c7b24e39-37e9-4fcf-bbf0-480309764eef", "f396619a-f2dc-455c-8f71-0fc77d424b46", "x?=/4ZkXh<Yv'4_m2&n]<B[L", | ||
["https://datalake.azure.net"])); | ||
``` | ||
There is also a custom `Property` instance called `ApplicationAuthorizationProperty` (_how original_) that allows you | ||
to load from and application authorization from a application setting. | ||
``` | ||
config.addApplicationAuthorization(new ApplicationAuthorizationProperty("YOUR PROPERTY NAME", true)); | ||
``` | ||
In Azure Key Vault add the following json string: | ||
``` | ||
{ | ||
"_authority":"https://login.microsoftonline.com", | ||
"_tenant": "c7b24e39-37e9-4fcf-bbf0-480309764eef", | ||
"_id":"f396619a-f2dc-455c-8f71-0fc77d424b46", | ||
"_secret":"x?=/4ZkXh<Yv'4_m2&n]<B[L", | ||
"_resources": [ | ||
"https://vault.azure.net", "https://datalake.azure.net/" | ||
] | ||
} | ||
``` | ||
I **highly** recommend you use `ApplicationAuthorizationProperty` and place the json in Azure Key Vault. I **DO NOT** | ||
recommend giving out application secrets to developers! | ||
For more information on Application registrations and MSAL, see [https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration). | ||
####How do I use the resource registrations and application authorizations? | ||
Use them is as simple as assing the bearer token to the `authorization` header of your RESTful requests to azure | ||
resources. | ||
Accessing these bearer tokens introduces another concept of Celastrina.js, the Sentry! Queue menacing music! The sentry | ||
handles all security matters in Celastrina.js from managed resources registrations, authentication, to authorization. The | ||
sentry is derrived from the base class `BaseSentry`, and is contained in the context of any life-cycle function. | ||
To use an application authorization bearer token simply look up the resource by the application ID. Let say I want the | ||
bearer token for Azure Datalake for the application authorization above: | ||
``` | ||
async _get(context) { | ||
return new Promise(async (resolve, reject) => { | ||
let token = await context.sentry.getAuthorizationToken("https://datalake.azure.net/", | ||
"f396619a-f2dc-455c-8f71-0fc77d424b46"); | ||
resolve(); | ||
}); | ||
} | ||
``` | ||
If you want to get the Datalake bearer token for the Azure function's managed identity simply omit the optional | ||
application ID: | ||
``` | ||
async _get(context) { | ||
return new Promise(async (resolve, reject) => { | ||
let token = await context.sentry.getAuthorizationToken("https://datalake.azure.net/"); | ||
resolve(); | ||
}); | ||
} | ||
``` | ||
All bearer tokens are fetched and cached during the bootstrap life-cycle. Tokens are also lazy refreshed during access if | ||
the token has expired. Because of this, a call to `getAuthorizationToken` may take longer then expected if the token | ||
has expired. | ||
###WOW, this is awesome! Wait, what about authentication and authorization of users? | ||
Yes! Of course! Now that you are using Azure AD B2C you have authentication and are iching to get your users to login. | ||
I'm so proud of how mindful you are about protecting your web application. Celastrina.js has got your back! | ||
####I wanna use JWT! | ||
Great I do too. Time to use Azure API Manager... _queue sound of record being scratched followed by silence_. | ||
_Queue wavy line fade-out_ | ||
>Look, gonna level with you about the architecture that Celastrina.js was designed to support. It was designed for | ||
>micro services behind an API Gateway. I know! I know! You are saying it right now - "_this architecture is way | ||
>too complex for my simple application!_" You probably almost clicked away. Look, don't fight this. Use Azure API | ||
>Manager. I promise it will cost you, in most cases, nothing to use. It has all the mechinisms in place to validate JWT. | ||
>Please don't do this in your azure function, especially if you went the Azure AD B2C route. I know you can do it in | ||
>the function, just don't. **I'll make my case:** It won't be long before your application has a dozen or so micro | ||
>services. Configuring JWT validation for each is daunting and prone to failure. Simply adding an API manager in front | ||
>with a global validation policy makes this so much safer and easier. Also, while I'm on my soap box, it won't be long | ||
>before your micro service will need a resource from another micro service. You know, even the smallest application, it | ||
>will take about 5 days before you are writing a call to another azure function within a function. **DON'T DO THIS | ||
>EITHER!** Simply enrich the message in API Manager, keep your code decoupled. It becomes a nightmare calling azure | ||
>functions from other functions, especially when deploying from DEV->INT->UAT->PROD. Make this decision early, bite the | ||
>bullet, spare yourself a lot of time, energy, and sanity. Use API manager for JWT validation and message enrichment. | ||
_Queue wavy line fade-in_ | ||
Whoa, where were you? Welcome back! Once API Manager is configured to validate your JWT token from Azure AD B2C | ||
Celastrina.js can leverage the token information for function AAA. Oh, and heres a good link to help you configure | ||
API Manager [https://docs.microsoft.com/en-us/azure/api-management/](https://docs.microsoft.com/en-us/azure/api-management/). | ||
**WARNING** I'm going to state this now in case it wasn't obvious above; **_Celastrina.js DOES NOT validate JWT. It | ||
only decodes it!_** One more time, it doesn't validate, it only decodes it. There, now don't go gett'n mad at me when you | ||
use out-of-the-box Celastrina.js JWT, don't use Azure API manager, you get compromised, and all your users are after | ||
you with torches and pitch forks. | ||
Using JWT requires changes to the core sentry we talked about earlier. To make these changes you simply extend the base | ||
class called `JwtJSONHTTPFunction`. | ||
``` | ||
class MyNewSecureHTTPTriggerFunction extends JwtJSONHTTPFunction { | ||
async initialize(context) { | ||
return new Promise((resolve, reject) => { | ||
// Do some initialization stuff | ||
resolve(); | ||
}); | ||
} | ||
async load(context) { | ||
return new Promise((resolve, reject) => { | ||
// Load some objects from your data store | ||
resolve(); | ||
}); | ||
} | ||
async _get(context) { | ||
return new Promise((resolve, reject) => { | ||
context.send({message: "_get invoked."}); | ||
resolve(); | ||
}); | ||
} | ||
async _post(context) { | ||
return new Promise((resolve, reject) => { | ||
context.send({message: "_post invoked."}); | ||
resolve(); | ||
}); | ||
} | ||
async save(context) { | ||
return new Promise((resolve, reject) => { | ||
// Save some objects to your data store | ||
resolve(); | ||
}); | ||
} | ||
async terminate(context) { | ||
return new Promise((resolve, reject) => { | ||
// Do your cleanup. | ||
resolve(); | ||
}); | ||
} | ||
} | ||
module.exports = new MyNewSecureHTTPTriggerFunction(config); | ||
``` | ||
Then you will need to set up the allowed issuers in your configuration. An `Issuer` tells Celastrina.js which JWT | ||
tokens to accept and, if you must, what roles to escalate your users to when they use those issuers. To use an `Issuer` | ||
you must also add a JWT configuration item using the `JwtConfiguration`. First, lets go over `Issuer`: | ||
- `name` {`string`}: Thhe issuer name to match, the `iss` attribute in the JWT token. | ||
- `audience` {`string`}: The audience name to match, the `aud` attribute in the JWT token. | ||
- `roles` {`Array.<string>`}: An optional array of role names to escalate the user to if they match this issuer. | ||
- `nonce` {`string`}: The optional nonce to match, the `nonce` attribute in the JWT token. | ||
Basically, during the `authenticate` phase of the `JwtJSONHTTPFunction`, the JWT token will be pulled from the header, | ||
and is compared against the `name`, `audiance`, and optionally the `nonce` of each `Issuer` in the configuration. Each | ||
issuer the subject matches will be escalated to the optional `roles` by adding those roles to the subject. If you do not | ||
match any `Issuer` then your request will fail with a 401. If the subject matches any `Issuer` it is considered | ||
"authenticated" and will move on to the `authorization` life-cycle. | ||
Now, let's configure JWT: | ||
``` | ||
const config = new Configuration(new StringProperty("function.name"), | ||
new BooleanProperty("function.managed")); | ||
const jwtconfig = new JwtConfiguration(); | ||
jwtconfig.addIssuer(new Issuer("https://YourRegisteredApplication.b2clogin.com/c7b24e39-37e9-4fcf-bbf0-480309764eef/v2.0/", | ||
"f396619a-f2dc-455c-8f71-0fc77d424b46", ["user_role"])) | ||
.addIssuer(new Issuer("https://sts.windows.net/c7b24e39-37e9-4fcf-bbf0-480309764eef/", | ||
"f396619a-f2dc-455c-8f71-0fc77d424b46", ["admin_role"])); | ||
config.addValue(JwtConfiguration.CELASTRINAJS_CONFIG_JWT, jwtconfig); | ||
``` | ||
You may also use the custom property `IssuerProperty`: | ||
``` | ||
jwtconfig.addIssuer(new IssuerProperty("function.jwt.issuer.user")) | ||
.addIssuer(new IssuerProperty("function.jwt.issuer.admin")); | ||
``` | ||
and include in your settings the following JSON: | ||
``` | ||
{ | ||
"_name": "https://YourRegisteredApplication.b2clogin.com/c7b24e39-37e9-4fcf-bbf0-480309764eef/v2.0/", | ||
"_audience": "f396619a-f2dc-455c-8f71-0fc77d424b46", | ||
"_roles": [ | ||
"user_role" | ||
] | ||
} | ||
``` | ||
Celastrina.js is an all-or-nothing authentication system, regardless of the HTTP methods you've implemented. If you | ||
need a different level of authentication per HTTP method you'll need to split them out into different functions. | ||
BAM! That's it! | ||
####Wait, you said Authentication and Authorization, what about roles? | ||
Whoa, slow your role child! _Queue Batman and Robin *slap* meme and slapping sound_ I got a soap box for this one... | ||
_Queue wavy line fade-out_ | ||
>So, you are about to go into Azure AD B2C, add a custom attribute for roles, and place a bunch of comma delim strings | ||
>of roles in there, add it to the claim, feel all clever about putting roles in your JWT toke. **DON'T!** Please, | ||
>just don't. Huge mistake. **I'll make my case:** First off, JWT is signed, not encrypted, and then encoded. All you | ||
>need to do is decode it. Anything like adding roles or user info outside the base claims introduces a potential attack | ||
>vector. Also, custom attributes in AD B2C only allow a length of 255 characters and you'll likely run out of space | ||
>quickly, especially if your like me and end up on a project whose roles are UUID's. After a few failed attempts | ||
>Celastrina.js landed on an encrypted cookie. I know this is spitting in the face of stateless micro services but its | ||
>the same as JWT being in the header. Celastrina.js has a concept of a "session" (to be used sparingly) that gets | ||
>encrypted and placed in a cookie, so we just place the roles there. | ||
_Queue wavy line fade-in_ | ||
Celastrina.js leverages user roles encrypted in a cookie header. By default Celastrina.js attempts to check its internal | ||
session object for an attribure called `roles` that is of type `Array.<string>`. If its there, it suffs them into the | ||
subject roles in `context.subject`. Thos roles, in addition to escalations from issuers can be compared to a | ||
`FunctionRole` within Celastrina.js sentry. First, lets create some function roles that force authorization to occur. | ||
`FunctionRole` has the following constructor: | ||
- `action` {`string`}: This is the action to authorize. The default is `process`. In the HTTP, the method will be used | ||
as the action. | ||
- `roles` {`Array.<string>`}: The roles related to the action. | ||
- `match` {`ValueMatch`}: The optional matching rule instance for this role. This is the rule on how to match roles | ||
between the function and the subject. The default is `MatchAny`. There are 3 types of rules: | ||
- `MatchAny`: Any role in subject matches any role in function. | ||
- `MatchAll`: All roles in subject match all roles in function. | ||
- `MatchNone`: No roles in subject match no roles in functions. | ||
``` | ||
const config = new Configuration(new StringProperty("function.name"), | ||
new BooleanProperty("function.managed")); | ||
config.addFunctionRole(new FunctionRole("post", ["role1", "role2", "role3"])); // Default MatchAny | ||
``` | ||
Of course, you may also use any of the `Property` types in the constructor of `FunctionRole`. There is also a custom | ||
`Property` type `FunctionRoleProperty` that resolves a `FunctionRole` from a JSON string. | ||
``` | ||
const config = new Configuration(new StringProperty("function.name"), | ||
new BooleanProperty("function.managed")); | ||
config.addFunctionRole(new FunctionRoleProperty("function.role.post")); // Default MatchAny | ||
``` | ||
A application setting or Azure Key Vault attribute must be created with the follow JSON: | ||
``` | ||
{ | ||
"_action": "post", | ||
"_roles": [ | ||
"role1", | ||
"role2", | ||
"role3" | ||
], | ||
"_match": { | ||
"_type": "MatchAny" | ||
} | ||
} | ||
``` | ||
The `_match._type` value must follow the same convension of the match parameter in `FunctionRole`. | ||
Now we've defined a role for an HTTP method, we can configure Celastrina.js to read an encrypted session | ||
cookie. To do this wee need to configre the `SecureCookieSessionResolver`. The constructor is as | ||
follows: | ||
- `crypto` {`Cryptography`}: The instance of `Cryptography` to encrypt and decrypt raw data. | ||
- `name` {`string`}: The name of the cookie to use from the `Cookie` header. | ||
You could create all this by hand or simply use the custom property `SecureCookieSessionResolverProperty`. When using | ||
the custom property you are forced into AES256, specifically aes-256-cbc. If you need different, you'll need to roll | ||
your own. | ||
``` | ||
const config = new Configuration(new StringProperty("function.name"), | ||
new BooleanProperty("function.managed")); | ||
const jwtconfig = new JwtConfiguration(); | ||
jwtconfig.addIssuer(new IssuerProperty("function.jwt.issuer.user", true)) | ||
.addIssuer(new IssuerProperty("function.jwt.issuer.admin", true))); | ||
config.addFunctionRole(new FunctionRoleProperty("function.role.post", true)) | ||
.addValue(JwtConfiguration.CELASTRINAJS_CONFIG_JWT, | ||
jwtconfig) | ||
.addValue(CookieSessionResolver.CELASTRINA_CONFIG_HTTP_SESSION_RESOLVER, | ||
new SecureCookieSessionResolverProperty("function.cookie.secure", true)); | ||
``` | ||
The JSON configiration for `SecureCookieSessionResolverProperty` must be: | ||
``` | ||
{ | ||
"_name":"celastrina_session", | ||
"_key":"bf3c199c2470cb477d907b1e0917c17b", | ||
"_iv":"5183666c72eec9e4" | ||
} | ||
``` | ||
For more information on the keys and initialization vectors, please see the crypto API in Node.js at | ||
[https://nodejs.org/api/crypto.html](https://nodejs.org/api/crypto.html). | ||
Where _`name` is the cookie name, _`key` the AES 256 Key and `_iv` the initialization vector. Once configured Celastrina.js | ||
will retrieve and decrypt the session cookie from the cookie header, copy the `roles` attribute from the session to the | ||
subject roles, authenticate the requesters JWT token against the issuers, apply role escalations, then authorize the | ||
HTTP method requested. | ||
#Putting it all together | ||
Setting up the index.js: | ||
``` | ||
const config = new Configuration(new StringProperty("function.name"), | ||
new BooleanProperty("function.managed")); | ||
const jwtconfig = new JwtConfiguration(); | ||
jwtconfig.addIssuer(new IssuerProperty("function.jwt.issuer.user")) | ||
.addIssuer(new IssuerProperty("function.jwt.issuer.admin"))); | ||
config.addFunctionRole(new FunctionRoleProperty("function.role.post")) | ||
.addResourceAuthorization(new StringProperty("function.resource.local.graph")) | ||
.addApplicationAuthorization(new ApplicationAuthorizationProperty("function.resource.app.datalake")) | ||
.addValue(JwtConfiguration.CELASTRINAJS_CONFIG_JWT, | ||
jwtconfig) | ||
.addValue(CookieSessionResolver.CELASTRINA_CONFIG_HTTP_SESSION_RESOLVER, | ||
new SecureCookieSessionResolverProperty("function.cookie.secure")); | ||
class MyFunction extends JwtJSONHTTPFunction { | ||
async initialize(context) { | ||
return new Promise((resolve, reject) => { | ||
// Do some initialization stuff | ||
context.log("Initialized.", LOG_LEVEL.LEVEL_INFO, "MyFunction.initialize(context)"); | ||
resolve(); | ||
}); | ||
} | ||
async load(context) { | ||
return new Promise((resolve, reject) => { | ||
// Load some objects from your data store | ||
context.log("Loaded.", LOG_LEVEL.LEVEL_INFO, "MyFunction.load(context)"); | ||
resolve(); | ||
}); | ||
} | ||
async _get(context) { | ||
return new Promise((resolve, reject) => { | ||
context.log("HTTP GET Invoked.", LOG_LEVEL.LEVEL_INFO, "MyFunction._get(context)"); | ||
context.send({message: "HTTP GET Invoked"}); | ||
resolve(); | ||
}); | ||
} | ||
async _post(context) { | ||
return new Promise((resolve, reject) => { | ||
context.log("HTTP POST Invoked.", LOG_LEVEL.LEVEL_INFO, "MyFunction._post(context)"); | ||
context.send({message: "HTTP POST Invoked"}); | ||
resolve(); | ||
}); | ||
} | ||
async save(context) { | ||
return new Promise((resolve, reject) => { | ||
// Save some objects to your data store | ||
context.log("Saved.", LOG_LEVEL.LEVEL_INFO, "MyFunction.save(context)"); | ||
resolve(); | ||
}); | ||
} | ||
async terminate(context) { | ||
return new Promise((resolve, reject) => { | ||
// Do your cleanup. | ||
context.log("Terminated.", LOG_LEVEL.LEVEL_INFO, "MyFunction.terminate(context)"); | ||
resolve(); | ||
}); | ||
} | ||
} | ||
module.exports = new MyFunction(config); | ||
``` | ||
Function application settings and `local.settings`.json: | ||
``` | ||
{ | ||
"IsEncrypted": false, | ||
"Values": { | ||
"FUNCTIONS_WORKER_RUNTIME": "node", | ||
"AzureWebJobsStorage": "{AzureWebJobsStorage}", | ||
"function.name": "ExampleJwtSecureRoleJSONHTTPFunction", | ||
"function.managed": "false", | ||
"function.role.post": "{\"_action\": \"post\", \"_roles\": [\"role1\", \"role2\", \"role3\"], \"_match\": {\"_type\": \"MatchAny\"}}", | ||
"function.resource.app.datalake": "{\"_authority\":\"https://login.microsoftonline.com\", \"_tenant\": \"c7b24e39-37e9-4fcf-bbf0-480309764eef\", \"_id\":\"f396619a-f2dc-455c-8f71-0fc77d424b46\", \"_secret\":\"x?=/4ZkXh<Yv'4_m2&n]<B[L\", \"_resources\": [\"https://datalake.azure.net/\"]}", | ||
"function.jwt.issuer.user": "{\"_name\": \"https://YourRegisteredApplication.b2clogin.com/c7b24e39-37e9-4fcf-bbf0-480309764eef/v2.0/\", \"_audience\":\"f396619a-f2dc-455c-8f71-0fc77d424b46\", \"_roles\":[\"user_role\"]}", | ||
"function.jwt.issuer.admin": "{\"_name\": \"https://sts.windows.net/c7b24e39-37e9-4fcf-bbf0-480309764eef/\", \"_audience\":\"f396619a-f2dc-455c-8f71-0fc77d424b46\", \"_roles\":[\"admin_role\"]}", | ||
"function.cookie.secure": "{\"_name\":\"celastrina_session\", \"_key\":\"bf3c199c2470cb477d907b1e0917c17b\", \"_iv\":\"5183666c72eec9e4\"}" | ||
} | ||
} | ||
``` | ||
Notice for the `local.settings.json` `managed` is set `false`. This is because the managed identity settings are **NOT** | ||
available when running locally. Azure func uses the VisualStudio credentials to access resources instead. Also, please | ||
make a note to use `secure` set to `true` when using `Property` instances that should be secured and replace the | ||
application settings with Key Vault Secret resource URL's. | ||
your function.json: | ||
``` | ||
{ | ||
"entryPoint": "execute", | ||
"bindings": [ | ||
{ | ||
"authLevel": "function", | ||
"type": "httpTrigger", | ||
"direction": "in", | ||
"name": "req", | ||
"methods": [ | ||
"get", | ||
"post" | ||
] | ||
}, | ||
{ | ||
"type": "http", | ||
"direction": "out", | ||
"name": "res" | ||
} | ||
] | ||
} | ||
``` |
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
92438
736
1682