Skip to content

Commit

Permalink
Merge pull request #10 from Creoox/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
CreooxTech authored Jun 22, 2023
2 parents 9e4652f + 0c6c11f commit abafa81
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 18 deletions.
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ Currently tested providers:
| Provider | Version | Result | Comment |
| ------------------------------------------------------ | ------- | ------ | ------------- |
| [Keycloak](https://www.keycloak.org/) | 17.1 || |
| [SAP Commerce](https://help.sap.com/docs/SAP_COMMERCE) | ? | | Running tests |
| [SAP Commerce](https://help.sap.com/docs/SAP_COMMERCE) | ? | | |
| Google | ? || Planned |
| GitHub | ? || Planned |
| GitHub | || |

<br/>

Expand Down Expand Up @@ -73,6 +73,8 @@ Currently tested providers:
| LOGIN_SCOPE | string | No | Requested scope(s), defaults to "openid email profile" |
| LOGIN_COOKIE_NAME | string | No | Name of the browser cookie, only if LOGIN_WHEN_NO_TOKEN=true |
| LOGIN_SESSION_SECRET | string | No | Randomized secret for cookie-session |
| AUTH_ROLES_STRUCT | string | No | Structure of roles (list) in token payload (**dot** notation)|
| AUTH_ROLE_NAME | string | No | Role name to check in token roles |

Please mind that if <code>AUTH_ALLOW_UNSEC_OPTIONS</code> is set to <code>true</code>, then the endpoint that should
accept OPTIONS request, should provide separate rule for that and pass <code>X-Forwarded-Method: OPTIONS</code> header
Expand All @@ -88,12 +90,26 @@ to **cx-traefik-forward-auth** there, for instance (docker).
...
```

</details>
Additionally, both <code>AUTH_ROLES_STRUCT</code> and <code>AUTH_ROLE_NAME</code> have to be either set or empty. Object dot notation is presented below:

## Examples
`resource_access.dummy-client.roles`

and is equall to JSON notation:

```json
...
"resource_access": {
"dummy-client": {
"roles": [...],
},
},
```

</details>
<br/>

## Examples

<details>
<summary>Docker Standalone:</summary>

Expand Down Expand Up @@ -145,7 +161,7 @@ traefik:
- cx-example-net

traefik-forward-auth:
image: creoox/cx-traefik-forward-auth:1.1.3
image: creoox/cx-traefik-forward-auth:1.1.4
container_name: cx-example-traefik-forward-auth
env_file:
- ./cx-traefik-forward-auth.env
Expand All @@ -160,7 +176,6 @@ traefik:
```
</details>
<br/>
<details>
Expand All @@ -169,7 +184,6 @@ traefik:
Not tested -> TODO
</details>
<br/>
<details>
Expand All @@ -178,7 +192,6 @@ Not tested -> TODO
Not implemented -> TODO
</details>
<br/>
# Project setup (containerized)
Expand Down Expand Up @@ -243,7 +256,6 @@ make down-dev-env
```
</details>
<br/>
<details>
Expand Down
9 changes: 7 additions & 2 deletions app/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Application settings
APP_NAME=cx-traefik-forward-auth
APP_VERSION=1.1.3
APP_VERSION=1.1.4
APP_PORT=4181

## Environment settings
Expand All @@ -21,4 +21,9 @@ LOGIN_WHEN_NO_TOKEN=true
LOGIN_AUTH_FLOW=code
LOGIN_SCOPE=openid
LOGIN_COOKIE_NAME=cx_forward_auth
LOGIN_SESSION_SECRET=cyHkxMY0tWDNrxnutdfaNngk
LOGIN_SESSION_SECRET=cyHkxMY0tWDNrxnutdfaNngk

## Additional middelware behaviour settings
# Use them if you need to check roles in token payload
AUTH_ROLES_STRUCT=resource_access.dummy-client.roles
AUTH_ROLE_NAME=dummy-client-admin
13 changes: 13 additions & 0 deletions app/src/models/dotenvModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const dotenvVars_optionalStr = [
"LOGIN_SESSION_SECRET",
"AUTH_ENDPOINT",
"AUTH_ALLOW_UNSEC_OPTIONS",
"AUTH_ROLES_STRUCT",
"AUTH_ROLE_NAME",
] as const;
const dotenvVars_optionalNum = ["APP_PORT"] as const;

Expand Down Expand Up @@ -134,6 +136,7 @@ export function validateDotenvFile(): void {
);
}

// Check if numeric values are really numbers
const optVars = dotenvVars_optionalNum;
for (const variable in optVars) {
if (
Expand All @@ -144,5 +147,15 @@ export function validateDotenvFile(): void {
}
}

// Check coherence between AUTH_GROUP
if (
(process.env["AUTH_ROLES_STRUCT"] && !process.env["AUTH_ROLE_NAME"]) ||
(!process.env["AUTH_ROLES_STRUCT"] && process.env["AUTH_ROLE_NAME"])
) {
throw new Error(
"Variables: AUTH_ROLES_STRUCT and AUTH_ROLE_NAME must be either defined or skipped at the same time"
);
}

return;
}
27 changes: 20 additions & 7 deletions app/src/services/postAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { logger } from "./logger";
*
* @param payload
* @throws Error if token is inactive
* @todo case-based implementation.
* @throws Error if authorization group is missing in token payload
*/
export function validateTokenPayload<
T extends ActiveOidcToken,
Expand All @@ -16,10 +16,23 @@ export function validateTokenPayload<
if (payload.active !== undefined && !payload.active) {
throw new Error("Token is inactive.");
}
/* istanbul ignore next */
logger.debug(
payload.active !== undefined && payload.active
? "token active"
: "token valid"
);

const authGroupName = process.env.AUTH_ROLE_NAME;
if (authGroupName) {
const groupStruct = process.env.AUTH_ROLES_STRUCT?.split(
"."
) as Array<string>;

const groupsAssigned = groupStruct.reduce((acc, field) => {
return acc[field];
/* eslint-disable @typescript-eslint/no-explicit-any */
}, payload as any) as Array<string>;

if (!groupsAssigned.includes(authGroupName)) {
logger.debug(
`Missing demanded authorization group ${authGroupName} in token payload.`
);
throw new Error(`Missing access rights.`);
}
}
}
32 changes: 32 additions & 0 deletions app/tests/models/dotenvModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const dotenvFile: DotenvFile = {
OIDC_VERIFICATION_TYPE: "jwt",
LOGIN_WHEN_NO_TOKEN: false,
JWT_STRICT_AUDIENCE: false,
AUTH_ROLES_STRUCT: "groups.special",
AUTH_ROLE_NAME: "dummy-group",
};

describe("Validator for dotenv files", () => {
Expand Down Expand Up @@ -203,4 +205,34 @@ describe("Validator for dotenv files", () => {
process.env.APP_PORT = "NaN";
expect(() => validateDotenvFile()).toThrow(Error);
});

it("fails if the `AUTH_ROLES_STRUCT` is set but `AUTH_ROLE_NAME` doesn't.", () => {
process.env = {
...process.env,
...stringifyObjectValues(dotenvFile),
};
process.env.AUTH_ROLES_STRUCT = "groups";
process.env.AUTH_ROLE_NAME = undefined;
expect(() => validateDotenvFile()).toThrow(Error);
});

it("fails if the `AUTH_ROLE_NAME` is set but `AUTH_ROLES_STRUCT` doesn't.", () => {
process.env = {
...process.env,
...stringifyObjectValues(dotenvFile),
};
process.env.AUTH_ROLES_STRUCT = undefined;
process.env.AUTH_ROLE_NAME = "dummy-group";
expect(() => validateDotenvFile()).toThrow(Error);
});

it("passes if both `AUTH_ROLE_NAME` and `AUTH_ROLES_STRUCT` are set.", () => {
process.env = {
...process.env,
...stringifyObjectValues(dotenvFile),
};
process.env.AUTH_ROLES_STRUCT = "groups";
process.env.AUTH_ROLE_NAME = "dummy-group";
expect(() => validateDotenvFile()).not.toThrow(Error);
});
});
41 changes: 41 additions & 0 deletions app/tests/services/postAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { logger } from "../../src/services/logger";

import { testTokenPayload } from "../testData";

const BASE_ENV = process.env;
jest.mock("../../src/services/logger");

describe("Post-Authenticator | Payload Validator", () => {
Expand All @@ -28,3 +29,43 @@ describe("Post-Authenticator | Payload Validator", () => {
expect(() => validateTokenPayload(inactiveToken)).toThrow(Error);
});
});

describe("Post-Authenticator | Role Validator", () => {
afterEach(() => {
process.env = { ...BASE_ENV };
});

it("validates legit roles in token", () => {
expect(() => validateTokenPayload(testTokenPayload)).not.toThrow(Error);
});

it("throws error for missing auth role when demanded", () => {
const changedToken: Partial<ActiveOidcToken> = {
...testTokenPayload,
resource_access: {
"dummy-client": {
roles: ["non-existing-role", "another-role"],
},
},
};
process.env.AUTH_ROLES_STRUCT = "resource_access.dummy-client.demanded-role.roles";
process.env.AUTH_ROLE_NAME = "demanded-role";

expect(() => validateTokenPayload(changedToken)).toThrow(Error);
});

it("doesn't throw error for missing auth role when not demanded", () => {
const changedToken: Partial<ActiveOidcToken> = {
...testTokenPayload,
resource_access: {
"dummy-client": {
roles: ["non-existing-role", "another-role"],
},
},
};
process.env.AUTH_ROLES_STRUCT = undefined;
process.env.AUTH_ROLE_NAME = undefined;

expect(() => validateTokenPayload(changedToken)).not.toThrow(Error);
});
});

0 comments on commit abafa81

Please sign in to comment.