Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Compiled class file
*.class
.classpath
.factorypath
.settings
.project
*.iml
*.iws
*.ipr
.idea
.DS_Store

# Log file
*.log
Expand All @@ -22,3 +31,6 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

# Ignore everything in this directory
target
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com).
~ Copyright (c) 2025-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 @@ -153,6 +153,7 @@
org.wso2.carbon.identity.notification.sender.tenant.config.dto; version="${identity.event.handler.notification.imp.pkg.version.range}",
org.wso2.carbon.identity.notification.sender.tenant.config.exception; version="${identity.event.handler.notification.imp.pkg.version.range}",
org.wso2.carbon.identity.application.authentication.framework.store; version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.application.authentication.framework.util; version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.utils; version="${carbon.kernel.package.import.version.range}",
org.wso2.carbon.utils.multitenancy; version="${carbon.kernel.package.import.version.range}",
org.wso2.carbon.database.utils.jdbc;version="${org.wso2.carbon.database.utils.version.range}",
Expand Down Expand Up @@ -189,7 +190,8 @@
<exclude>**/*Constants*.class</exclude>
<exclude>**/model/**</exclude>
<exclude>**/internal/**</exclude>
<exclude>**/cache/**</exclude>
<exclude>**/cache/DeviceRegistrationRequestCache.class</exclude>
<exclude>**/cache/DeviceRegistrationRequestCacheKey.class</exclude>
</excludes>
</configuration>
<executions>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com).
* Copyright (c) 2025-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 All @@ -18,16 +18,29 @@

package org.wso2.carbon.identity.notification.push.device.handler.cache;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils;
import org.wso2.carbon.identity.core.cache.CacheEntry;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationContext;

import java.util.concurrent.TimeUnit;

import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD;

/**
* Cache entry for Device Registration Request.
*/
public class DeviceRegistrationRequestCacheEntry extends CacheEntry {

private static final long serialVersionUID = -4976259795783529978L;
private static final Log LOG = LogFactory.getLog(DeviceRegistrationRequestCacheEntry.class);

private final DeviceRegistrationContext deviceRegistrationContext;
private final long expiresAt;

/**
* Constructor for DeviceRegistrationRequestCacheEntry.
Expand All @@ -37,15 +50,44 @@ public class DeviceRegistrationRequestCacheEntry extends CacheEntry {
public DeviceRegistrationRequestCacheEntry(DeviceRegistrationContext deviceRegistrationContext) {

this.deviceRegistrationContext = deviceRegistrationContext;
long validityPeriodInNanos = getValidityPeriodInNanos();
this.expiresAt = getCurrentTimeInNanos() + validityPeriodInNanos;
// This will be used in the session store to set the cache entry expiry time.
this.setValidityPeriod(validityPeriodInNanos);
}

/**
* Get the device registration context.
*
* @return DeviceRegistrationContext
* @return DeviceRegistrationContext if the cache entry has not expired, null otherwise.
*/
public DeviceRegistrationContext getDeviceRegistrationContext() {

if (getCurrentTimeInNanos() > expiresAt) {
LOG.debug("Push device registration request cache entry has expired.");
return null;
}
return deviceRegistrationContext;
}

private long getValidityPeriodInNanos() {

int expirySeconds = DEFAULT_DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD;
String configuredExpiry = IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD);
if (StringUtils.isNotBlank(configuredExpiry)) {
try {
expirySeconds = Integer.parseInt(configuredExpiry.trim());
} catch (NumberFormatException e) {
LOG.warn("Invalid value configured for '" + DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD
+ "': " + configuredExpiry + ". Using default value: " + expirySeconds + " seconds.", e);
}
}

return TimeUnit.SECONDS.toNanos(expirySeconds);
Comment thread
ashanthamara marked this conversation as resolved.
}

private long getCurrentTimeInNanos() {

return FrameworkUtils.getCurrentStandardNano();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class PushDeviceHandlerConstants {
public static final String SIGNATURE_ALGORITHM = "RSA";
public static final String DEVICE_REGISTRATION_REQUEST_CACHE = "PushDeviceRegistrationRequestCache";
public static final String DEFAULT_PUSH_PROVIDER = "defaultPushProvider";
public static final String DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD =
"PushAuthenticator.DeviceRegistrationContext.ValidityPeriod";
public static final int DEFAULT_DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD = 180;

/**
* Private constructor to prevent initialization of the class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package org.wso2.carbon.identity.notification.push.device.handler.impl;

import org.wso2.carbon.identity.application.authentication.framework.store.SessionDataStore;
import org.wso2.carbon.identity.core.util.IdentityCacheUtil;
import org.wso2.carbon.identity.notification.push.device.handler.DeviceRegistrationContextManager;
import org.wso2.carbon.identity.notification.push.device.handler.cache.DeviceRegistrationRequestCache;
import org.wso2.carbon.identity.notification.push.device.handler.cache.DeviceRegistrationRequestCacheEntry;
Expand All @@ -36,27 +35,22 @@ public class DeviceRegistrationContextManagerImpl implements DeviceRegistrationC
@Override
public void storeRegistrationContext(String key, DeviceRegistrationContext context, String tenantDomain) {

DeviceRegistrationRequestCache.getInstance().addToCache(
new DeviceRegistrationRequestCacheKey(key),
new DeviceRegistrationRequestCacheEntry(context),
DeviceRegistrationRequestCacheEntry cacheEntry = new DeviceRegistrationRequestCacheEntry(context);
DeviceRegistrationRequestCache.getInstance().addToCache(new DeviceRegistrationRequestCacheKey(key), cacheEntry,
tenantDomain);
storeToSessionStore(key, new DeviceRegistrationRequestCacheEntry(context));
storeToSessionStore(key, cacheEntry);
}

@Override
public DeviceRegistrationContext getContext(String key, String tenantDomain) {

DeviceRegistrationRequestCacheEntry cacheEntry = DeviceRegistrationRequestCache.getInstance().getValueFromCache(
new DeviceRegistrationRequestCacheKey(key), tenantDomain);
if (cacheEntry != null) {
return cacheEntry.getDeviceRegistrationContext();
} else {
if (cacheEntry == null) {
cacheEntry = getFromSessionStore(key);
if (!IdentityCacheUtil.isCacheEntryExpired(cacheEntry)) {
return cacheEntry.getDeviceRegistrationContext();
}
}
return null;

return cacheEntry != null ? cacheEntry.getDeviceRegistrationContext() : null;
Comment thread
ashanthamara marked this conversation as resolved.
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* 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.notification.push.device.handler.cache;

import org.mockito.MockedStatic;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationContext;

import java.util.concurrent.TimeUnit;

import static org.mockito.Mockito.mockStatic;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD;
import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD;

/**
* Unit tests for DeviceRegistrationRequestCacheEntry.
*/
public class DeviceRegistrationRequestCacheEntryTest {

private static final String TEST_CHALLENGE = "test-challenge";
private static final String TEST_USERNAME = "test-user";
private static final String TEST_TENANT_DOMAIN = "test.com";
private static final long DEFAULT_VALIDITY_NANOS =
TimeUnit.SECONDS.toNanos(DEFAULT_DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD);

private MockedStatic<IdentityUtil> mockedIdentityUtil;
private MockedStatic<FrameworkUtils> mockedFrameworkUtils;

@BeforeMethod
public void setUp() {

mockedIdentityUtil = mockStatic(IdentityUtil.class);
mockedFrameworkUtils = mockStatic(FrameworkUtils.class);
}

@AfterMethod
public void tearDown() {

mockedIdentityUtil.close();
mockedFrameworkUtils.close();
}

@Test
public void testGetDeviceRegistrationContext_ReturnsContext_WhenNotExpired() {

DeviceRegistrationContext context =
new DeviceRegistrationContext(TEST_CHALLENGE, TEST_USERNAME, TEST_TENANT_DOMAIN, false);

mockedIdentityUtil.when(() -> IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD))
.thenReturn(null);
mockedFrameworkUtils.when(FrameworkUtils::getCurrentStandardNano).thenCallRealMethod();

DeviceRegistrationRequestCacheEntry entry = new DeviceRegistrationRequestCacheEntry(context);
DeviceRegistrationContext result = entry.getDeviceRegistrationContext();

Assert.assertNotNull(result, "Expected context to be returned when not expired.");
Assert.assertEquals(result, context, "Expected the same context object to be returned.");
}

@Test
public void testGetDeviceRegistrationContext_ReturnsNull_WhenExpired() {

DeviceRegistrationContext context =
new DeviceRegistrationContext(TEST_CHALLENGE, TEST_USERNAME, TEST_TENANT_DOMAIN, false);

mockedIdentityUtil.when(() -> IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD))
.thenReturn(null);
// Constructor at time 0, getter past default validity
long expiredQueryTime = DEFAULT_VALIDITY_NANOS + 1L;
mockedFrameworkUtils.when(FrameworkUtils::getCurrentStandardNano).thenReturn(0L, expiredQueryTime);

DeviceRegistrationRequestCacheEntry entry = new DeviceRegistrationRequestCacheEntry(context);
DeviceRegistrationContext result = entry.getDeviceRegistrationContext();

Assert.assertNull(result, "Expected null when the cache entry has expired.");
}

@Test
public void testGetDeviceRegistrationContext_ReturnsContext_AtExactExpiry() {

DeviceRegistrationContext context =
new DeviceRegistrationContext(TEST_CHALLENGE, TEST_USERNAME, TEST_TENANT_DOMAIN, false);

mockedIdentityUtil.when(() -> IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD))
.thenReturn(null);
mockedFrameworkUtils.when(FrameworkUtils::getCurrentStandardNano).thenReturn(0L, DEFAULT_VALIDITY_NANOS);

DeviceRegistrationRequestCacheEntry entry = new DeviceRegistrationRequestCacheEntry(context);
DeviceRegistrationContext result = entry.getDeviceRegistrationContext();

Assert.assertNotNull(result,
"Expected context at exact expiry boundary (expiry check uses strict > not >=).");
}

@Test
public void testGetDeviceRegistrationContext_ReturnsNull_WhenContextIsNull() {

mockedIdentityUtil.when(() -> IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD))
.thenReturn(null);
mockedFrameworkUtils.when(FrameworkUtils::getCurrentStandardNano).thenReturn(0L, 0L);

DeviceRegistrationRequestCacheEntry entry = new DeviceRegistrationRequestCacheEntry(null);
DeviceRegistrationContext result = entry.getDeviceRegistrationContext();

Assert.assertNull(result, "Expected null when the stored context itself is null.");
}

@Test
public void testValidityPeriod_UsesDefault_WhenConfigIsNull() {

DeviceRegistrationContext context =
new DeviceRegistrationContext(TEST_CHALLENGE, TEST_USERNAME, TEST_TENANT_DOMAIN, false);

mockedIdentityUtil.when(() -> IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD))
.thenReturn(null);
// Constructor: 0L | 1st getter: at exact boundary (still valid) | 2nd getter: past boundary (expired)
mockedFrameworkUtils.when(FrameworkUtils::getCurrentStandardNano)
.thenReturn(0L, DEFAULT_VALIDITY_NANOS, DEFAULT_VALIDITY_NANOS + 1L);

DeviceRegistrationRequestCacheEntry entry = new DeviceRegistrationRequestCacheEntry(context);

Assert.assertNotNull(entry.getDeviceRegistrationContext(),
"Context should be valid at the exact default expiry boundary.");
Assert.assertNull(entry.getDeviceRegistrationContext(),
"Context should be null after the default 180s validity period.");
}

@Test
public void testValidityPeriod_UsesDefault_WhenConfigIsBlank() {

DeviceRegistrationContext context =
new DeviceRegistrationContext(TEST_CHALLENGE, TEST_USERNAME, TEST_TENANT_DOMAIN, false);

mockedIdentityUtil.when(() -> IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD))
.thenReturn(" ");
// Constructor: 0L | 1st getter: at exact boundary (still valid) | 2nd getter: past boundary (expired)
mockedFrameworkUtils.when(FrameworkUtils::getCurrentStandardNano)
.thenReturn(0L, DEFAULT_VALIDITY_NANOS, DEFAULT_VALIDITY_NANOS + 1L);

DeviceRegistrationRequestCacheEntry entry = new DeviceRegistrationRequestCacheEntry(context);

Assert.assertNotNull(entry.getDeviceRegistrationContext(),
"Context should be valid at the exact default expiry boundary when config is blank.");
Assert.assertNull(entry.getDeviceRegistrationContext(),
"Context should be null after default validity when config is blank.");
}

@Test
public void testValidityPeriod_UsesCustomValue_WhenConfigIsValid() {

DeviceRegistrationContext context =
new DeviceRegistrationContext(TEST_CHALLENGE, TEST_USERNAME, TEST_TENANT_DOMAIN, false);
int customSeconds = 300;
long customValidityNanos = TimeUnit.SECONDS.toNanos(customSeconds);

mockedIdentityUtil.when(() -> IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD))
.thenReturn(String.valueOf(customSeconds));
// Constructor: 0L
// 1st getter: past default 180s but within custom 300s (still valid)
// 2nd getter: past custom 300s (expired)
mockedFrameworkUtils.when(FrameworkUtils::getCurrentStandardNano)
.thenReturn(0L, DEFAULT_VALIDITY_NANOS + 1L, customValidityNanos + 1L);

DeviceRegistrationRequestCacheEntry entry = new DeviceRegistrationRequestCacheEntry(context);

Assert.assertNotNull(entry.getDeviceRegistrationContext(),
"Context should be valid past default 180s when custom 300s period is configured.");
Assert.assertNull(entry.getDeviceRegistrationContext(),
"Context should be null after the custom 300s validity period.");
}

@Test
public void testValidityPeriod_UsesDefault_WhenConfigIsInvalid() {

DeviceRegistrationContext context =
new DeviceRegistrationContext(TEST_CHALLENGE, TEST_USERNAME, TEST_TENANT_DOMAIN, false);

mockedIdentityUtil.when(() -> IdentityUtil.getProperty(DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD))
.thenReturn("notAnInt");
mockedFrameworkUtils.when(FrameworkUtils::getCurrentStandardNano).thenCallRealMethod();

DeviceRegistrationRequestCacheEntry entry = new DeviceRegistrationRequestCacheEntry(context);

Assert.assertNotNull(entry.getDeviceRegistrationContext(),
"Context should be valid at exact default expiry when config value causes NumberFormatException.");
Assert.assertEquals(entry.getValidityPeriod(), DEFAULT_VALIDITY_NANOS);
}
}
Loading
Loading