Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,17 @@ public enum ErrorMessage {
// Organization application based issuer configuration related error messages.
ERROR_RETRIEVING_ALLOWED_ISSUERS("65603",
"Error occurred while retrieving allowed issuers for the tenant.",
"Unexpected error occurred while retrieving allowed issuers for the tenant.");
"Unexpected error occurred while retrieving allowed issuers for the tenant."),

// FAPI metadata related error messages.
ERROR_RETRIEVING_FAPI_METADATA("65604",
"Error occurred while retrieving FAPI metadata.",
"Unexpected error occurred while retrieving FAPI metadata for tenant: %s."),

// FAPI profile validation related error messages.
UNSUPPORTED_FAPI_PROFILE("65605",
"Unsupported FAPI profile.",
"The provided FAPI profile '%s' is not supported by the organization. Supported profiles: %s.");

private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.wso2.carbon.identity.oauth.ciba.api.CibaAuthServiceImpl;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
import org.wso2.carbon.identity.oauth2.config.services.OAuth2OIDCConfigOrgUsageScopeMgtService;
import org.wso2.carbon.identity.oauth2.fapi.services.FapiConfigMgtService;
import org.wso2.carbon.identity.organization.management.application.OrgApplicationManager;
import org.wso2.carbon.identity.sso.saml.SAMLSSOConfigServiceImpl;
import org.wso2.carbon.identity.template.mgt.TemplateManager;
Expand Down Expand Up @@ -151,6 +152,12 @@ private static class OAuthOIDCConfigOrgUsageScopeMgtServiceHolder {
.getOSGiService(OAuth2OIDCConfigOrgUsageScopeMgtService.class, null);
}

private static class FapiConfigMgtServiceHolder {

static final FapiConfigMgtService SERVICE = (FapiConfigMgtService) PrivilegedCarbonContext
.getThreadLocalCarbonContext().getOSGiService(FapiConfigMgtService.class, null);
}

/**
* Get ApplicationManagementService.
*
Expand Down Expand Up @@ -289,4 +296,14 @@ public static OAuth2OIDCConfigOrgUsageScopeMgtService getOAuth2OIDCConfigOrgUsag

return OAuthOIDCConfigOrgUsageScopeMgtServiceHolder.SERVICE;
}

/**
* Get FapiConfigMgtService.
*
* @return FapiConfigMgtService.
*/
public static FapiConfigMgtService getFapiConfigMgtService() {

return FapiConfigMgtServiceHolder.SERVICE;
}
Comment on lines +299 to +308

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 1

Suggested change
/**
* Get FapiConfigMgtService.
*
* @return FapiConfigMgtService.
*/
public static FapiConfigMgtService getFapiConfigMgtService() {
return FapiConfigMgtServiceHolder.SERVICE;
}
/**
* Get FapiConfigMgtService.
*
* @return FapiConfigMgtService.
*/
public static FapiConfigMgtService getFapiConfigMgtService() {
if (log.isDebugEnabled()) {
log.debug("Retrieving FapiConfigMgtService from OSGi context.");
}
return FapiConfigMgtServiceHolder.SERVICE;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com).
* Copyright (c) 2023-2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand Down Expand Up @@ -33,11 +33,30 @@
import javax.xml.bind.annotation.*;

public class FapiMetadata {


private MetadataProperty allowedFapiProfiles;
private MetadataProperty allowedSignatureAlgorithms;
private MetadataProperty allowedEncryptionAlgorithms;
private ClientAuthenticationMethodMetadata tokenEndpointAuthMethod;

/**
**/
public FapiMetadata allowedFapiProfiles(MetadataProperty allowedFapiProfiles) {

this.allowedFapiProfiles = allowedFapiProfiles;
return this;
}

@ApiModelProperty(value = "")
@JsonProperty("allowedFapiProfiles")
@Valid
public MetadataProperty getAllowedFapiProfiles() {
return allowedFapiProfiles;
}
public void setAllowedFapiProfiles(MetadataProperty allowedFapiProfiles) {
this.allowedFapiProfiles = allowedFapiProfiles;
}

/**
**/
public FapiMetadata allowedSignatureAlgorithms(MetadataProperty allowedSignatureAlgorithms) {
Expand Down Expand Up @@ -104,22 +123,24 @@ public boolean equals(java.lang.Object o) {
return false;
}
FapiMetadata fapiMetadata = (FapiMetadata) o;
return Objects.equals(this.allowedSignatureAlgorithms, fapiMetadata.allowedSignatureAlgorithms) &&
return Objects.equals(this.allowedFapiProfiles, fapiMetadata.allowedFapiProfiles) &&
Objects.equals(this.allowedSignatureAlgorithms, fapiMetadata.allowedSignatureAlgorithms) &&
Objects.equals(this.allowedEncryptionAlgorithms, fapiMetadata.allowedEncryptionAlgorithms) &&
Objects.equals(this.tokenEndpointAuthMethod, fapiMetadata.tokenEndpointAuthMethod);
}

@Override
public int hashCode() {
return Objects.hash(allowedSignatureAlgorithms, allowedEncryptionAlgorithms, tokenEndpointAuthMethod);
return Objects.hash(allowedFapiProfiles, allowedSignatureAlgorithms, allowedEncryptionAlgorithms, tokenEndpointAuthMethod);
}

@Override
public String toString() {

StringBuilder sb = new StringBuilder();
sb.append("class FapiMetadata {\n");


sb.append(" allowedFapiProfiles: ").append(toIndentedString(allowedFapiProfiles)).append("\n");
sb.append(" allowedSignatureAlgorithms: ").append(toIndentedString(allowedSignatureAlgorithms)).append("\n");
sb.append(" allowedEncryptionAlgorithms: ").append(toIndentedString(allowedEncryptionAlgorithms)).append("\n");
sb.append(" tokenEndpointAuthMethod: ").append(toIndentedString(tokenEndpointAuthMethod)).append("\n");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.carbon.identity.api.server.application.management.v1;

import io.swagger.annotations.ApiModel;
import javax.validation.constraints.*;

/**
* FAPI security profile. - `FAPI1_ADVANCED`: FAPI 1.0 Advanced Security Profile. - `FAPI2_SECURITY`: FAPI 2.0 Security Profile.
**/
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlEnumValue;

@XmlType(name="")
@XmlEnum(String.class)
public enum FapiProfile {

@XmlEnumValue("FAPI1_ADVANCED") FAPI1_ADVANCED(String.valueOf("FAPI1_ADVANCED")), @XmlEnumValue("FAPI2_SECURITY") FAPI2_SECURITY(String.valueOf("FAPI2_SECURITY"));


private String value;

FapiProfile(String v) {
value = v;
}

public String value() {
return value;
}

@Override
public String toString() {
return String.valueOf(value);
}

public static FapiProfile fromValue(String value) {
for (FapiProfile b : FapiProfile.values()) {
if (b.value.equals(value)) {
return b;
}
}
throw new IllegalArgumentException("Unexpected value '" + value + "'");
}
}



Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public static StateEnum fromValue(String value) {
private SubjectConfiguration subject;
private Boolean isFAPIApplication = false;
private FapiMetadata fapiMetadata;
private FapiProfile fapiProfile = null;
private CIBAAuthenticationRequestConfiguration cibaAuthenticationRequest;
private AllowedIssuer issuer;

Expand Down Expand Up @@ -538,6 +539,25 @@ public void setFapiMetadata(FapiMetadata fapiMetadata) {
this.fapiMetadata = fapiMetadata;
}

/**
* The FAPI security profile applied to the application. Applicable only when isFAPIApplication is true. If isFAPIApplication is true and this property is omitted, `FAPI1_ADVANCED` will be applied by default.
**/
public OpenIDConnectConfiguration fapiProfile(FapiProfile fapiProfile) {

this.fapiProfile = fapiProfile;
return this;
}

@ApiModelProperty(example = "FAPI1_ADVANCED", value = "The FAPI security profile applied to the application. Applicable only when isFAPIApplication is true. If isFAPIApplication is true and this property is omitted, `FAPI1_ADVANCED` will be applied by default. ")
@JsonProperty("fapiProfile")
@Valid
public FapiProfile getFapiProfile() {
return fapiProfile;
}
public void setFapiProfile(FapiProfile fapiProfile) {
this.fapiProfile = fapiProfile;
}

/**
**/
public OpenIDConnectConfiguration cibaAuthenticationRequest(CIBAAuthenticationRequestConfiguration cibaAuthenticationRequest) {
Expand Down Expand Up @@ -608,6 +628,7 @@ public boolean equals(java.lang.Object o) {
Objects.equals(this.subject, openIDConnectConfiguration.subject) &&
Objects.equals(this.isFAPIApplication, openIDConnectConfiguration.isFAPIApplication) &&
Objects.equals(this.fapiMetadata, openIDConnectConfiguration.fapiMetadata) &&
Objects.equals(this.fapiProfile, openIDConnectConfiguration.fapiProfile) &&
Objects.equals(this.cibaAuthenticationRequest, openIDConnectConfiguration.cibaAuthenticationRequest) &&
Objects.equals(this.issuer, openIDConnectConfiguration.issuer);
}
Expand Down Expand Up @@ -646,6 +667,7 @@ public String toString() {
sb.append(" subject: ").append(toIndentedString(subject)).append("\n");
sb.append(" isFAPIApplication: ").append(toIndentedString(isFAPIApplication)).append("\n");
sb.append(" fapiMetadata: ").append(toIndentedString(fapiMetadata)).append("\n");
sb.append(" fapiProfile: ").append(toIndentedString(fapiProfile)).append("\n");
sb.append(" cibaAuthenticationRequest: ").append(toIndentedString(cibaAuthenticationRequest)).append("\n");
sb.append(" issuer: ").append(toIndentedString(issuer)).append("\n");
sb.append("}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@
import org.wso2.carbon.identity.cors.mgt.core.exception.CORSManagementServiceException;
import org.wso2.carbon.identity.cors.mgt.core.model.CORSOrigin;
import org.wso2.carbon.identity.oauth.dao.OAuthAppDO;
import org.wso2.carbon.identity.oauth2.fapi.exceptions.FapiConfigMgtException;
import org.wso2.carbon.identity.oauth2.fapi.models.FapiProfileEnum;
import org.wso2.carbon.identity.oauth2.fapi.services.FapiConfigMgtService;
import org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants;
import org.wso2.carbon.identity.sso.saml.dto.SAMLSSOServiceProviderDTO;
import org.wso2.carbon.identity.template.mgt.TemplateManager;
Expand Down Expand Up @@ -190,6 +193,7 @@
import static org.wso2.carbon.identity.api.server.application.management.common.ApplicationManagementConstants.ErrorMessage.FORBIDDEN_OPERATION;
import static org.wso2.carbon.identity.api.server.application.management.common.ApplicationManagementConstants.ErrorMessage.INBOUND_NOT_CONFIGURED;
import static org.wso2.carbon.identity.api.server.application.management.common.ApplicationManagementConstants.ErrorMessage.UNSUPPORTED_ENHANCED_ORGANIZATION_AUTHENTICATION_ENABLED_CONFIGURATION;
import static org.wso2.carbon.identity.api.server.application.management.common.ApplicationManagementConstants.ErrorMessage.UNSUPPORTED_FAPI_PROFILE;
import static org.wso2.carbon.identity.api.server.application.management.common.ApplicationManagementConstants.ErrorMessage.USE_EXTERNAL_CONSENT_PAGE_NOT_SUPPORTED;
import static org.wso2.carbon.identity.api.server.application.management.common.ApplicationManagementConstants.ISSUER;
import static org.wso2.carbon.identity.api.server.application.management.common.ApplicationManagementConstants.NAME;
Expand Down Expand Up @@ -815,7 +819,10 @@ public String createApplication(ApplicationModel applicationModel, String templa
*/
if (applicationModel.getInboundProtocolConfiguration() != null &&
applicationModel.getInboundProtocolConfiguration().getOidc() != null) {
validateCORSOrigins(applicationModel.getInboundProtocolConfiguration().getOidc().getAllowedOrigins());
OpenIDConnectConfiguration oidcConfig =
applicationModel.getInboundProtocolConfiguration().getOidc();
Comment on lines 820 to +823

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 2

Suggested change
if (applicationModel.getInboundProtocolConfiguration() != null &&
applicationModel.getInboundProtocolConfiguration().getOidc() != null) {
validateCORSOrigins(applicationModel.getInboundProtocolConfiguration().getOidc().getAllowedOrigins());
OpenIDConnectConfiguration oidcConfig =
applicationModel.getInboundProtocolConfiguration().getOidc();
if (applicationModel.getInboundProtocolConfiguration() != null &&
applicationModel.getInboundProtocolConfiguration().getOidc() != null) {
log.info("Validating OIDC configuration for new application");
OpenIDConnectConfiguration oidcConfig =

validateCORSOrigins(oidcConfig.getAllowedOrigins());
validateFapiProfile(oidcConfig);
}

/*
Expand Down Expand Up @@ -1235,6 +1242,7 @@ public void putInboundOAuthConfiguration(String applicationId, OpenIDConnectConf
* But for now we are handling the validation here.
*/
validateCORSOrigins(oidcConfigModel.getAllowedOrigins());
validateFapiProfile(oidcConfigModel);
putApplicationInbound(applicationId, oidcConfigModel, OAuthInboundFunctions::getInboundProtocolConfig);
}

Expand Down Expand Up @@ -2219,6 +2227,64 @@ private String buildFormattedDescription(String description, String... formatDat
}
}

/**
* Validates the FAPI profile in the given OIDC configuration.
*
* <p>Validation: checks the resolved profile against the organisation's allowed list from
* {@link FapiConfigMgtService}. Throws a 400 error if the profile is not supported.
* No-ops when {@code oidcConfig} is null, FAPI is disabled, or {@link FapiConfigMgtService}
* is unavailable (treated as optional OSGi service).
*
* @param oidcConfig the OIDC configuration from the incoming request; may be null.
*/
private void validateFapiProfile(OpenIDConnectConfiguration oidcConfig) {

// Only validate when the application is marked as FAPI and a specific profile is provided.
// If isFAPIApplication is false the profile field is ignored in storage, so validation would
// produce a misleading error for a field that has no effect.
if (oidcConfig == null || !Boolean.TRUE.equals(oidcConfig.getIsFAPIApplication())
|| oidcConfig.getFapiProfile() == null) {
return;
}

/*
* FapiConfigMgtService is treated as optional here. Profile validation is an enhancement —
* silently skipping it avoids blocking all OIDC application updates in deployments where
* the FAPI bundle is not present. The configs API module (/configs/fapi) hard-requires
* the service at startup and fails fast if it is absent.
*/
FapiConfigMgtService fapiConfigMgtService = ApplicationManagementServiceHolder.getFapiConfigMgtService();
if (fapiConfigMgtService == null) {
log.debug("FapiConfigMgtService is unavailable. Skipping fapiProfile validation.");
return;
}

String tenantDomain = ContextLoader.getTenantDomainFromContext();
try {
List<FapiProfileEnum> supportedProfiles =
fapiConfigMgtService.getFapiConfig(tenantDomain).getSupportedProfiles();
String requestedProfileValue = oidcConfig.getFapiProfile().toString();

boolean isSupported = supportedProfiles.stream()
.anyMatch(p -> p.value().equals(requestedProfileValue));
Comment on lines +2245 to +2269

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the effective default FAPI profile.

When isFAPIApplication is true and fapiProfile is omitted, downstream mapping applies FAPI1_ADVANCED; returning at Line 2246 skips tenant support validation for that effective value. Resolve the requested profile first and validate the default too. Based on PR objectives, omitted FAPI profiles default to FAPI1_ADVANCED.

Proposed fix
-        // Only validate when the application is marked as FAPI and a specific profile is provided.
-        // If isFAPIApplication is false the profile field is ignored in storage, so validation would
-        // produce a misleading error for a field that has no effect.
-        if (oidcConfig == null || !Boolean.TRUE.equals(oidcConfig.getIsFAPIApplication())
-                || oidcConfig.getFapiProfile() == null) {
+        // Only validate when the application is marked as FAPI. If the profile is omitted,
+        // validate the default profile that is applied downstream.
+        if (oidcConfig == null || !Boolean.TRUE.equals(oidcConfig.getIsFAPIApplication())) {
             return;
         }
 
@@
-            String requestedProfileValue = oidcConfig.getFapiProfile().toString();
+            String requestedProfileValue = oidcConfig.getFapiProfile() != null
+                    ? oidcConfig.getFapiProfile().toString()
+                    : FapiProfileEnum.FAPI1_ADVANCED.value();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@components/org.wso2.carbon.identity.api.server.application.management/org.wso2.carbon.identity.api.server.application.management.v1/src/main/java/org/wso2/carbon/identity/api/server/application/management/v1/core/ServerApplicationManagementService.java`
around lines 2245 - 2269, The validateFapiProfile method currently returns early
when fapiProfile is null without validating the default profile that will be
applied downstream. Modify the early return condition to remove the check for
null fapiProfile. After the fapiConfigMgtService null check, determine the
effective requested profile by using the fapiProfile value if provided, or
defaulting to FAPI1_ADVANCED if omitted. Then validate this resolved profile
value against the supported profiles retrieved from fapiConfigMgtService,
ensuring both explicitly set and defaulted FAPI profiles are properly validated.


Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!isSupported) {
String supportedProfilesStr = supportedProfiles.stream()
.map(FapiProfileEnum::value)
.collect(Collectors.joining(", "));
if (log.isDebugEnabled()) {
log.debug(String.format("Rejected fapiProfile '%s' for tenant '%s'. Supported: [%s]",
requestedProfileValue, tenantDomain, supportedProfilesStr));
Comment on lines +2274 to +2277

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 3

Suggested change
.collect(Collectors.joining(", "));
if (log.isDebugEnabled()) {
log.debug(String.format("Rejected fapiProfile '%s' for tenant '%s'. Supported: [%s]",
requestedProfileValue, tenantDomain, supportedProfilesStr));
if (log.isDebugEnabled()) {
log.debug(String.format("Rejected fapiProfile '%s' for tenant '%s'. Supported: [%s]",
requestedProfileValue, tenantDomain, supportedProfilesStr));
}
log.warn(String.format("FAPI profile validation failed. Requested: '%s', Supported: [%s]",
requestedProfileValue, supportedProfilesStr));

}
throw buildClientError(UNSUPPORTED_FAPI_PROFILE, requestedProfileValue, supportedProfilesStr);
}
} catch (FapiConfigMgtException e) {
throw Utils.buildServerError(
String.format("Error while retrieving FAPI configuration for tenant '%s' during fapiProfile " +
"validation.", tenantDomain), e);
Comment on lines +2281 to +2284

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the new FAPI metadata error code here.

Line 2281 currently builds a generic server error, so this retrieval-failure path does not return the new ERROR_RETRIEVING_FAPI_METADATA code. Use the enum to keep API error responses consistent.

Proposed fix
         } catch (FapiConfigMgtException e) {
-            throw Utils.buildServerError(
-                    String.format("Error while retrieving FAPI configuration for tenant '%s' during fapiProfile " +
-                            "validation.", tenantDomain), e);
+            ErrorMessage errorEnum = ErrorMessage.ERROR_RETRIEVING_FAPI_METADATA;
+            throw Utils.buildServerError(errorEnum.getCode(), errorEnum.getMessage(),
+                    buildFormattedDescription(errorEnum.getDescription(), tenantDomain), e);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@components/org.wso2.carbon.identity.api.server.application.management/org.wso2.carbon.identity.api.server.application.management.v1/src/main/java/org/wso2/carbon/identity/api/server/application/management/v1/core/ServerApplicationManagementService.java`
around lines 2281 - 2284, The catch block for FapiConfigMgtException at the
specified location currently uses Utils.buildServerError with a generic error
message. Replace this with an error-building call that uses the
ERROR_RETRIEVING_FAPI_METADATA enum constant to ensure consistent and specific
API error responses. This will align the FAPI configuration retrieval failure
with the new error code instead of returning a generic server error.

}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Validate the CORS Origins. This should be moved to the service layer with the CORS adding step on creating
* OIDC applications.
Expand Down
Loading
Loading