Implementation Keycloak adapter for aws Lambda
- 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
- cross-realm authentication
- Regexp endpoints for Lambda@Edge
- Resource based authorization ( Keycloak Authorization Services )
npm install keycloak-lambda-authorizer -S
- Serverless example (Api gateway with lambda authorizer)
- CloudFront with Lambda:Edge example
- CloudFront with portal authorization (switching between security realms)
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
});
}
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
});
}
{
"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
}
]
}
}
}
{
"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
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
});
}
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 async 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 async 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');
}
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).
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
});
}
{
"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
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
- 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,
});
}
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
});
}
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);
}
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(
(^)(\/|)someUrl(|((\/)))$,
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);
}
lamdaEdge.routes.addProtected(
'/',
keycloakJson,
{
enforce: {
enabled: true,
resource: {
name: 'tenantResource',
},
},
responseHandler: async (request, options)=>{
const jwtToken = request.token;
const uri = request.uri;
if (uri.startsWith('/callback') ||
uri.startsWith('callback')) {
return callBackPageHandle;
}
}
}
);
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);
}
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);
}
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: async (request) => await 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);
}
7. 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: async (request) => await 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);
}
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,
},
},
});
...
}
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 = ...;
function getKeycloakJson(realm, clientId){
return {
"realm": realm,
"auth-server-url": "http://localhost:8090/auth",
"ssl-required": "external",
"resource": clientId,
"verify-token-audience": true,
"credentials": {
"secret": "772decbe-0151-4b08-8171-bec6d097293b"
},
"confidential-port": 0,
"policy-enforcer": {}
}
}
lamdaEdge.routes.addProtected(
'/',
getKeycloakJson("lambda-authorizer", "lambda"),
{
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);
}
lamdaEdge.routes.addProtected(
'/',
keycloakJson,
{
enforce: {
enabled: true,
clientId: 'CLIENT_ID',
resource: {
name: 'tenantResource',
},
},
}
);
lamdaEdge.routes.addProtected(
'/',
keycloakJson,
{
enforce: {
enabled: true,
resource: {
name: 'tenantResource',
},
},
sessionModify: (sessionToken, token, options) => {
const newSessionToken = { ...sessionToken };
sessionToken.newProperty="test";
return newSessionToken;
},
sessionDelete: (sessionToken, token, options) => {
const newSessionToken = { ...sessionToken };
delete sessionToken.newProperty;
return newSessionToken;
},
}
);
lamdaEdge.routes.addProtected(
'/',
keycloakJson,
{
enforce: {
enabled: true,
resource: {
name: 'tenantResource',
},
},
kc_idp_hint:'tenantIdp'
}
);
lamdaEdge.routes.addProtected(
'/',
keycloakJson,
{
enforce: {
enabled: true,
resource: {
name: 'tenantResource',
},
},
isRequest: (request, routePath, ret)=>{
return true;
}
}
);
const keycloakJSON = ...; // read Keycloak.json
apigateway.awsHandler(event, keycloakJSON, {
enforce: {
enabled: true,
resource: {
name: 'SOME_RESOURCE',
uri: 'RESOURCE_URI',
matchingUri: true,
},
resourceHandler:(resourceJson, options)=>{
console.log('resource: ' + JSON.stringify(resourceJson));
}
},
}).then((token)=>{
// Success
}).catch((e)=>{
// Failed
});
}