keycloak-cloudfront-dynamodb v0.1.5
Description
Implementation Keycloak adapter for aws Lambda
Features
- supports AWS API Gateway, AWS Cloudfront with Lambda@Edge
- works with non amazon services.
- validate expiration of JWT token
- validate JWS signature
- supports "clientId/secret" and "client-jwt" credential types
- Role based authorization
- support MultiTenant
- Resource based authorization ( Keycloak Authorization Services )
Installation
npm install keycloak-lambda-authorizer -S
Examples
How to use
Role Based
import { apigateway } from 'keycloak-lambda-authorizer';
export function authorizer(event, context, callback) {
const keycloakJSON = ...; // read Keycloak.json
awsAdapter.awsHandler(event, keycloakJSON, {
enforce: { enabled: true, role: 'SOME_ROLE' },
}).then((token)=>{
// Success
}).catch((e)=>{
// Failed
});
}
Resource Based (Keycloak Authorization Services)
import { apigateway } from 'keycloak-lambda-authorizer';
export function authorizer(event, context, callback) {
const keycloakJSON = ...; // read Keycloak.json
apigateway.awsHandler(event, keycloakJSON, {
enforce: {
enabled: true,
resource: {
name: 'SOME_RESOURCE',
uri: 'RESOURCE_URI',
matchingUri: true,
},
},
}).then((token)=>{
// Success
}).catch((e)=>{
// Failed
});
}
Configuration
Option structure:
{
"cache":"defaultCache",
"logger":console,
"keys":{
"privateKey":{
"key": privateKey,
"passphrase": 'privateKey passphrase'
},
"publicKey":{
"key": publicKey,
}
},
"enforce":{
"enabled":true,
"resource":{
"name":"SOME_RESOURCE",
"uri":"/test",
"owner":"...",
"type":"...",
"scope":"...",
"matchingUri":false,
"deep":false
},
"resources":[
{
"name":"SOME_RESOURCE1",
"uri":"/test1",
"owner":"...",
"type":"...",
"scope":"...",
"matchingUri":false,
"deep":false
},
{
"name":"SOME_RESOURCE2",
"uri":"/test2",
"owner":"...",
"type":"...",
"scope":"...",
"matchingUri":false,
"deep":false
}
]
}
}
}
Resource Structure:
{
"name":"",
"uri":"",
"owner":"",
"type":"",
"scope":"",
"matchingUri":false
}
name : unique name of resource
uri : URIs which are protected by resource.
Owner : Owner of resource
type : Type of Resource
scope : The scope associated with this resource.
matchingUri : matching Uri
Change logger
awsHandler(event, keycloakJSON, {
logger:winston,
...
}).then().catch()
const winston from 'winston';
import { awsHandler } from 'keycloak-lambda-authorizer';
export function authorizer(event, context, callback) {
const keycloakJSON = ...; // read Keycloak.json
awsHandler(event, keycloakJSON, {
logger:winston
}).then((token)=>{
// Success
}).catch((e)=>{
// Failed
});
}
Cache
Example of cache:
const NodeCache = require('node-cache');
const defaultCache = new NodeCache({ stdTTL: 180, checkperiod: 0, errorOnMissing: false });
const resourceCache = new NodeCache({ stdTTL: 30, checkperiod: 0, errorOnMissing: false });
export function put(region, key, value) {
if (region === 'publicKey') {
defaultCache.set(key, value);
} else if (region === 'uma2-configuration') {
defaultCache.set(key, value);
} else if (region === 'client_credentials') {
defaultCache.set(key, value);
} else if (region === 'resource') {
resourceCache.set(key, value);
} else {
throw new Error('Unsupported Region');
}
}
export function get(region, key) {
if (region === 'publicKey') {
return defaultCache.get(key);
} if (region === 'uma2-configuration') {
return defaultCache.get(key);
} if (region === 'client_credentials') {
return defaultCache.get(key);
} if (region === 'resource') {
return resourceCache.get(key);
}
throw new Error('Unsupported Region');
}
Cache Regions:
publicKey - Cache for storing Public Keys. (The time to live - 180 sec)
uma2-configuration - uma2-configuration link. example of link http://localhost:8090/auth/realms/lambda-authorizer/.well-known/uma2-configuration (The time to live - 180 sec)
client_credentials - Service Accounts Credential Cache (The time to live - 180 sec).
resource - Resources Cache (The time to live - 30 sec).
Change Cache:
import { awsHandler } from 'keycloak-lambda-authorizer';
export function authorizer(event, context, callback) {
const keycloakJSON = ...; // read Keycloak.json
awsHandler(event, keycloakJSON, {
cache: newCache,
}).then((token)=>{
// Success
}).catch((e)=>{
// Failed
});
}
Client Jwt Credential Type
- RSA Keys Structure
{
"privateKey":{
"key":"privateKey",
"passphrase":"privateKey passphrase"
},
"publicKey":{
"key":"publicKey"
}
}
privateKey.key - RSA Private Key privateKey.passphrase - word or phrase that protects private key publicKey.key - RSA Public Key or Certificate
RSA keys generation example using openssl
openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=lambda-jwks" -keyout server.key -out server.crt
Create JWKS endpoint by AWS API Gateway
- serverless.yaml
functions:
cert:
handler: handler.cert
events:
- http:
path: cert
method: GET
- lambda function (handler.cert)
import { jwksUrl } from 'keycloak-lambda-authorizer';
export function cert(event, context, callback) {
const jwksResponse = jwksUrl(publicKey);
callback(null, {
statusCode: 200,
body: jwksResponse,
});
}
- Keycloak Settings
Create Api GateWay Authorizer function
import { awsHandler } from 'keycloak-lambda-authorizer';
export function authorizer(event, context, callback) {
const keycloakJSON = ...; // read Keycloak.json
awsHandler(event, keycloakJSON, {
keys:{
privateKey:{
key: privateKey,
},
publicKey:{
key: publicKey,
}
},
enforce: {
enabled: true,
resource: {
name: 'SOME_RESOURCE',
uri: 'RESOURCE_URI',
matchingUri: true,
},
},
}).then((token)=>{
// Success
}).catch((e)=>{
// Failed
});
}
Lambda:Edge
1. protect Url
import { lamdaEdge } from 'keycloak-lambda-authorizer';
import { SessionManager } from 'keycloak-lambda-authorizer/src/edge/storage/SessionManager';
import { LocalSessionStorage } from 'keycloak-lambda-authorizer/src/edge/storage/localSessionStorage';
import { DynamoDbSessionStorage } from 'keycloak-lambda-authorizer/src/edge/storage/DynamoDbSessionStorage';
import { isLocalhost } from 'keycloak-lambda-authorizer/src/edge/lambdaEdgeUtils';
const keycloakJson = ...;
const privateKey = ...;
const publicKey = ...;
lamdaEdge.routes.addProtected(
'/',
keycloakJson,
{
enforce: {
enabled: true,
resource: {
name: 'tenantResource',
},
},
}
);
// eslint-disable-next-line import/prefer-default-export
export async function authorization(event, context, callback) {
await lamdaEdge.lambdaEdgeRouter(event, context, new SessionManager(isLocalhost()? new LocalSessionStorage(): new DynamoDbSessionStorage({ region: 'us-east-1' },'teablename'), {
keys: {
privateKey,
publicKey,
},
}), callback);
}
2. Create JWKS endpoint by Lambda:Edge
import { lamdaEdge } from 'keycloak-lambda-authorizer';
import { SessionManager } from 'keycloak-lambda-authorizer/src/edge/storage/SessionManager';
import { LocalSessionStorage } from 'keycloak-lambda-authorizer/src/edge/storage/localSessionStorage';
import { DynamoDbSessionStorage } from 'keycloak-lambda-authorizer/src/edge/storage/DynamoDbSessionStorage';
import { isLocalhost } from 'keycloak-lambda-authorizer/src/edge/lambdaEdgeUtils';
const privateKey = ...;
const publicKey = ...;
lamdaEdge.routes.addJwksEndpoint('/cert', publicKey.key);
// eslint-disable-next-line import/prefer-default-export
export async function authorization(event, context, callback) {
await lamdaEdge.lambdaEdgeRouter(event, context, new SessionManager(isLocalhost()? new LocalSessionStorage(): new DynamoDbSessionStorage({ region: 'us-east-1' },'teablename'), {
keys: {
privateKey,
publicKey,
},
}), callback);
}
3. Public url
import { lamdaEdge } from 'keycloak-lambda-authorizer';
import { SessionManager } from 'keycloak-lambda-authorizer/src/edge/storage/SessionManager';
import { LocalSessionStorage } from 'keycloak-lambda-authorizer/src/edge/storage/localSessionStorage';
import { DynamoDbSessionStorage } from 'keycloak-cloudfront-dynamodb/DynamoDbSessionStorage';
import { isLocalhost } from 'keycloak-lambda-authorizer/src/edge/lambdaEdgeUtils';
const privateKey = ...;
const publicKey = ...;
lamdaEdge.routes.addUnProtected('/withoutAuthorization');
// eslint-disable-next-line import/prefer-default-export
export async function authorization(event, context, callback) {
await lamdaEdge.lambdaEdgeRouter(event, context, new SessionManager(isLocalhost()? new LocalSessionStorage(): new DynamoDbSessionStorage({ region: 'us-east-1' },'teablename'), {
keys: {
privateKey,
publicKey,
},
}), callback);
}
4. Custom Url Handler
import { lamdaEdge } from 'keycloak-lambda-authorizer';
import { SessionManager } from 'keycloak-lambda-authorizer/src/edge/storage/SessionManager';
import { LocalSessionStorage } from 'keycloak-lambda-authorizer/src/edge/storage/localSessionStorage';
import { DynamoDbSessionStorage } from 'keycloak-cloudfront-dynamodb/DynamoDbSessionStorage';
import { isLocalhost } from 'keycloak-lambda-authorizer/src/edge/lambdaEdgeUtils';
const privateKey = ...;
const publicKey = ...;
lamdaEdge.routes.addRoute({
isRoute: (request) => isRequest(request, '/someUrl'),
handle: async (request, config, callback) => {
const response=... ;
YOUR LOGIC
callback(null, response);
},
});
// eslint-disable-next-line import/prefer-default-export
export async function authorization(event, context, callback) {
await lamdaEdge.lambdaEdgeRouter(event, context, new SessionManager(isLocalhost()? new LocalSessionStorage(): new DynamoDbSessionStorage({ region: 'us-east-1' },'teablename'), {
keys: {
privateKey,
publicKey,
},
}), callback);
}
5. Custom Url Handler with Lambda:Edge EventType
import { lamdaEdge } from 'keycloak-lambda-authorizer';
import { SessionManager } from 'keycloak-lambda-authorizer/src/edge/storage/SessionManager';
import { LocalSessionStorage } from 'keycloak-lambda-authorizer/src/edge/storage/localSessionStorage';
import { DynamoDbSessionStorage } from 'keycloak-cloudfront-dynamodb/DynamoDbSessionStorage';
import { isLocalhost } from 'keycloak-lambda-authorizer/src/edge/lambdaEdgeUtils';
const privateKey = ...;
const publicKey = ...;
lamdaEdge.routes.addRoute({
isRoute: (request) => isRequest(request, '/someUrl'),
handle: async (request, config, callback) => {
if (config.eventType === 'viewer-request') { // original-request, origin-response, viewer-request, viewer-response, local-request
const response=... ;
YOUR LOGIC
callback(null, response);
} else {
callback(null, request);
}
},
});
// eslint-disable-next-line import/prefer-default-export
export async function authorization(event, context, callback) {
await lamdaEdge.lambdaEdgeRouter(event, context, new SessionManager(isLocalhost()? new LocalSessionStorage(): new DynamoDbSessionStorage({ region: 'us-east-1' },'teablename'), {
keys: {
privateKey,
publicKey,
},
}), callback);
}
Implementation For Custom Service or non amazon cloud
import { adapter } from 'keycloak-lambda-authorizer';
const keycloakJson = {
"realm": "lambda-authorizer",
"auth-server-url": "http://localhost:8090/auth",
"ssl-required": "external",
"resource": "lambda",
"verify-token-audience": true,
"credentials": {
"secret": "772decbe-0151-4b08-8171-bec6d097293b"
},
"confidential-port": 0,
"policy-enforcer": {}
}
async function handler(request,response) {
const authorization = request.headers.Authorization;
const match = authorization.match(/^Bearer (.*)$/);
if (!match || match.length < 2) {
throw new Error(`Invalid Authorization token - '${authorization}' does not match 'Bearer .*'`);
}
const jwtToken = match[1];
await adapter(jwtToken,keycloakJson, {
enforce: {
enabled: true,
resource: {
name: 'SOME_RESOURCE',
uri: 'RESOURCE_URI',
matchingUri: true,
},
},
});
...
}