This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch knox_idf
in repository https://gitbox.apache.org/repos/asf/knox.git

commit 7802c3c65cf88cd0ac97c8bf96770b62e6e10e17
Author: Sandor Molnar <[email protected]>
AuthorDate: Mon Aug 10 21:55:15 2026 +0200

    KNOX-3414: Tier 3 hardening + Tier 4 polish for the OIDC provider
    
    Tier 3 (hardening):
    - Registration redirect_uris require HTTPS (plain HTTP only for loopback, 
RFC 8252)
    - XSS: escape unknown scopes in AuthConsentServlet; rebuild knoxauth.js OP 
links
      via the DOM API instead of raw HTML / inline onclick interpolation
    - URL-encode clientId and federated-OP redirect params in AuthorizeResource 
/ KnoxIDFUtils
    - Single-use consent/federation state: add KnoxIDFArtifactStore.remove() 
and invalidate
      state after use in authCallback and consentAccepted
    - Reject disabled refresh tokens (isEnabled check) in TokenResource
    - Remove hardcoded LDAP "admin-password" fallback; fail fast without a 
configured alias
    - DiscoveryResource: literal String.replace instead of regex replaceAll for 
topology name
    - error() maps OAuth error codes to correct HTTP status per RFC 6749 5.2 
(no longer always 401)
    - RedirectToUrlFilter: treat a blank fedOpSid the same as absent
    
    Tier 4 (polish):
    - Fix public-API typos: EmptyFederatedIdentityService, BASE_RESOURCE_PATH
    - Make ALLOWED_RESPONSE_TYPES / DEFAULT_SCOPES immutable; copy at mutating 
call sites
    - Drop nonce from the UserInfo response (id_token only)
    - Document the KnoxIDFArtifactStore ttl*2 grace window
    
    Tests: KnoxIDFUtilsErrorStatusTest, RegistrationRedirectUriPolicyTest, 
KnoxIDFArtifactStoreTest
    (+18); TokenResourceAuthCodeReplayTest updated 401->400. All affected 
modules green.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../applications/knoxauth/app/js/knoxauth.js       | 14 ++--
 .../knox/gateway/filter/RedirectToUrlFilter.java   |  5 +-
 .../factory/FederatedIdentityServiceFactory.java   | 10 +--
 ...ice.java => EmptyFederatedIdentityService.java} |  2 +-
 .../service/knoxidf/AuthConsentServlet.java        |  6 +-
 .../gateway/service/knoxidf/AuthorizeResource.java | 20 ++++--
 .../gateway/service/knoxidf/DiscoveryResource.java | 10 +--
 .../knox/gateway/service/knoxidf/JwksResource.java |  4 +-
 .../service/knoxidf/RegistrationResource.java      | 33 +++++++--
 .../gateway/service/knoxidf/TokenResource.java     | 14 ++--
 .../gateway/service/knoxidf/UserInfoResource.java  | 11 ++-
 .../knoxidf/userparams/LdapUserParamsProvider.java | 11 ++-
 .../knoxidf/KnoxIDFUtilsErrorStatusTest.java       | 83 ++++++++++++++++++++++
 .../knoxidf/RegistrationRedirectUriPolicyTest.java | 76 ++++++++++++++++++++
 .../knoxidf/TokenResourceAuthCodeReplayTest.java   |  2 +-
 .../gateway/util/knoxidf/KnoxIDFArtifactStore.java |  9 +++
 .../gateway/util/knoxidf/KnoxIDFConstants.java     | 11 +--
 .../knox/gateway/util/knoxidf/KnoxIDFUtils.java    | 53 ++++++++++++--
 .../util/knoxidf/KnoxIDFArtifactStoreTest.java     | 63 ++++++++++++++++
 19 files changed, 381 insertions(+), 56 deletions(-)

diff --git 
a/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js
 
b/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js
index 5a304a556..329ad930c 100644
--- 
a/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js
+++ 
b/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js
@@ -74,12 +74,14 @@ var loadFederatedOpLinks = function() {
        }
 
        ops.forEach(op => {
-               container.append(`
-            <div class="fed-op-btn" onclick="loginWithOp('${op}')">
-                <span class="fed-op-icon">๐ŸŒ</span>
-                <span class="fed-op-label">Continue with ${op}</span>
-            </div>
-        `);
+               // Build the element via the DOM API and bind the handler in JS 
rather than interpolating
+               // the (attacker-controllable) op name into an HTML string / 
inline onclick attribute.
+               // .text() escapes the label; the click closure captures op 
without string injection.
+               const btn = $('<div class="fed-op-btn"></div>');
+               $('<span class="fed-op-icon"></span>').text("๐ŸŒ").appendTo(btn);
+               $('<span class="fed-op-label"></span>').text("Continue with " + 
op).appendTo(btn);
+               btn.on("click", function() { loginWithOp(op); });
+               container.append(btn);
        });
 };
 
diff --git 
a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java
 
b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java
index 7501dbe4b..bffc5049a 100644
--- 
a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java
+++ 
b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java
@@ -27,6 +27,7 @@ import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.knox.gateway.config.GatewayConfig;
 
 public class RedirectToUrlFilter extends AbstractGatewayFilter {
@@ -47,7 +48,9 @@ public class RedirectToUrlFilter extends 
AbstractGatewayFilter {
 
   @Override
   protected void doFilter(HttpServletRequest request, HttpServletResponse 
response, FilterChain chain) throws IOException, ServletException {
-    if (redirectUrl != null && request.getHeader("Authorization") == null && 
request.getParameter("fedOpSid") == null) {
+    // Treat a blank fedOpSid the same as absent: a bare "?fedOpSid=" must not 
suppress the redirect.
+    // (Downstream authentication still runs; this only prevents an empty 
param from skipping it.)
+    if (redirectUrl != null && request.getHeader("Authorization") == null && 
StringUtils.isBlank(request.getParameter("fedOpSid"))) {
       response.sendRedirect(redirectUrl + getOriginalQueryString(request));
     }
     chain.doFilter(request, response);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
index f78f96af6..7ffe009fa 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
@@ -23,7 +23,7 @@ import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.Service;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.ServiceType;
-import 
org.apache.knox.gateway.services.knoxidf.federation.EmptyFederatedIdentitityService;
+import 
org.apache.knox.gateway.services.knoxidf.federation.EmptyFederatedIdentityService;
 import 
org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService;
 import 
org.apache.knox.gateway.services.knoxidf.federation.JdbcFederatedIdentityService;
 import org.apache.knox.gateway.services.topology.TopologyService;
@@ -36,7 +36,7 @@ import java.util.Map;
 public class FederatedIdentityServiceFactory extends AbstractServiceFactory {
 
     private static final GatewayMessages LOG = 
MessagesFactory.get(GatewayMessages.class);
-    private static final String DEFAULT_IMPLEMENTATION = 
EmptyFederatedIdentitityService.class.getName();
+    private static final String DEFAULT_IMPLEMENTATION = 
EmptyFederatedIdentityService.class.getName();
 
     @Override
     protected Service createService(GatewayServices gatewayServices, 
ServiceType serviceType, GatewayConfig gatewayConfig, Map<String, String> 
options, String implementation)
@@ -52,8 +52,8 @@ public class FederatedIdentityServiceFactory extends 
AbstractServiceFactory {
 
         FederatedIdentityService service = null;
         if (shouldCreateService(implementationToUse)) {
-            if (matchesImplementation(implementationToUse, 
EmptyFederatedIdentitityService.class, true)) {
-                service = new EmptyFederatedIdentitityService();
+            if (matchesImplementation(implementationToUse, 
EmptyFederatedIdentityService.class, true)) {
+                service = new EmptyFederatedIdentityService();
             } else if (matchesImplementation(implementationToUse, 
JdbcFederatedIdentityService.class)) {
                 try {
                     try {
@@ -62,7 +62,7 @@ public class FederatedIdentityServiceFactory extends 
AbstractServiceFactory {
                         service.init(gatewayConfig, options);
                     } catch (ServiceLifecycleException e) {
                         LOG.errorInitializingService(implementationToUse, 
e.getMessage(), e);
-                        service =  new EmptyFederatedIdentitityService();
+                        service =  new EmptyFederatedIdentityService();
                     }
                 } catch (Exception e) {
                     throw new ServiceLifecycleException("Error while creating 
Federated Identity Service: " + e, e);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentityService.java
similarity index 95%
rename from 
gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java
rename to 
gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentityService.java
index 8ce9e550e..d73cd0c8d 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentityService.java
@@ -22,7 +22,7 @@ import 
org.apache.knox.gateway.services.ServiceLifecycleException;
 import java.util.Map;
 import java.util.Optional;
 
-public class EmptyFederatedIdentitityService implements 
FederatedIdentityService {
+public class EmptyFederatedIdentityService implements FederatedIdentityService 
{
     @Override
     public void addFederatedIdentity(FederatedIdentity identity) {
     }
diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java
index 6200edc83..211c9fec6 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java
@@ -16,6 +16,8 @@
  */
 package org.apache.knox.gateway.service.knoxidf;
 
+import org.apache.commons.text.StringEscapeUtils;
+
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -114,7 +116,9 @@ public class AuthConsentServlet extends HttpServlet {
             case "calendar.write":
                 return "Modify your calendar events";
             default:
-                return scope;
+                // Unknown scopes are echoed into the HTML consent page. 
Escape them so an
+                // attacker-influenced scope value cannot inject markup 
(defense in depth).
+                return StringEscapeUtils.escapeHtml4(scope);
         }
     }
 
diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
index d778aa7e1..b27c7f9d8 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
@@ -87,7 +87,7 @@ import java.util.stream.Collectors;
 import static 
org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_SECRET;
 import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE;
 import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.ALLOWED_SCOPES;
-import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESOURCE_PATH;
 import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CLIENT_ID;
 import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE;
 import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_CHALLENGE;
@@ -107,7 +107,7 @@ import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
 
 @Path(AuthorizeResource.RESOURCE_PATH)
 public class AuthorizeResource extends PasscodeTokenResourceBase {
-    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/authorize";
+    static final String RESOURCE_PATH = BASE_RESOURCE_PATH + "/authorize";
     private static final UUID KNOX_NAMESPACE = 
UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
     private static final NameBasedGenerator UUID_V5 = 
Generators.nameBasedGenerator(KNOX_NAMESPACE);
     public static final Set<String> ALLOWED_CLAIMS = 
Set.of("preferred_username", "email", "email_verified",
@@ -166,7 +166,8 @@ public class AuthorizeResource extends 
PasscodeTokenResourceBase {
                                String codeChallenge,
                                String codeChallengeMethod) {
         final String subject = SubjectUtils.getCurrentEffectivePrincipalName();
-        final Set<String> requestedScopes = StringUtils.isBlank(scope) ? 
DEFAULT_SCOPES : new HashSet<>(Arrays.asList(scope.split("\\s+")));
+        // DEFAULT_SCOPES is an ImmutableSet; copy it into a mutable set so 
downstream mutation is safe.
+        final Set<String> requestedScopes = StringUtils.isBlank(scope) ? new 
HashSet<>(DEFAULT_SCOPES) : new HashSet<>(Arrays.asList(scope.split("\\s+")));
         final AuthorizeRequestMetadata authorizeRequestMetadata = new 
AuthorizeRequestMetadata(clientId, subject, responseType, redirectUri, 
requestedScopes, state, nonce, codeChallenge, codeChallengeMethod);
         final Response verificationErrorResponse = 
verifyParams(authorizeRequestMetadata);
         if (verificationErrorResponse != null) {
@@ -180,8 +181,11 @@ public class AuthorizeResource extends 
PasscodeTokenResourceBase {
                 final String consentAuthState = UUID.randomUUID().toString();
                 authorizeRequestMetadataStore.put(consentAuthState, 
authorizeRequestMetadata);
                 final String baseUri = servletContext.getContextPath() + 
"/authConsent";
+                // Every value placed into the consent redirect's query string 
must be percent-encoded;
+                // a client_id containing '&', '=' or '#' would otherwise 
split or corrupt the URL.
+                final String clientIdParam = URLEncoder.encode(clientId, 
StandardCharsets.UTF_8);
                 final String scopeParam = 
URLEncoder.encode(authorizeRequestMetadata.getJoinedRequestedScopes(), 
StandardCharsets.UTF_8);
-                final String redirect = String.format(Locale.US, 
"%s?client_id=%s&state=%s&scope=%s", baseUri, clientId, consentAuthState, 
scopeParam);
+                final String redirect = String.format(Locale.US, 
"%s?client_id=%s&state=%s&scope=%s", baseUri, clientIdParam, consentAuthState, 
scopeParam);
                 return 
Response.seeOther(java.net.URI.create(redirect)).build();
             }
         }
@@ -277,6 +281,10 @@ public class AuthorizeResource extends 
PasscodeTokenResourceBase {
         if (federatedOpConfiguration == null) {
             return error("invalid_request", "No federated OP configuration 
available for the request");
         }
+        // The federated callback state is single-use: invalidate it in both 
stores now that it has
+        // been validated and captured, so a replayed callback with the same 
state is rejected.
+        authorizeRequestMetadataStore.remove(state);
+        federatedOpConfigurationStore.remove(state);
         final Pair<String, String> federatedTokens = 
exchangeFederatedAuthCodeToTokens(federatedAuthCode, federatedOpConfiguration);
         if (StringUtils.isBlank(federatedTokens.getLeft())) {
             return error("invalid_request", "Federated OP did not return an 
id_token");
@@ -297,8 +305,10 @@ public class AuthorizeResource extends 
PasscodeTokenResourceBase {
         final String state = request.getParameter(STATE);
         final AuthorizeRequestMetadata authorizeRequestMetadata = 
authorizeRequestMetadataStore.get(state);
         if (authorizeRequestMetadata == null) {
-            return error("Consent cannot be accepted", "Invalid state");
+            return error("invalid_request", "Invalid state");
         }
+        // Single-use consent state: invalidate it so the accepted-consent 
redirect cannot be replayed.
+        authorizeRequestMetadataStore.remove(state);
         markConsentAccepted(authorizeRequestMetadata);
         return authorize(authorizeRequestMetadata.getResponseType(),
                 authorizeRequestMetadata.getClientId(),
diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java
index ce895549d..eb6584f3e 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java
@@ -32,10 +32,10 @@ import javax.ws.rs.core.UriInfo;
 import java.util.HashMap;
 import java.util.Map;
 
-import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESOURCE_PATH;
 import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.TOKEN_EXCHANGE_TOPOLOGY_NAME;
 
-@Path(BASE_RESORCE_PATH + "/.well-known/openid-configuration")
+@Path(BASE_RESOURCE_PATH + "/.well-known/openid-configuration")
 @Produces(MediaType.APPLICATION_JSON)
 public class DiscoveryResource {
     private String currentTopologyName;
@@ -59,8 +59,10 @@ public class DiscoveryResource {
         String tokenEndpoint = baseUrl + TokenResource.RESOURCE_PATH;
         String userInfoEndpoint = baseUrl + UserInfoResource.RESOURCE_PATH;
         if (tokenExchangeTopologyName != null) {
-            tokenEndpoint = tokenEndpoint.replaceAll(currentTopologyName, 
tokenExchangeTopologyName);
-            userInfoEndpoint = 
userInfoEndpoint.replaceAll(currentTopologyName, tokenExchangeTopologyName);
+            // Literal substitution: the topology name is data, not a regex. 
replaceAll would treat
+            // any regex metacharacter in the topology name as a pattern.
+            tokenEndpoint = tokenEndpoint.replace(currentTopologyName, 
tokenExchangeTopologyName);
+            userInfoEndpoint = userInfoEndpoint.replace(currentTopologyName, 
tokenExchangeTopologyName);
         }
         config.put("token_endpoint", tokenEndpoint);
         config.put("userinfo_endpoint", userInfoEndpoint);
diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java
index 47f802b04..1dbca3c8e 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java
@@ -24,12 +24,12 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
-import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESOURCE_PATH;
 
 @Path(JwksResource.RESOURCE_PATH)
 @Produces(MediaType.APPLICATION_JSON)
 public class JwksResource extends JWKSResource {
-    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/jwks";
+    static final String RESOURCE_PATH = BASE_RESOURCE_PATH + "/jwks";
 
     @GET
     public Response getKeys() {
diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java
index ed3ad7411..bd7dedff2 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java
@@ -43,7 +43,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
-import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESOURCE_PATH;
 import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CLIENT_REGISTRATION_ANONYMOUS_ALLOWED;
 import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES;
 import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
@@ -52,7 +52,7 @@ import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
 @RequestScoped //this is important because redirectUris/allowedScopes are part 
of the state of this class
 public class RegistrationResource extends ClientCredentialsResource {
 
-    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/client";
+    static final String RESOURCE_PATH = BASE_RESOURCE_PATH + "/client";
     private static final String ANONYMOUS_PRINCIPAL = "anonymous";
 
     private List<String> redirectUris;
@@ -125,6 +125,12 @@ public class RegistrationResource extends 
ClientCredentialsResource {
     }
 
     private Response verifyRedirectUris() {
+        return verifyRedirectUris(redirectUris);
+    }
+
+    // Package-private and list-parameterized so the redirect-URI policy 
(https-only except loopback,
+    // no wildcard host, restricted path/query/fragment wildcards) is 
unit-testable in isolation.
+    static Response verifyRedirectUris(List<String> redirectUris) {
         if (redirectUris == null || redirectUris.isEmpty()) {
             return error("invalid_request", "redirect_uris must be provided");
         }
@@ -137,16 +143,20 @@ public class RegistrationResource extends 
ClientCredentialsResource {
                 return error("invalid_request", "Invalid redirect URI: " + 
uriStr);
             }
 
-            // Scheme check
-            if (!"https".equalsIgnoreCase(uri.getScheme()) && 
!"http".equalsIgnoreCase(uri.getScheme())) {
-                return error("invalid_request", "Redirect URI must use HTTPS 
or HTTP as scheme: " + uriStr);
-            }
-
             // Host check (no wildcard allowed)
             if (uri.getHost() == null || uri.getHost().contains("*")) {
                 return error("invalid_request", "Wildcard not allowed in host: 
" + uriStr);
             }
 
+            // Scheme check: require HTTPS per RFC 8252, allowing plain HTTP 
only for loopback
+            // (localhost / 127.0.0.1 / ::1) native-app dev. Any other http:// 
redirect is rejected.
+            final String scheme = uri.getScheme();
+            final boolean https = "https".equalsIgnoreCase(scheme);
+            final boolean loopbackHttp = "http".equalsIgnoreCase(scheme) && 
isLoopbackHost(uri.getHost());
+            if (!https && !loopbackHttp) {
+                return error("invalid_request", "Redirect URI must use HTTPS 
(plain HTTP allowed only for localhost): " + uriStr);
+            }
+
             // Path wildcard check
             String path = uri.getPath();
             if (path != null && path.contains("*") && !path.endsWith("*")) {
@@ -162,6 +172,15 @@ public class RegistrationResource extends 
ClientCredentialsResource {
         return null;
     }
 
+    private static boolean isLoopbackHost(String host) {
+        if (host == null) {
+            return false;
+        }
+        // Strip brackets from an IPv6 literal (e.g. [::1]).
+        final String h = host.startsWith("[") && host.endsWith("]") ? 
host.substring(1, host.length() - 1) : host;
+        return "localhost".equalsIgnoreCase(h) || "127.0.0.1".equals(h) || 
"::1".equals(h);
+    }
+
     @Override
     protected void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) {
         tokenMetadata.add("redirect_uris", getRedirectUris());
diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java
index 540df06f6..513b275c3 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java
@@ -61,7 +61,7 @@ import java.util.Map;
 import static 
org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_SECRET;
 import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE;
 import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.AUTH_CODE;
-import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESOURCE_PATH;
 import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CLIENT_ID;
 import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE;
 import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_CHALLENGE;
@@ -80,7 +80,7 @@ import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
 @Path(TokenResource.RESOURCE_PATH)
 @Produces(MediaType.APPLICATION_JSON)
 public class TokenResource extends PasscodeTokenResourceBase {
-    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/token";
+    static final String RESOURCE_PATH = BASE_RESOURCE_PATH + "/token";
 
     // Per-request stash for the auth-code TokenMetadata read during 
validation. The code is
     // atomically consumed (deleted) BEFORE token issuance to close the replay 
window, so the
@@ -228,7 +228,7 @@ public class TokenResource extends 
PasscodeTokenResourceBase {
         } catch (UnknownTokenException e) {
             return error("invalid_grant", "Unknown refresh_token");
         } catch (RefreshTokenValidationError e) {
-            return error("Refresh token validation error", e.getMessage());
+            return error("invalid_grant", e.getMessage());
         }
 
     }
@@ -248,6 +248,12 @@ public class TokenResource extends 
PasscodeTokenResourceBase {
             throw new RefreshTokenValidationError("Invalid grant: invalid 
refresh_token");
         }
 
+        // A refresh token that has been administratively disabled (revoked) 
must not mint new tokens,
+        // even if it has not yet expired.
+        if (!refreshTokenMetadata.isEnabled()) {
+            throw new RefreshTokenValidationError("Invalid grant: 
refresh_token disabled");
+        }
+
         if (tokenStateService.getTokenExpiration(refreshTokenId) <= 
System.currentTimeMillis()) {
             throw new RefreshTokenValidationError("Invalid grant: Refresh 
token expired");
         }
@@ -268,7 +274,7 @@ public class TokenResource extends 
PasscodeTokenResourceBase {
         try {
             authCodeMetadata = validateAuthCode(code, redirectUri);
         } catch (AuthTokenValidationError e) {
-            return error("Auth code validation error", e.getMessage());
+            return error("invalid_grant", e.getMessage());
         }
 
         // Enforce single-use: atomically consume the code BEFORE issuing any 
token. Of N concurrent
diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java
index c837605ae..b4fccedd0 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java
@@ -43,7 +43,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.stream.Collectors;
 
-import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESOURCE_PATH;
 import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.SCOPE_ATTRIBUTE;
 import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.TOKEN_ID_ATTRIBUTE;
 import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
@@ -53,7 +53,7 @@ import static 
org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
 @Produces(MediaType.APPLICATION_JSON)
 public class UserInfoResource {
 
-    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/userinfo";
+    static final String RESOURCE_PATH = BASE_RESOURCE_PATH + "/userinfo";
     private UserParamsProvider userParamsProvider;
 
     @Context
@@ -117,11 +117,8 @@ public class UserInfoResource {
             claims.put("federated_sub", 
federatedIdentity.getExternalSubject());
             claims.put("federated_iss", federatedIdentity.getExternalIssuer());
 
-            // Add nonce if available
-            String nonce = tokenMetadata.getMetadata("nonce");
-            if (StringUtils.isNotBlank(nonce)) {
-                claims.put("nonce", nonce);
-            }
+            // Note: nonce is deliberately NOT returned here. Per OIDC it 
belongs in the id_token
+            // only; echoing it from the UserInfo endpoint is a spec violation 
and serves no purpose.
 
             userInfo.putAll(claims);
         } else {
diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java
index 080cc32de..10791d7be 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java
@@ -52,7 +52,6 @@ public class LdapUserParamsProvider implements 
UserParamsProvider {
     private static final String DEFAULT_BASE_DN = "dc=hadoop,dc=apache,dc=org";
     private static final String DEFAULT_USER_DN_TEMPLATE = "uid=%s,ou=people," 
+ DEFAULT_BASE_DN;
     private static final String DEFAULT_SYSTEM_USER = "uid=admin,ou=people," + 
DEFAULT_BASE_DN;
-    private static final String DEFAULT_SYSTEM_PASSWORD = "admin-password";
 
     private static final String[] ATTRIBUTES = {"cn", "sn", "givenName", 
"mail"};
 
@@ -80,9 +79,11 @@ public class LdapUserParamsProvider implements 
UserParamsProvider {
         final AliasService aliasService = 
services.getService(ServiceType.ALIAS_SERVICE);
         try {
             final char[] systemPassword = 
aliasService.getPasswordFromAliasForGateway(LDAP_SYSTEM_PASSWORD_ALIAS);
-            return systemPassword == null ? DEFAULT_SYSTEM_PASSWORD : new 
String(systemPassword);
+            // No hardcoded fallback: if the alias is absent/unresolvable, 
return null so the LDAP
+            // bind fails fast rather than silently binding with a well-known 
demo password.
+            return systemPassword == null ? null : new String(systemPassword);
         } catch (AliasServiceException e) {
-            return DEFAULT_SYSTEM_PASSWORD;
+            return null;
         }
     }
 
@@ -179,6 +180,10 @@ public class LdapUserParamsProvider implements 
UserParamsProvider {
     }
 
     private LdapContext createSystemContext() throws Exception {
+        if (ldapSystemPassword == null) {
+            throw new IllegalStateException("No LDAP system password 
configured. Set the '"
+                    + LDAP_SYSTEM_PASSWORD_ALIAS + "' alias; there is no 
default password.");
+        }
         Hashtable<String, Object> env = new Hashtable<>();
         env.put(Context.INITIAL_CONTEXT_FACTORY, 
"com.sun.jndi.ldap.LdapCtxFactory");
         env.put(Context.PROVIDER_URL, ldapUrl);
diff --git 
a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/KnoxIDFUtilsErrorStatusTest.java
 
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/KnoxIDFUtilsErrorStatusTest.java
new file mode 100644
index 000000000..d7cd7fb55
--- /dev/null
+++ 
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/KnoxIDFUtilsErrorStatusTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.apache.knox.gateway.service.knoxidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils;
+import org.junit.Test;
+
+/**
+ * Verifies {@link KnoxIDFUtils#error(String, String)} maps each OAuth 2.0 
error code to the HTTP
+ * status RFC 6749 ยง5.2 prescribes, rather than the previous always-401 
behavior.
+ */
+public class KnoxIDFUtilsErrorStatusTest {
+
+  private static int statusOf(String error) {
+    return KnoxIDFUtils.error(error, "desc").getStatus();
+  }
+
+  @Test
+  public void testInvalidClientIsUnauthorized() {
+    assertEquals(401, statusOf("invalid_client"));
+  }
+
+  @Test
+  public void testAccessDeniedIsForbidden() {
+    assertEquals(403, statusOf("access_denied"));
+  }
+
+  @Test
+  public void testServerErrorIs500() {
+    assertEquals(500, statusOf("server_error"));
+  }
+
+  @Test
+  public void testTemporarilyUnavailableIs503() {
+    assertEquals(503, statusOf("temporarily_unavailable"));
+  }
+
+  @Test
+  public void testProtocolErrorsDefaultToBadRequest() {
+    for (final String error : new String[]{"invalid_request", "invalid_grant", 
"invalid_scope",
+        "unsupported_grant_type", "unsupported_response_type", 
"unauthorized_client"}) {
+      assertEquals("Expected 400 for " + error, 400, statusOf(error));
+    }
+  }
+
+  @Test
+  public void testUnknownAndNullErrorDefaultToBadRequest() {
+    assertEquals(400, statusOf("something_unexpected"));
+    assertEquals(400, statusOf(null));
+  }
+
+  @Test
+  public void testExplicitStatusOverloadWins() {
+    final Response response = KnoxIDFUtils.error("invalid_request", "desc", 
Response.Status.CONFLICT);
+    assertEquals(409, response.getStatus());
+  }
+
+  @Test
+  public void testBodyCarriesErrorCodeAndDescription() {
+    final String body = String.valueOf(KnoxIDFUtils.error("invalid_grant", 
"bad code").getEntity());
+    assertTrue(body.contains("invalid_grant"));
+    assertTrue(body.contains("bad code"));
+  }
+}
diff --git 
a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/RegistrationRedirectUriPolicyTest.java
 
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/RegistrationRedirectUriPolicyTest.java
new file mode 100644
index 000000000..db91f336c
--- /dev/null
+++ 
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/RegistrationRedirectUriPolicyTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.apache.knox.gateway.service.knoxidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+
+import org.junit.Test;
+
+/**
+ * Verifies the dynamic-registration redirect-URI policy: HTTPS is required 
(RFC 8252), plain HTTP is
+ * tolerated only for loopback dev, and wildcard hosts are rejected.
+ */
+public class RegistrationRedirectUriPolicyTest {
+
+  private static Response verify(String... uris) {
+    return 
RegistrationResource.verifyRedirectUris(java.util.Arrays.asList(uris));
+  }
+
+  @Test
+  public void testHttpsAccepted() {
+    assertNull("A plain https redirect must be accepted.", 
verify("https://app.example.com/cb";));
+  }
+
+  @Test
+  public void testPlainHttpRejectedForNonLoopback() {
+    final Response response = verify("http://app.example.com/cb";);
+    assertEquals(400, response.getStatus());
+    assertTrue(String.valueOf(response.getEntity()).contains("HTTPS"));
+  }
+
+  @Test
+  public void testPlainHttpAllowedForLoopback() {
+    assertNull(verify("http://localhost:8080/cb";));
+    assertNull(verify("http://127.0.0.1/cb";));
+    assertNull(verify("http://[::1]:9000/cb";));
+  }
+
+  @Test
+  public void testWildcardHostRejected() {
+    final Response response = verify("https://*.example.com/cb";);
+    assertEquals(400, response.getStatus());
+  }
+
+  @Test
+  public void testEmptyListRejected() {
+    assertEquals(400, verify().getStatus());
+    assertEquals(400, 
RegistrationResource.verifyRedirectUris(Collections.<String>emptyList()).getStatus());
+    assertEquals(400, RegistrationResource.verifyRedirectUris((List<String>) 
null).getStatus());
+  }
+
+  @Test
+  public void testOneBadUriAmongGoodOnesRejectsWhole() {
+    assertEquals(400, verify("https://good.example.com/cb";, 
"http://evil.example.com/cb";).getStatus());
+  }
+}
diff --git 
a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/TokenResourceAuthCodeReplayTest.java
 
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/TokenResourceAuthCodeReplayTest.java
index ac0fb6cab..92e6632ed 100644
--- 
a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/TokenResourceAuthCodeReplayTest.java
+++ 
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/TokenResourceAuthCodeReplayTest.java
@@ -144,7 +144,7 @@ public class TokenResourceAuthCodeReplayTest {
     final Response response = resource.handleAuthorizationCodeFlow();
 
     assertEquals("A code already consumed by a concurrent redemption must be 
rejected.",
-        Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
     assertTrue("The error body should identify the invalid_grant condition.",
         String.valueOf(response.getEntity()).contains("invalid_grant"));
     assertEquals("A losing redemption must not mint any token.", 0, 
issuedCount.get());
diff --git 
a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java
 
b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java
index dbeba8141..94ab55122 100644
--- 
a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java
+++ 
b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java
@@ -26,6 +26,10 @@ public abstract class KnoxIDFArtifactStore<T> {
     private final Cache<String, T> cache;
 
     protected KnoxIDFArtifactStore(long ttl) {
+        // Entries live for twice the caller's TTL as a deliberate grace 
window: an artifact (e.g. an
+        // in-flight authorize/consent request) is created against the token 
TTL but must survive the
+        // extra round-trip through the browser/consent screen before it is 
consumed. Callers that
+        // finish with an entry earlier should remove() it rather than wait 
for expiry.
         this.cache = Caffeine.newBuilder().expireAfterWrite(ttl * 2, 
TimeUnit.MILLISECONDS).build();
     }
 
@@ -36,4 +40,9 @@ public abstract class KnoxIDFArtifactStore<T> {
     public T get(String key) {
         return cache.getIfPresent(key);
     }
+
+    /** Invalidates an entry so a single-use artifact cannot be replayed 
within its TTL grace window. */
+    public void remove(String key) {
+        cache.invalidate(key);
+    }
 }
diff --git 
a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
 
b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
index 57e8b3caf..8cad77b0b 100644
--- 
a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
+++ 
b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
@@ -16,22 +16,25 @@
  */
 package org.apache.knox.gateway.util.knoxidf;
 
-import com.google.common.collect.Sets;
+import com.google.common.collect.ImmutableSet;
 
 import java.util.Set;
 
 public interface KnoxIDFConstants {
-    String BASE_RESORCE_PATH = "knoxidf/api/v1";
+    String BASE_RESOURCE_PATH = "knoxidf/api/v1";
     String AUTH_CODE = "authorization_code";
     String CLIENT_ID = "client_id";
     String REDIRECT_URI = "redirect_uri";
     String REDIRECT_URIS = "redirect_uris";
     String RESPONSE_TYPE = "response_type";
-    Set<String> ALLOWED_RESPONSE_TYPES = Sets.newHashSet("code", "id_token", 
"code id_token");
+    // Immutable: an interface field is implicitly public static final, but a 
mutable HashSet would
+    // still let any caller add()/remove() on the shared instance. 
ImmutableSet forbids that.
+    Set<String> ALLOWED_RESPONSE_TYPES = ImmutableSet.of("code", "id_token", 
"code id_token");
     String SCOPE = "scope";
     String ALLOWED_SCOPES = "allowed_scopes";
     String OFFLINE_ACCESS_SCOPE = "offline_access";
-    Set<String> DEFAULT_SCOPES = Sets.newHashSet("openid", "profile", "email", 
OFFLINE_ACCESS_SCOPE);
+    // Immutable shared constant; callers that need a mutable working set copy 
it (new HashSet<>(...)).
+    Set<String> DEFAULT_SCOPES = ImmutableSet.of("openid", "profile", "email", 
OFFLINE_ACCESS_SCOPE);
     String OPENID_SCOPE = SCOPE + "=openid";
     String STATE = "state";
     String CODE = "code";
diff --git 
a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java
 
b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java
index 98aeb8520..423ad37ad 100644
--- 
a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java
+++ 
b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java
@@ -23,6 +23,8 @@ import org.apache.knox.gateway.util.JsonUtils;
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.Response;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -33,11 +35,42 @@ import java.util.Set;
 
 public class KnoxIDFUtils {
 
+    /**
+     * Builds an OAuth 2.0 error response, deriving the HTTP status from the 
error code per
+     * RFC 6749 ยง5.2 (rather than the previous always-401). Most protocol 
errors are client errors
+     * (400); {@code invalid_client} is an authentication failure (401), 
{@code access_denied} is a
+     * policy denial (403), and {@code server_error} is 500. Use the three-arg 
overload to override
+     * the status explicitly when a call site needs a status the code alone 
does not imply.
+     */
     public static Response error(String error, String description) {
+        return error(error, description, statusForError(error));
+    }
+
+    public static Response error(String error, String description, 
Response.Status status) {
         final Map<String, String> errorMap = new HashMap<>();
         errorMap.put("error", error);
         errorMap.put("error_description", description);
-        return 
Response.status(Response.Status.UNAUTHORIZED).entity(JsonUtils.renderAsJsonString(errorMap)).build();
+        return 
Response.status(status).entity(JsonUtils.renderAsJsonString(errorMap)).build();
+    }
+
+    private static Response.Status statusForError(String error) {
+        if (error == null) {
+            return Response.Status.BAD_REQUEST;
+        }
+        switch (error) {
+            case "invalid_client":
+                return Response.Status.UNAUTHORIZED;            // 401
+            case "access_denied":
+                return Response.Status.FORBIDDEN;               // 403
+            case "server_error":
+                return Response.Status.INTERNAL_SERVER_ERROR;   // 500
+            case "temporarily_unavailable":
+                return Response.Status.SERVICE_UNAVAILABLE;     // 503
+            default:
+                // invalid_request, invalid_grant, invalid_scope, 
unsupported_grant_type,
+                // unsupported_response_type, unauthorized_client are all 400s.
+                return Response.Status.BAD_REQUEST;             // 400
+        }
     }
 
     public static String getRequestParamSafe(final HttpServletRequest request, 
final String key) {
@@ -59,7 +92,9 @@ public class KnoxIDFUtils {
         final String responseType = 
request.getParameter(KnoxIDFConstants.RESPONSE_TYPE);
         final String redirectUri = 
request.getParameter(KnoxIDFConstants.REDIRECT_URI);
         final String scope = request.getParameter(KnoxIDFConstants.SCOPE);
-        final Set<String> requestedScopes = StringUtils.isBlank(scope) ? 
KnoxIDFConstants.DEFAULT_SCOPES : new 
HashSet<>(Arrays.asList(scope.split("\\s+")));
+        // Copy DEFAULT_SCOPES into a mutable set: the constant is now an 
ImmutableSet, and callers
+        // downstream may add/remove scopes on the returned set.
+        final Set<String> requestedScopes = StringUtils.isBlank(scope) ? new 
HashSet<>(KnoxIDFConstants.DEFAULT_SCOPES) : new 
HashSet<>(Arrays.asList(scope.split("\\s+")));
         final String state = request.getParameter(KnoxIDFConstants.STATE);
         final String nonce = request.getParameter(KnoxIDFConstants.NONCE);
         final String codeChallenge = 
request.getParameter(KnoxIDFConstants.CODE_CHALLENGE);
@@ -68,12 +103,20 @@ public class KnoxIDFUtils {
     }
 
     public static String buildFederatedOpAuthRedirect(final 
FederatedOpConfiguration federatedOpConfiguration, final String federatedState) 
{
+        // URL-encode every value placed into the query string. client_id and 
the callback URI
+        // (which itself contains ':' '/' '?' etc.) and the state must be 
percent-encoded or the
+        // OP receives a malformed/parameter-split URL. CODE_RESPONSE_TYPE and 
OPENID_SCOPE are
+        // fixed "key=value" literals with no reserved characters, so they are 
appended as-is.
         return federatedOpConfiguration.getAuthorizeEndpoint()
-                + "?" + KnoxIDFConstants.CLIENT_ID + "=" + 
federatedOpConfiguration.getClientId()
-                + "&" + KnoxIDFConstants.REDIRECT_URI + "=" + 
federatedOpConfiguration.getAuthorizeCallback()
+                + "?" + KnoxIDFConstants.CLIENT_ID + "=" + 
urlEncode(federatedOpConfiguration.getClientId())
+                + "&" + KnoxIDFConstants.REDIRECT_URI + "=" + 
urlEncode(federatedOpConfiguration.getAuthorizeCallback())
                 + "&" + KnoxIDFConstants.CODE_RESPONSE_TYPE
                 + "&" + KnoxIDFConstants.OPENID_SCOPE
-                + "&" + KnoxIDFConstants.STATE + "=" + federatedState;
+                + "&" + KnoxIDFConstants.STATE + "=" + 
urlEncode(federatedState);
+    }
+
+    private static String urlEncode(final String value) {
+        return value == null ? "" : URLEncoder.encode(value, 
StandardCharsets.UTF_8);
     }
 
 }
diff --git 
a/gateway-util-common/src/test/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStoreTest.java
 
b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStoreTest.java
new file mode 100644
index 000000000..04dbb9e84
--- /dev/null
+++ 
b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStoreTest.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.apache.knox.gateway.util.knoxidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+/**
+ * Verifies the {@link KnoxIDFArtifactStore#remove(String)} single-use 
invalidation added so an
+ * artifact (e.g. an authorize/consent state) cannot be replayed within its 
TTL grace window.
+ */
+public class KnoxIDFArtifactStoreTest {
+
+  /** Minimal concrete store; the base class is abstract. TTL is large so 
nothing expires mid-test. */
+  private static final class TestStore extends KnoxIDFArtifactStore<String> {
+    TestStore() {
+      super(60_000L);
+    }
+  }
+
+  @Test
+  public void testPutThenGetReturnsValue() {
+    final TestStore store = new TestStore();
+    store.put("k", "v");
+    assertEquals("v", store.get("k"));
+  }
+
+  @Test
+  public void testRemoveInvalidatesEntry() {
+    final TestStore store = new TestStore();
+    store.put("state", "payload");
+    store.remove("state");
+    assertNull("A removed entry must not be retrievable (single-use replay 
guard).", store.get("state"));
+  }
+
+  @Test
+  public void testRemoveIsIdempotentAndSafeForUnknownKey() {
+    final TestStore store = new TestStore();
+    store.remove("never-put"); // must not throw
+    assertNull(store.get("never-put"));
+  }
+
+  @Test
+  public void testGetUnknownKeyReturnsNull() {
+    assertNull(new TestStore().get("missing"));
+  }
+}

Reply via email to