Skip to content

Commit

Permalink
Merge pull request #32 from FireBlinkLTD/feat/configuration_examples
Browse files Browse the repository at this point in the history
feat: add idp specific configuration and mappings match  mode
  • Loading branch information
vlad-tkachenko committed Jan 2, 2024
2 parents 3a6fa41 + 9b85183 commit 4c9752e
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 9 deletions.
4 changes: 3 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ MAPPINGS_PAGES='[
{
"pattern": "/pages/.*",
"auth": {
"mode": "all",
"claims": {
"realm": [ "test_role" ]
"realm": [ "test_role" ],
"account": [ "manage-account" ]
}
}
},
Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ Mappings format:
# [optional] when "false", allows either unauthorized or a claims hit to GRANT access, default value "true", meaning only authorized access is allowed
"required": false,

# [optional] claims matching mode, can be:
# "ANY" - default value, any of the claims grant user access
# "ALL" - only when all the claims are included access will be granted
"mode": "ANY",

# [optional] list of JWT claims to match over, note: when "auth.required" is true "auth.claims" should be provided too
"claims": {
# claims can reference one or many named paths (refer to the JWT_AUTH_CLAIM_PATHS environment variable configuration)
Expand Down Expand Up @@ -212,12 +217,21 @@ Upstream service may respond with custom headers to control the prxi-openid-conn
|-------------------------|-------|-------------|
| `X-Prxi-Refresh-Tokens` | any | When header is returned by the upstream service, prxi-openid-connect will refresh any existing tokens (access/id) |

## HTMX Support

Every time prxi-openid-connect needs to send a redirect it checks an incoming request to have the `Hx-Boosted` header. If header is found and its value is `true` then prxi-openid-connect will return `200` status code with [Hx-Redirect](https://htmx.org/reference/#response_headers) header instead of making a standard HTTP redirect.

## Links

- [Docker Image](https://hub.docker.com/r/fireblink/prxi-openid-connect) official Docker image
- [GitHub Repository](https://github.com/FireBlinkLTD/prxi-openid-connect)
- [@prixi/dev](https://www.npmjs.com/package/@prxi/dev) a simple CLI reverse proxy tool for local development purposes, can be handy to simulate fireblink/prxi-openid-connect setup without a need to run docker container and/or setup test IDP configuration

## IDP Specific Configuration Details

- [AWS Cognito User Pool](https://github.com/FireBlinkLTD/prxi-openid-connect/blob/main/docs/idp/Cognito.md)
- [Keycloak](https://github.com/FireBlinkLTD/prxi-openid-connect/blob/main/docs/idp/Keycloak.md)

## License

This project is distributed under dual licensing.
Expand All @@ -236,6 +250,3 @@ More can be found in [LICENSE.md](https://github.com/FireBlinkLTD/prxi-openid-co

To obtain a commercial license [click here](https://fireblink.com/#contact-us) to get in a contact.

## HTMX Support

Every time prxi-openid-connect needs to send a redirect it checks an incoming request to have the `Hx-Boosted` header. If header is found and its value is `true` then prxi-openid-connect will return `200` status code with [Hx-Redirect](https://htmx.org/reference/#response_headers) header instead of making a standard HTTP redirect.
36 changes: 36 additions & 0 deletions docs/idp/Cognito.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# AWS Cognito

## Client Configuration

```bash
# KeyCloak discovery URL to fetch client configuration from
# <region> should be replaced with an actual AWS Region where User Pool is located
# <pool id> should be replaced with the AWS Cognito User Pool ID
OPENID_CONNECT_DISCOVER_URL='https://cognito-idp.<region>.amazonaws.com/<pool id>/.well-known/openid-configuration'
# client id
OPENID_CLIENT_ID='<client id>'
# client secret
OPENID_CLIENT_SECRET='<client secret>'

# Information on where to extract cognito user groups
JWT_AUTH_CLAIM_PATHS='{
"groups": ["cognito:groups"]
}'
```

## Mappings

```yaml
{
# RegEx pattern
"pattern": ".*",

"auth": {
# allowed JWT claims
"claims": {
# allowed groups
"groups": [ "<group name>" ]
}
}
}
```
43 changes: 43 additions & 0 deletions docs/idp/Keycloak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# KeyCloak configuration example

## Client Configuration

```bash
# KeyCloak discovery URL to fetch client configuration from
# <host> should be replaced with an actual Keycloak host
# <realm> should be replaced with the realm name, by default KC creates "master" realm
OPENID_CONNECT_DISCOVER_URL='https://<host>/realms/<realm>/.well-known/openid-configuration'
# client id
OPENID_CLIENT_ID='<client id>'
# client secret
OPENID_CLIENT_SECRET='<client secret>'

# Information on where to extract realm and client specific roles
# <client id> should be replaced with an actual client ID
JWT_AUTH_CLAIM_PATHS='{
"realm": ["realm_access", "roles"],
"client": ["resource_access", "<client id>", "roles"]
}'
```

## Mappings

```yaml
{
# RegEx pattern
"pattern": ".*",

"auth": {
# require all roles to be presented, as by default mode is "ANY", meaning any of the specified roles grant user access
"mode": "ALL",

# allowed JWT claims
"claims": {
# allowed realm roles
"realm": [ "<realm-role>" ],
# allowed client roles
"client": [ "<client-role>" ],
}
}
}
```
11 changes: 11 additions & 0 deletions src/config/Mapping.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { HttpMethod } from "prxi";

export enum MAPPING_MODE {
'ANY' = 'ANY',
'ALL' = 'ALL',
}

export interface Mapping {
pattern: RegExp;
methods?: HttpMethod[];
Expand All @@ -9,6 +14,7 @@ export interface Mapping {
}[];
auth: {
required: boolean,
mode: MAPPING_MODE,
claims: Record<string, string[]>;
}
}
Expand Down Expand Up @@ -90,6 +96,11 @@ export const prepareMapping = (value: any): Mapping => {
value.auth.required = true;
}

if (!value.auth.mode) {
value.auth.mode = MAPPING_MODE.ANY;
}
value.auth.mode = value.auth.mode.toUpperCase();

// if no claims set, set default object
if (!value.auth.claims || JSON.stringify(value.auth.claims) === '{}') {
/* istanbul ignore next */
Expand Down
19 changes: 14 additions & 5 deletions src/utils/RequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { HttpMethod, Request } from "prxi";
import * as getRawBody from "raw-body";

import { Debugger } from "./Debugger";
import { Mapping } from "../config/Mapping";
import { MAPPING_MODE, Mapping } from "../config/Mapping";
import { getConfig } from "../config/getConfig";

export class RequestUtils {
Expand Down Expand Up @@ -80,27 +80,35 @@ export class RequestUtils {
idTokenJWT,
], authClaimPaths);

let pass = false;
let intersectionLength = 0;
let expectedLength = 0;
for (const key of Object.keys(auth.claims)) {
matchingAuthClaims[key] = [];
const expectedKeyClaims = auth.claims[key];
const jwtKeyClaims = allAuthClaims[key] = jwtClaims[key];
expectedLength += expectedKeyClaims.length;

if (jwtKeyClaims?.length) {
const intersection = expectedKeyClaims.filter(claim => jwtKeyClaims.includes(claim));
if (intersection.length) {
matchingAuthClaims[key] = intersection;
pass = true;
intersectionLength += intersection.length;
}
}
}

if (pass) {
_.debug('Found intersection of claims, access allowed', {
if (intersectionLength) {
_.debug('Found intersection of claims', {
matchingAuthClaims,
mode: auth.mode,
});
}

let pass = intersectionLength > 0;
if (auth.mode === MAPPING_MODE.ALL) {
pass = intersectionLength === expectedLength;
}

if (pass || !auth.required) {
const proxy = RequestUtils.extractRawJWTClaims([
accessTokenJWT,
Expand All @@ -119,6 +127,7 @@ export class RequestUtils {
_.info('No intersection of claims found, access denied', {
expectedClaims: auth.claims,
actualClaims: jwtClaims,
mode: auth.mode,
});

return false;
Expand Down

0 comments on commit 4c9752e

Please sign in to comment.