Skip to content

Commit

Permalink
Merge pull request #41 from vzakharchenko/service-account-api
Browse files Browse the repository at this point in the history
Service account api
  • Loading branch information
vzakharchenko committed May 24, 2021
2 parents 57bbf0d + 19d8adf commit bc527a1
Show file tree
Hide file tree
Showing 24 changed files with 3,136 additions and 35 deletions.
6 changes: 6 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,9 @@ jobs:
- run:
name: lint example/express/express-service
command: cd example/express/express-service && npm i && npm run lint
- run:
name: lint example/userToAdminAPI/frontend
command: cd example/userToAdminAPI/frontend && npm i && npm run lint
- run:
name: lint example/userToAdminAPI/express-service
command: cd example/userToAdminAPI/express-service && npm i && npm run lint
2 changes: 2 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@ jobs:
- run: cd example/chain-service-calls/service3 && npm i && npm run lint
- run: cd example/express/frontend && npm i && npm run lint
- run: cd example/express/express-service && npm i && npm run lint
- run: cd example/userToAdminAPI/frontend && npm i && npm run lint
- run: cd example/userToAdminAPI/express-service && npm i && npm run lint


152 changes: 122 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Implementation [Keycloak](https://www.keycloak.org/) adapter for aws Lambda
- supports AWS API Gateway, AWS Cloudfront with Lambda@Edge
- Resource based authorization ( [Keycloak Authorization Services](https://www.keycloak.org/docs/latest/authorization_services/) )
- works with non amazon services.
- [Service to Service communication](./example/userToAdminAPI).
- validate expiration of JWT token
- validate JWS signature
- supports "clientId/secret" and "client-jwt" credential types
Expand All @@ -29,6 +30,7 @@ npm install keycloak-lambda-authorizer -S
- [Serverless example (Api gateway with lambda authorizer)](example/keycloak-authorizer/README.md)
- [Example of expressjs middleware](example/express)
- [Example of calling a chain of micro services, where each service is protected by its secured client](example/chain-service-calls)
- [Example of calling the Admin API Using the regular User Permissions (Role or Resource)](example/userToAdminAPI)
- [CloudFront with Lambda:Edge example](example/keycloak-cloudfront/README.md)
- [CloudFront with portal authorization (switching between security realms)](example/keycloak-cloudfront-portal)
# How to use
Expand All @@ -48,6 +50,21 @@ export function authorizer(event, context, callback) {
});
}
```
### Client Role Based
```javascript
import { apigateway } from 'keycloak-lambda-authorizer';

export function authorizer(event, context, callback) {
const keycloakJSON = ...; // read Keycloak.json
awsAdapter.awsHandler(event, keycloakJSON, {
enforce: { enabled: true, clientRole: {roleName: 'SOME_ROLE',clientId: 'Client Name',}, },
}).then((token)=>{
// Success
}).catch((e)=>{
// Failed
});
}
```

### Resource Based (Keycloak Authorization Services)
```javascript
Expand Down Expand Up @@ -167,6 +184,110 @@ export function authorizer(event, context, callback) {
}
```

## ExpressJS middleware

```js
const fs = require('fs');
const { middlewareAdapter } = require('keycloak-lambda-authorizer');

function getKeycloakJSON() {
return JSON.parse(fs.readFileSync(`${__dirname}/keycloak.json`, 'utf8'));
}

const app = express();

app.get('/expressServiceApi', middlewareAdapter(
getKeycloakJSON(),
{
enforce: {
enabled: true,
resource: {
name: 'service-api',
},
},
},
).middleware,
async (request, response) => {
response.json({
message: `Hi ${request.jwt.payload.preferred_username}. Your function executed successfully!`,
});
});
```

## Get Service Account Token
- ExpressJS
```js
const fs = require('fs');
const { middlewareAdapter } = require('keycloak-lambda-authorizer');

function getKeycloakJSON() {
return JSON.parse(fs.readFileSync(`${__dirname}/keycloak.json`, 'utf8'));
}

const app = express();

app.get('/expressServiceApi', middlewareAdapter(
getKeycloakJSON(),
{
enforce: {
enabled: true,
resource: {
name: 'service-api',
},
},
},
).middleware,
async (request, response) => {
const serviceAccountToken = await request.serviceAccountJWT();
...
});
```
- AWS Lambda/Serverless or another cloud

```js
const { serviceAccountJWT } = require('keycloak-lambda-authorizer/src/serviceAccount');

const keycloakJSON = ...

async function getServiceAccountJWT(){
return serviceAccountJWT(keycloakJSON);
}

...

const serviceAccountToken = await getServiceAccountJWT();
```

- AWS Lambda/Serverless or another cloud with Signed JWT

```js
const { serviceAccountJWT } = require('keycloak-lambda-authorizer/src/serviceAccount');

const keycloakJSON = ...
const options = {
"keys":{
"privateKey":{
"key": privateKey,
"passphrase": 'privateKey passphrase'
},
"publicKey":{
"key": publicKey,
}
}
}

async function getServiceAccountJWT(){
return serviceAccountJWT(keycloakJSON,options);
}

...

const serviceAccountToken = await getServiceAccountJWT();
```




## Cache
Example of cache:
```javascript
Expand Down Expand Up @@ -321,6 +442,7 @@ export function authorizer(event, context, callback) {
});
}
```

# Lambda:Edge
## 1. protect Url

Expand Down Expand Up @@ -730,35 +852,5 @@ keycloakJson,
});
}
```
## 15. ExpressJS middleware

```
const fs = require('fs');
const { middlewareAdapter } = require('keycloak-lambda-authorizer');
function getKeycloakJSON() {
return JSON.parse(fs.readFileSync(`${__dirname}/keycloak.json`, 'utf8'));
}
const app = express();
app.get('/expressServiceApi', middlewareAdapter(
getKeycloakJSON(),
{
enforce: {
enabled: true,
resource: {
name: 'service-api',
},
},
},
).middleware,
async (request, response) => {
response.json({
message: `Hi ${request.jwt.payload.preferred_username}. Your function executed successfully!`,
});
});
```


# If you find these useful, please [Donate](https://secure.wayforpay.com/button/b18610f33a01c)!
39 changes: 39 additions & 0 deletions __tests__/src/serviceAccountTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
jest.mock('../../src/umaConfiguration');
jest.mock('../../src/clientAuthorization');
jest.mock('../../src/utils/optionsUtils');

const { clientAuthentication } = require('../../src/clientAuthorization');

const { serviceAccountJWT } = require('../../src/serviceAccount');

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': {},
});

describe('testing umaConfiguration', () => {
beforeEach(() => {
clientAuthentication.mockImplementation(async () => ({ access_token: 'access_token' }));
});

afterEach(() => {
});

test('test serviceAccountJWT with keycloakJson', async () => {
const token = await serviceAccountJWT(keycloakJson);
expect(token).toEqual('access_token');
});

test('test serviceAccountJWT with options', async () => {
const token = await serviceAccountJWT(null, { keycloakJson });
expect(token).toEqual('access_token');
});
});
27 changes: 27 additions & 0 deletions __tests__/src/umaConfigurationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ const token = {
'accessRole',
],
},
resource_access: {
testClient: {
roles: [
'accessRole',
],
},
},
authorization: {
permissions: [{ rsid: 'resourceId' }],
},
Expand All @@ -30,6 +37,13 @@ const accessToken = {
roles: [
'accessRole',
],
resource_access: {
testClient: {
roles: [
'accessRole',
],
},
},
},
},
};
Expand Down Expand Up @@ -82,6 +96,19 @@ describe('testing umaConfiguration', () => {
});
});

test('test enforce client Role success', async () => {
await enforce(token, {
keycloakJson,
enforce: {
enabled: true,
clientRole: {
roleName: 'accessRole',
clientId: 'testClient',
},
},
});
});

test('test access_token success', async () => {
await enforce(accessToken, {
cache,
Expand Down
Binary file added docs/UserToAdminAPI.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/serviceAccountRoles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions example/userToAdminAPI/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Example of Calling the Admin API Using User Permissions (Role or Resource)

This example allow to get list of users and List of security clients (with secrets) using regular user permissions.

## How it works
User calls the service API using their own token, but the service API calls Keycloak using the service account token (service-to-service communication)
- **user has no administrator roles!!!**
- service Account has Admin Roles ![](../../docs/serviceAccountRoles.png)
- FrontEnd does not have access to call Admin Api.
![](../../docs/UserToAdminAPI.png)

## 1. Start Keycloak

### Docker
Using the image from https://hub.docker.com/r/jboss/keycloak/
```
docker run -p 8090:8080 -e JAVA_OPTS="-Dkeycloak.profile.feature.scripts=enabled -Dkeycloak.profile.feature.upload_scripts=enabled -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true" -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -v `pwd`/example/userToAdminAPI:/userToAdminAPI -e KEYCLOAK_IMPORT=/userToAdminAPI/example-realm-export.json jboss/keycloak
```
### Standard
```
sh bin/standalone.sh -c standalone.xml -b 0.0.0.0 -Djboss.bind.address.management=0.0.0.0 --debug 8190 -Djboss.http.port=8090
```
Open the Keycloak admin console, click on Add Realm, click on import 'Select file', select example-realm-export.json and click Create.

## 2. Run Services Locally
- Express Service
```bash
cd express-service
npm i
npm run start
```

## 3. Run UI locally

```bash
cd frontend
npm i
npm run start
```

## 4. Open UI
[http://localhost:3001](http://localhost:3001)

users:

| User | Password | UserList Role | Client List Role | Client Secret Role |
|:----------|:-----------|:-----------------|:-----------------|:-------------------|
| user | user | X | X | X |
| user1 | user1 | - | - | - |

## 6. Results

| User | Result | Description |
|:----------|:-------------------------------------------------------------------------------------------------------|:------------------------------------------------------|
| User | User List, Client List with secrets | All Access |
| User1 | Client List with secrets | User has access to Client List and secrets |
| User2 | Client List without secrets | User has access only to Client List |
Loading

0 comments on commit bc527a1

Please sign in to comment.