Skip to content

Commit

Permalink
[MODCONSKC-65] - Custom Authentication Flow Configuration (#151)
Browse files Browse the repository at this point in the history
* [MODCONSKC-65] - Custom Authentication Flow Configuration
  • Loading branch information
azizbekxm authored Feb 21, 2025
1 parent a54f68a commit 95d89c9
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 21 deletions.
106 changes: 98 additions & 8 deletions src/main/java/org/folio/consortia/client/KeycloakClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.List;
import java.util.Map;

import org.folio.consortia.config.keycloak.KeycloakFeignClientConfig;
import org.folio.consortia.domain.dto.KeycloakClientCredentials;
import org.folio.consortia.domain.dto.KeycloakIdentityProvider;
import org.folio.consortia.domain.dto.KeycloakTokenResponse;
import org.folio.consortia.domain.dto.RealmExecutions;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
Expand Down Expand Up @@ -60,6 +65,18 @@ KeycloakIdentityProvider getIdentityProvider(@PathVariable("realm") String realm
@PathVariable("alias") String alias,
@RequestHeader(AUTHORIZATION) String token);

/**
* Create identity provider in a realm
*
* @param realm realm to create identity provider in
* @param identityProvider identity provider to create
* @param token authorization token
*/
@PostMapping("/admin/realms/{realm}/identity-provider/instances")
void createIdentityProvider(@PathVariable("realm") String realm,
@RequestBody KeycloakIdentityProvider identityProvider,
@RequestHeader(AUTHORIZATION) String token);

/**
* Delete identity provider by alias in a realm
*
Expand All @@ -73,15 +90,88 @@ void deleteIdentityProvider(@PathVariable("realm") String realm,
@RequestHeader(AUTHORIZATION) String token);

/**
* Create identity provider in a realm
* Retrieves the realm configuration for the specified tenant.
*
* @param realm realm to create identity provider in
* @param identityProvider identity provider to create
* @param token authorization token
* @param tenant the tenant identifier
* @param token the authorization token
* @return the realm configuration as an ObjectNode
*/
@PostMapping("/admin/realms/{realm}/identity-provider/instances")
void createIdentityProvider(@PathVariable("realm") String realm,
@RequestBody KeycloakIdentityProvider identityProvider,
@RequestHeader(AUTHORIZATION) String token);
@GetMapping(value = "admin/realms/{tenant}")
ObjectNode getRealm(@PathVariable("tenant") String tenant,
@RequestHeader(AUTHORIZATION) String token);

/**
* Updates the realm configuration for the specified tenant.
*
* @param tenant the tenant identifier
* @param realm the updated realm configuration as a JsonNode
* @param token the authorization token
*/
@PutMapping(value = "/admin/realms/{tenant}")
void updateRealm(@PathVariable("tenant") String tenant,
@RequestBody JsonNode realm,
@RequestHeader(AUTHORIZATION) String token);

/**
* Duplicates the built-in browser authentication flow for the specified tenant.
*
* @param tenant the tenant identifier
* @param copyRequest the request containing the new flow name
* @param token the authorization token
*/
@PostMapping(value = "/admin/realms/{tenant}/authentication/flows/browser/copy")
void copyBrowserFlow(@PathVariable("tenant") String tenant,
@RequestBody Map<String, ?> copyRequest,
@RequestHeader(AUTHORIZATION) String token);

/**
* Retrieves the executions for the specified authentication flow in the specified tenant.
*
* @param tenant the tenant identifier
* @param flowName the name of the authentication flow
* @param token the authorization token
* @return the list of executions for the specified flow
*/
@GetMapping(value = "/admin/realms/{tenant}/authentication/flows/{flowName}/executions")
List<RealmExecutions> getExecutions(@PathVariable("tenant") String tenant,
@PathVariable("flowName") String flowName,
@RequestHeader(AUTHORIZATION) String token);

/**
* Adds an execution to the specified authentication flow in the specified tenant.
*
* @param tenant the tenant identifier
* @param flowName the name of the authentication flow
* @param executionRequest the request containing the execution details
* @param token the authorization token
*/
@PostMapping(value = "/admin/realms/{tenant}/authentication/flows/{flowName}/executions/execution")
void executeBrowserFlow(@PathVariable("tenant") String tenant,
@PathVariable("flowName") String flowName,
@RequestBody Map<String, ?> executionRequest,
@RequestHeader(AUTHORIZATION) String token);

/**
* Raises the priority of the specified execution in the specified tenant.
*
* @param tenant the tenant identifier
* @param executionId the identifier of the execution to raise priority
* @param token the authorization token
*/
@PostMapping(value = "/admin/realms/{tenant}/authentication/executions/{executionId}/raise-priority")
void raisePriority(@PathVariable("tenant") String tenant,
@PathVariable("executionId") String executionId,
@RequestHeader(AUTHORIZATION) String token);

/**
* Deletes the specified execution from the specified tenant.
*
* @param tenant the tenant identifier
* @param executionId the identifier of the execution to delete
* @param token the authorization token
*/
@DeleteMapping(value = "/admin/realms/{tenant}/authentication/executions/{executionId}")
void deleteExecution(@PathVariable("tenant") String tenant,
@PathVariable("executionId") String executionId,
@RequestHeader(AUTHORIZATION) String token);
}
23 changes: 23 additions & 0 deletions src/main/java/org/folio/consortia/domain/dto/RealmExecutions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.folio.consortia.domain.dto;

import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.With;

@Data
@With
@AllArgsConstructor
@NoArgsConstructor
public class RealmExecutions {
private String id;
private String requirement;
private String displayName;
private List<String> requirementChoices;
private boolean configurable;
private String providerId;
private int level;
private int index;
private int priority;
}
20 changes: 19 additions & 1 deletion src/main/java/org/folio/consortia/service/KeycloakService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
package org.folio.consortia.service;


import org.folio.consortia.domain.dto.Tenant;

public interface KeycloakService {

/**
* Adds a custom authentication flow for a central tenant.
* <p>
* This method performs the following steps:
* 1. Duplicates the built-in browser authentication flow.
* 2. Adds a custom ECS Folio authentication form provider to the duplicated flow.
* 3. Fetches executions from the current flow.
* 4. Deletes the default auth-username-password-form execution from the flow.
* 5. Raises the priority of the custom ECS Folio authentication form provider.
* 6. Binds the custom flow to the realm.
*
* @param tenant the tenant for which the custom authentication flow is to be added
* @throws IllegalStateException if the required executions are not found
*/
void addCustomAuthFlowForCentralTenant(Tenant tenant);

/**
* Creates an identity provider in the central tenant realm for the member tenant with conditions:<br/>
* 1) In case the identity provider already exists, it will not be created again to
Expand All @@ -21,5 +40,4 @@ public interface KeycloakService {
* @param memberTenantId member tenant
*/
void deleteIdentityProvider(String centralTenantId, String memberTenantId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import static org.folio.consortia.utils.KeycloakUtils.buildIdpClientConfig;

import java.util.Map;

import com.fasterxml.jackson.databind.node.ObjectNode;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.folio.consortia.client.KeycloakClient;
import org.folio.consortia.config.keycloak.KeycloakIdentityProviderProperties;
import org.folio.consortia.domain.dto.KeycloakIdentityProvider;
import org.folio.consortia.domain.dto.Tenant;
import org.folio.consortia.service.KeycloakCredentialsService;
import org.folio.consortia.service.KeycloakService;
import org.springframework.stereotype.Service;
Expand All @@ -21,15 +26,66 @@
@Log4j2
public class KeycloakServiceImpl implements KeycloakService {

private static final String CUSTOM_BROWSER_FLOW = "custom-browser";
private static final String ECS_FOLIO_AUTH_USRNM_PWD_FORM = "ecs-folio-auth-usrnm-pwd-form";
private static final String AUTH_USERNAME_PASSWORD_FORM = "auth-username-password-form";
private static final String CUSTOM_BROWSER_FLOW_FORMS = CUSTOM_BROWSER_FLOW + "%20forms";
private static final String KEYCLOAK_PROVIDER_ID = "keycloak-oidc";

private final KeycloakClient keycloakClient;
private final KeycloakIdentityProviderProperties keycloakIdpProperties;
private final KeycloakCredentialsService keycloakCredentialsService;

@Override
public void addCustomAuthFlowForCentralTenant(Tenant tenant) {
log.debug("Trying to add custom authentication flow for tenant with id={}", tenant.getId());
if (isUnifiedLoginDisabled()) {
log.info("addCustomAuthFlowForCentralTenant:: Identity provider creation is disabled. Skipping creation for tenant {}", tenant.getId());
return;
}
if (Boolean.FALSE.equals(tenant.getIsCentral())) {
log.info("addCustomAuthFlowForCentralTenant:: Tenant with id={} is not central, skipping custom authentication flow addition", tenant.getId());
return;
}

var token = keycloakCredentialsService.getMasterAuthToken();
var tenantId = tenant.getId();

// 1. Duplicate built-in browser authentication flow
var browserFlowCopyConfig = Map.of("newName", CUSTOM_BROWSER_FLOW);
keycloakClient.copyBrowserFlow(tenantId, browserFlowCopyConfig, token);

// 2. Add custom ecs folio authentication form provider to the duplicated flow
var browserFlowProviderConfig = Map.of("provider", ECS_FOLIO_AUTH_USRNM_PWD_FORM);
keycloakClient.executeBrowserFlow(tenantId, CUSTOM_BROWSER_FLOW_FORMS, browserFlowProviderConfig, token);

// 3. Fetch executions from current flow
var executions = keycloakClient.getExecutions(tenantId, CUSTOM_BROWSER_FLOW, token);
var authUsernamePasswordFormExecution = executions.stream()
.filter(execution -> StringUtils.equals(execution.getProviderId(), AUTH_USERNAME_PASSWORD_FORM))
.findFirst()
.orElseThrow(() -> new IllegalStateException("auth-username-password-form execution not found"));
var ecsFolioAuthUsernamePasswordFormExecution = executions.stream()
.filter(execution -> StringUtils.equals(execution.getProviderId(), ECS_FOLIO_AUTH_USRNM_PWD_FORM))
.findFirst()
.orElseThrow(() -> new IllegalStateException("ecs-folio-auth-usrnm-pwd-form execution not found"));

// 4. Delete default auth-username-password-form execution from the flow
keycloakClient.deleteExecution(tenant.getId(), authUsernamePasswordFormExecution.getId(), token);

// 5. Raise priority of the custom ecs folio authentication form provider
keycloakClient.raisePriority(tenantId, ecsFolioAuthUsernamePasswordFormExecution.getId(), token);

// 6. Bind the custom flow to the realm
ObjectNode realm = keycloakClient.getRealm(tenantId, token);
realm.put("browserFlow", CUSTOM_BROWSER_FLOW);
keycloakClient.updateRealm(tenantId, realm, token);
log.info("addCustomAuthFlowForCentralTenant:: Custom authentication flow successfully added for tenant with id={}", tenantId);
}

@Override
public void createIdentityProvider(String centralTenantId, String memberTenantId) {
if (isIdpCreationDisabled()) {
if (isUnifiedLoginDisabled()) {
log.info("createIdentityProvider:: Identity provider creation is disabled. Skipping creation for tenant {}", memberTenantId);
return;
}
Expand Down Expand Up @@ -58,7 +114,7 @@ public void createIdentityProvider(String centralTenantId, String memberTenantId

@Override
public void deleteIdentityProvider(String centralTenantId, String memberTenantId) {
if (isIdpCreationDisabled()) {
if (isUnifiedLoginDisabled()) {
log.info("deleteIdentityProvider:: Identity provider creation is disabled. Skipping deletion for tenant {}", memberTenantId);
return;
}
Expand All @@ -84,7 +140,7 @@ private boolean identityProviderExists(String realm, String providerAlias, Strin
}
}

private boolean isIdpCreationDisabled() {
private boolean isUnifiedLoginDisabled() {
return BooleanUtils.isNotTrue(keycloakIdpProperties.getEnabled());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

import java.util.Objects;
import java.util.UUID;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.ObjectUtils;
import org.folio.consortia.client.ConsortiaConfigurationClient;
import org.folio.consortia.client.UserTenantsClient;
Expand Down Expand Up @@ -40,9 +41,6 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Log4j2
@Service
@RequiredArgsConstructor
Expand All @@ -68,6 +66,7 @@ public class TenantManagerImpl implements TenantManager {
private final ExecutionContextBuilder contextBuilder;
private final FolioExecutionContext folioExecutionContext;


@Override
public TenantCollection get(UUID consortiumId, Integer offset, Integer limit) {
return tenantService.get(consortiumId, offset, limit);
Expand All @@ -82,10 +81,9 @@ public Tenant save(UUID consortiumId, UUID adminUserId, Tenant tenantDto) {
tenantService.checkTenantUniqueNameAndCodeOrThrow(tenantDto);

createCustomFieldIfNeeded(tenantDto.getId());
keycloakService.addCustomAuthFlowForCentralTenant(tenantDto);

var existingTenant = tenantService.getByTenantId(tenantDto.getId());

// checked whether tenant exists or not.
return existingTenant != null
? reAddSoftDeletedTenant(consortiumId, existingTenant, tenantDto)
: addNewTenant(consortiumId, tenantDto, adminUserId);
Expand Down
Loading

0 comments on commit 95d89c9

Please sign in to comment.