Skip to content

Commit

Permalink
Implement "Select Organization" Authenticator. (#172)
Browse files Browse the repository at this point in the history
* Implement "Select Organization" Authenticator.
- Added ActiveOrganizationAuthenticator
- Added ActiveOrganizationAuthenticatorFactory
- Added "select-organization.ftl"
- Added Documentation
- Did Manual Testing
- Add built-in browser and direct grant flows
- Add Cypress tests
- Extend documentation
- Rebased on main
- Fix conflict, Update tests according to v24 changes, add auth flow creation on PostMigrationEvent
- Rework code of ActiveOrganizationAuthenticator
- remove useless lines of code, add debug logs
- revert cypress testcontainers config changes

---

Manual Testing report:
Browser Flow
**Authenticator is Enabled**
- prompt=select_account is not used
    - User has no Organizations = User is not required to select an organization. OK
    - User has 1 or more Organization = User is not required to select an organization. OK
- prompt=select_account is used
    - User has no Organizations = User get "You are not part of any organization, Contact an Administrator." error. OK
    - User has 1 Organization = User doesn't have to select an organization. OK
    - User has 2 Organization = User have to select an organization. OK
- account_hint=<org-id> is used (with a right organization)
    - User has no Organizations = User get "Invalid Organization." error. OK
    - User has 1 or more Organization = User doesn't have to select an organization. OK
- account_hint=<org-id> is used (with a wrong organization)
    - All users get "Invalid Organization." error. OK

**Authenticator is Disabled**
- prompt=select_account is used
    - Users doesn't have to choose an organization. OK
- account_hint=<org-id> is used
    - Account hint is ignored. OK

Direct Grant Flow
**Authenticator is Enabled**
- account_hint=<org-id> is used with a right organization
    - Authentication is successfull.
- account_hint=<org-id> is used with a wrong organization
    - Authentication fails with a 401 status.

All case
In all case, if the authentication is successfull, the attribute is updated.
In all case, if the authentication is failure, the attribute isn't updated.

* refactor - clean code

* Put Cypress tests under cypress-tests profile, Fix tests conflicts by creating an AbstractCypressOrganizationTest (separate container), Update cypress documentation
  • Loading branch information
MGLL committed Apr 9, 2024
1 parent ad4f281 commit 6d5c9d7
Show file tree
Hide file tree
Showing 48 changed files with 3,456 additions and 120 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ dependency-reduced-pom.xml
.vscode
.swagger*
src/main/resources/META-INF/jpa-changelog-phasetwo-full.xml

# cypress
node_modules
src/test/e2e/cypress/reports
src/test/e2e/cypress/videos
src/test/e2e/cypress/screenshots
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The extensions herein are used in the [Phase Two](https://phasetwo.io) cloud off
- [Definitions](#definitions)
- [Quick start](#quick-start)
- [Building](#building)
- [Cypress-Test](#cypress-test)
- [Installation](#installation)
- [Admin UI](#admin-ui)
- [Compatibility](#compatibility)
Expand All @@ -30,6 +31,7 @@ The extensions herein are used in the [Phase Two](https://phasetwo.io) cloud off
- [Invitations](#invitations)
- [IdP Discovery](#idp-discovery)
- [Import/Export organizations](#importexport-organizations)
- [Active Organization](#active-organization)
- [License](#license)

## Overview
Expand Down Expand Up @@ -75,6 +77,10 @@ And then run the build with the tests using the `test` profile:
mvn clean install -Ptest
```

## Cypress Test
For more information you can refer to [cypress-tests](./docs/cypress-tests.md).


## Installation

The maven build uses the shade plugin to package a fat-jar with all dependencies, except for the [`keycloak-admin-client`](https://mvnrepository.com/artifact/org.keycloak/keycloak-admin-client). Put the `keycloak-orgs` jar and `keycloak-admin-client` jar (that corresponds to your Keycloak version) in your `provider` (for Quarkus-based distribution) or in `standalone/deployments` (for Wildfly, legacy distribution) directory and restart Keycloak. It is unknown if these extensions will work with hot reloading using the legacy distribution.
Expand Down
141 changes: 141 additions & 0 deletions docs/active-organization-authenticator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Active Organizations Authenticator

## Contents
<!-- TOC -->
* [Active Organizations Authenticator](#active-organizations-authenticator)
* [Contents](#contents)
* [Overview](#overview)
* [Explanation](#explanation)
* [prompt=select_account](#promptselect_account)
* [account_hint](#account_hint)
* [account switching](#account-switching)
* [Built-in Flows](#built-in-flows)
* [Select Org Browser Flow](#select-org-browser-flow)
* [Configuration](#configuration)
* [Flow](#flow)
* [Select Org Direct Grant Flow](#select-org-direct-grant-flow)
* [Configuration](#configuration-1)
* [Flow](#flow-1)
* [Configuration & Example](#configuration--example)
* [Browser Flow (Configuration)](#browser-flow-configuration)
* [Examples (Tests)](#examples-tests)
* [Direct Grant Flow (Configuration)](#direct-grant-flow-configuration)
<!-- TOC -->

## Overview
This documentation is dedicated to the use of "Select Organization" Authenticator.
Note that to have the active_organization information into tokens, you still need the "Active Organization" mapper.

## Explanation
You can add the `Select Organization` authenticator into the user authentication flow.
With this authenticator enabled, you can pass the query parameters:
- `prompt=select_account`
- `account_hint={ORG-ID}`

In all case, if the use try to access an organization where he isn't a member, an error will be returned.


### prompt=select_account
If the `prompt=select_account` is given for the authentication flow, the user will be requested to select an organization during login.
Note that:
- If the user doesn't have any organization, an error is returned.
- If the user has only 1 organization, the selection will be skipped, and it will use this organization.

_Example of authentication request:_
- `{HOSTNAME}/realms/{REALM}/protocol/openid-connect/auth?response_type=code&client_id={PUBLIC-CLIENT}&scope=openid&redirect_uri={HOSTNAME}/realms/{REALM}/account&prompt=select_account`


### account_hint
If the `account_hint={ORG-ID}` is given for the authentication flow, it will use the given organization without having to select one.
_Example of authentication request:_
- `{HOSTNAME}/realms/{REALM}/protocol/openid-connect/auth?response_type=code&client_id={PUBLIC-CLIENT}&scope=openid&redirect_uri={HOSTNAME}/realms/{REALM}/account&account_hint={ORG-ID}`

_or with the `prompt=select_account`_:
- `{HOSTNAME}/realms/{REALM}/protocol/openid-connect/auth?response_type=code&client_id={PUBLIC-CLIENT}&scope=openid&redirect_uri={HOSTNAME}/realms/{REALM}/account&prompt=select_account&account_hint={ORG-ID}`


### account switching
To **switch account based on account_hint**, you will need to define the 'Select Organization' step after the Cookie step. For that, you will need to
define a Sub-Flow like this:
![account-switch](assets/active-organization-authenticator/account-switch.png)

Then making an authentication request with **account_hint**:
- `{HOSTNAME}/realms/{REALM}/protocol/openid-connect/auth?response_type=code&client_id={PUBLIC-CLIENT}&scope=openid&redirect_uri={HOSTNAME}/realms/{REALM}/account&account_hint={ORG-ID}`

Will skip the select organization form and switch the organization (change user's attribute).


## Built-in Flows
A **browser flow** and **direct grant flow** have been added. They are similar to the default Keycloak built-in flows but include the Select Organization step.

![built-in-flows](assets/active-organization-authenticator/built-in-flows.png)

### Select Org Browser Flow
#### Configuration
![browser-config](assets/active-organization-authenticator/browser-config.png)

#### Flow
![browser-flow](assets/active-organization-authenticator/browser-flow.png)

### Select Org Direct Grant Flow
#### Configuration
![direct-grant-config](assets/active-organization-authenticator/direct-grant-config.png)

#### Flow
![direct-grant-flow](assets/active-organization-authenticator/direct-grant-flow.png)


## Configuration & Example

### Browser Flow (Configuration)
To configure the Browser Flow, you need to duplicate the default `Browser flow` and modify it.
In the `forms` part _(with Username Password Form)_, select `Add Step`.

![add-step](assets/active-organization-authenticator/add-step.png)

In the selection, search and select `Select Organization`.

![select-org-step](assets/active-organization-authenticator/select-org-step.png)

Once selected, enable it by setting it to `Required`.

![enable-select-organization](assets/active-organization-authenticator/enable-select-organization.png)

Don't forget to bind this flow to "Browser flow".

![bind-flow](assets/active-organization-authenticator/bind-flow.png)

#### Examples (Tests)
Once configured, you can test the authenticator with an authentication request including `prompt=select_account`.
**Example:** `{HOSTNAME}/realms/{REALM}/protocol/openid-connect/auth?response_type=code&client_id={PUBLIC-CLIENT}&scope=openid&redirect_uri={HOSTNAME}/realms/{REALM}/account&prompt=select_account`.

Once the user enter his username (or email) and password, he will have a drop-down to select an organization.

![org-choice-1](assets/active-organization-authenticator/org-choice-1.png)

![org-choice-2](assets/active-organization-authenticator/org-choice-2.png)

If the user has no organization, instead of the selection step, he will get an Error message.

![no-org](assets/active-organization-authenticator/no-org.png)

If the user try to access an organization where is not a member (with `account_hint` for example), he will also get an error message.

![not-member](assets/active-organization-authenticator/not-member.png)

Not that doing the authentication request with `account_hint={ORG-ID}` will skip the selection during log-in but still verify that the user has an organization or has membership.


### Direct Grant Flow (Configuration)

`account_hint={ORG-ID}` of "Select Organization" authenticator can also be used for a `Direct Grant` flow.
For that, apply the same configuration as the browser flow to the direct grant flow.

![direct-grant](assets/active-organization-authenticator/direct-grant.png)

Then, when you make the direct grant authentication, you can pass the query parameter `account_hint{ORG-ID}` and it will be used to define the active_organization.

**Example:**
`{{HOSTNAME}}/realms/{{REALM}}/protocol/openid-connect/token?account_hint={ORG-ID}`

![direct-grant-2](assets/active-organization-authenticator/direct-grant-2.png)
9 changes: 8 additions & 1 deletion docs/active-organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* [Currently supported mode](#currently-supported-mode)
* [Important note (`ATTRIBUTE_MODE`) before v24.x+](#important-note-attribute_mode-before-v24x)
* [Code extension](#code-extension)
* [Authenticator](#authenticator)
* [Endpoints](#endpoints)
* [Switch Organization](#switch-organization)
* [Active organization](#active-organization)
Expand Down Expand Up @@ -55,6 +56,11 @@ Several files have been added or modified to cover the "active organization" fea
- [Modified `OrganizationResourceTest.java`](../src/test/java/io/phasetwo/service/resource/OrganizationResourceTest.java)
- Added `void testOrganizationSwitch()` to cover the switch organization logic with standard flow or 'malicious attack'.

## Authenticator
For a [spec-compliant](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) approach,
you can use the "Select Organization" authenticator based on `prompt=select_account`.
**For more information**, see [active-organization-authenticator](./active-organization-authenticator.md) doc.

## Endpoints
2 new endpoints were added.
### Switch Organization
Expand Down Expand Up @@ -206,7 +212,8 @@ As you can see, the user can't perform this action.
**End.**

Side note, if the user tries to get the active-organization without belonging to an organization,
the `404 NOT FOUND` response will be returned.
the `404 NOT FOUND` response will be returned.

![no-organization](assets/active-organization/flow/case-with-no-org.png)

## Attack Scenario and Mitigation (before v24.x+)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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/assets/cypress.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions docs/cypress-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Cypress Tests

## Contents
<!-- TOC -->
* [Cypress Tests](#cypress-tests)
* [Contents](#contents)
* [Setup](#setup)
* [pnpm](#pnpm)
* [cypress.config.ts](#cypressconfigts)
* [Running with Cypress](#running-with-cypress)
* [Cypress Testcontainers](#cypress-testcontainers)
<!-- TOC -->

## Setup
Versions used:
- Node: **20.9.0**
- npm: **10.2.3**
- pnpm: **8.10.2**

### pnpm
Cypress setup use pnpm, you will first need to install it:
```bash
npm install -g pnpm
```

Then in the `test/e2e` folder, install the dependencies
```bash
pnpm install
```

### cypress.config.ts
To configure cypress for you setup, modify `cypress.config.ts`.
```typescript
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:8080/',
reporter: 'cypress-multi-reporters',
reporterOptions: {
configFile: 'reporter-config.json'
}
},
});
```
For example, if your Keycloak instance or container is not running on http://localhost:8080/, modify it.

## Running the Cypress test
Cypress test are under a specific profile. To run them with maven use `-P cypress-tests`.
Example: `maven clean test -P cypress-tests`

## Running with Cypress
After the setting up your environment and configuring your `cypress.config.ts`.
You start cypress UI by executing the command
```bash
npx cypress open
```
in `test/e2e` folder.

You should get the Cypress UI.
![cypress](assets/cypress.png)

Then, click on `E2E Testing`, choose the browser you want and click on `Start E2E Testing in ...`.
After that, you can choose any `*.cy.ts` file in `Specs` to run the tests and debug.

## Cypress Testcontainers
A Cypress testcontainers based on https://github.com/wimdeblauwe/testcontainers-cypress is used.
It populates dynamically the baseUrl with the Keycloak testcontainers instance and run all tests in the
`test/e2e` folder.

To run the tests with the testcontainers, run the `runCypressTests()` in `CypressOrganizationTest.java` like any Junit test.
Loading

0 comments on commit 6d5c9d7

Please sign in to comment.