0.1.5 • Published 3 years ago

keycloak-cloudfront-dynamodb v0.1.5

Weekly downloads
1
License
Apache-2.0
Repository
github
Last release
3 years ago

CircleCI
npm version

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

Keycloak Admin Console 2020-04-11 23-58-06

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
    Keycloak Admin Console 2020-04-12 13-30-26

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,
                                          },
                                        },
                                      });
...
}
0.1.5

3 years ago

0.1.4

4 years ago