diff --git a/changelog.md b/changelog.md
index 375a3af0..365536a0 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,11 @@
# Changelog
+## v1.10.0
+
+### Oct 27, 2025
+
+- Feature : TOTP support
+
## v1.9.0
### Oct 13, 2025
@@ -8,7 +14,7 @@
## v1.8.0
-### Sep 15, 2025
+### Sep 29, 2025
- Feature : OAuth 2.0 support with PKCE flow
- Improved code organization and removed redundant methods
diff --git a/pom.xml b/pom.xml
index 0b0ceaee..11e2b793 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
cms
jar
contentstack-management-java
- 1.9.0
+ 1.10.0
Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an
API-first approach
@@ -214,6 +214,11 @@
kotlin-stdlib
2.1.21
+
+ com.warrenstrange
+ googleauth
+ 1.5.0
+
diff --git a/src/main/java/com/contentstack/cms/Contentstack.java b/src/main/java/com/contentstack/cms/Contentstack.java
index dbf3dbc0..1f01dbcb 100644
--- a/src/main/java/com/contentstack/cms/Contentstack.java
+++ b/src/main/java/com/contentstack/cms/Contentstack.java
@@ -27,6 +27,7 @@
import com.contentstack.cms.stack.Stack;
import com.contentstack.cms.user.User;
import com.google.gson.Gson;
+import com.warrenstrange.googleauth.GoogleAuthenticator;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
@@ -40,9 +41,8 @@
* Contentstack Java Management SDK
*
* Java Management SDK Interact with the Content Management APIs and
- * allow you to create, update,
- * delete, and fetch content from your Contentstack account. (They are
- * read-write in nature.)
+ * allow you to create, update, delete, and fetch content from your Contentstack
+ * account. (They are read-write in nature.)
*
* You can use them to build your own apps and manage your content from
* Contentstack.
@@ -66,8 +66,7 @@ public class Contentstack {
/**
* All accounts registered with Contentstack are known as Users. A stack can
- * have many users with varying
- * permissions and roles
+ * have many users with varying permissions and roles
*
* To perform User operations first get User instance like below.
*
@@ -89,7 +88,8 @@ public class Contentstack {
*
*
* @return User
- * @author ***REMOVED***
+ * @author ***REMOVED
+ ***
* @see User
*
@@ -105,15 +105,14 @@ public User user() {
/**
* [Note]: Before executing any calls, retrieve the authtoken by
- * authenticating yourself via the Log in call of User Session. The authtoken is
- * returned to the 'Response' body of
- * the Log in call and is mandatory in all the calls.
+ * authenticating yourself via the Log in call of User Session. The
+ * authtoken is returned to the 'Response' body of the Log in call and is
+ * mandatory in all the calls.
*
* Example:
*
* All accounts registered with Contentstack are known as Users. A stack can
- * have many users with varying
- * permissions and roles
+ * have many users with varying permissions and roles
*
* To perform User operations first get User instance like below.
*
@@ -136,18 +135,20 @@ public User user() {
*
*
*
- * @param emailId the email id of the user
+ * @param emailId the email id of the user
* @param password the password of the user
* @return LoginDetails
* @throws IOException the IOException
- * @author ***REMOVED***
+ * @author ***REMOVED
+ ***
* @see User
*
*/
public Response login(String emailId, String password) throws IOException {
- if (this.authtoken != null)
+ if (this.authtoken != null) {
throw new IllegalStateException(Util.USER_ALREADY_LOGGED_IN);
+ }
user = new User(this.instance);
Response response = user.login(emailId, password).execute();
setupLoginCredentials(response);
@@ -155,61 +156,58 @@ public Response login(String emailId, String password) throws IOEx
}
/**
- * [Note]: Before executing any calls, retrieve the authtoken by
- * authenticating yourself via the Log in call of User Session. The authtoken is
- * returned to the 'Response' body of
- * the Log in call and is mandatory in all the calls.
- *
- * Example:
- *
- * All accounts registered with Contentstack are known as Users. A stack can
- * have many users with varying
- * permissions and roles
- *
- * To perform User operations first get User instance like below.
- *
- * Example:
- *
+ * Login with two-factor authentication. This method provides flexibility to use either:
+ * 1. A direct 2FA token using params.put("tfaToken", "123456"), OR
+ * 2. An MFA secret to generate TOTP using params.put("mfaSecret", "YOUR_SECRET")
+ *
+ * Note: Do not provide both tfaToken and mfaSecret. Choose one authentication method.
+ *
+ * @param emailId The email ID of the user
+ * @param password The user's password
+ * @param params Map containing either tfaToken or mfaSecret
+ * @return Response containing login details
+ * @throws IOException if there's a network error
+ * @throws IllegalArgumentException if validation fails or if both tfaToken and mfaSecret are provided
+ * @throws IllegalStateException if user is already logged in
+ *
+ * Example:
*
- * Contentstack contentstack = new Contentstack.Builder().setAuthtoken("authtoken").build();
- * Response login = contentstack.login();
- *
- * Access more other user functions from the userInstance
+ * // Login with direct token
+ * {@code Map params = new HashMap<>();}
+ * {@code params.put("tfaToken", "123456");}
+ * {@code Response response = contentstack.login(email, password, params);}
+ *
+ * // OR login with MFA secret
+ * {@code Map params = new HashMap<>();}
+ * {@code params.put("mfaSecret", "YOUR_SECRET");}
+ * {@code Response response = contentstack.login(email, password, params);}
*
- *
- *
- * OR:
- *
- *
- * Contentstack contentstack = new Contentstack.Builder().build();
- * Response login = contentstack.login("emailId", "password");
- *
- *
- *
- *
- * @param emailId the email id
- * @param password the password
- * @param tfaToken the tfa token
- * @return LoginDetails
- * @throws IOException the io exception
- * @throws IOException the IOException
- * @author ***REMOVED***
- * @see Login
- * your account
- *
*/
- public Response login(String emailId, String password, String tfaToken) throws IOException {
- if (this.authtoken != null)
+ public Response login(String emailId, String password, Map params) throws IOException {
+ // Check if already logged in
+ if (this.authtoken != null) {
throw new IllegalStateException(Util.USER_ALREADY_LOGGED_IN);
+ }
+
+ // Validate basic inputs
+ if (emailId.trim().isEmpty()) {
+ throw new IllegalArgumentException("Email is required");
+ }
+ if (password.trim().isEmpty()) {
+ throw new IllegalArgumentException("Password is required");
+ }
+ if (params.isEmpty()) {
+ throw new IllegalArgumentException("Authentication parameters are required");
+ }
+
+ // Perform login
user = new User(this.instance);
- Response response = user.login(emailId, password, tfaToken).execute();
+ Response response = user.login(emailId, password, params).execute();
setupLoginCredentials(response);
- user = new User(this.instance);
return response;
}
+
private void setupLoginCredentials(Response loginResponse) throws IOException {
if (loginResponse.isSuccessful()) {
LoginDetails loginDetails = loginResponse.body();
@@ -230,8 +228,8 @@ private void setupLoginCredentials(Response loginResponse) throws
}
/**
- * The Log out of your account call is used to sign out the user of Contentstack
- * account
+ * The Log out of your account call is used to sign out the user of
+ * Contentstack account
*
* Example
*
@@ -250,8 +248,8 @@ Response logout() throws IOException {
}
/**
- * The Log out of your account using authtoken is used to sign out the user of
- * Contentstack account
+ * The Log out of your account using authtoken is used to sign out the user
+ * of Contentstack account
*
* Example
*
@@ -276,9 +274,8 @@ Response logoutWithAuthtoken(String authtoken) throws IOException
/**
* Organization is the top-level entity in the hierarchy of Contentstack,
- * consisting of stacks and stack resources,
- * and users. Organization allows easy management of projects as well as users
- * within the Organization.
+ * consisting of stacks and stack resources, and users. Organization allows
+ * easy management of projects as well as users within the Organization.
*
* Example
*
@@ -293,7 +290,7 @@ public Organization organization() {
if (!isOAuthConfigured() && this.authtoken == null) {
throw new IllegalStateException(Util.OAUTH_LOGIN_REQUIRED + " organization");
}
-
+
// If using OAuth, get organization from tokens
if (isOAuthConfigured() && oauthHandler.getTokens() != null) {
String orgUid = oauthHandler.getTokens().getOrganizationUid();
@@ -301,19 +298,19 @@ public Organization organization() {
return organization(orgUid);
}
}
-
+
return new Organization(this.instance);
}
/**
* Organization is the top-level entity in the hierarchy of Contentstack,
- * consisting of stacks and stack resources,
- * and users. Organization allows easy management of projects as well as users
- * within the Organization.
+ * consisting of stacks and stack resources, and users. Organization allows
+ * easy management of projects as well as users within the Organization.
*
* Example
*
- * @param organizationUid The UID of the organization that you want to retrieve
+ * @param organizationUid The UID of the organization that you want to
+ * retrieve
* @return the organization
*
* Example
@@ -322,7 +319,7 @@ public Organization organization() {
* Contentstack contentstack = new Contentstack.Builder().build();
*
* Organization org = contentstack.organization();
- *
+ *
*/
public Organization organization(@NotNull String organizationUid) {
if (!isOAuthConfigured() && this.authtoken == null) {
@@ -337,10 +334,9 @@ public Organization organization(@NotNull String organizationUid) {
/**
* stack
- * A stack is
- * a space that stores the content of a project (a web or mobile property).
- * Within a stack, you can create content
- * structures, content entries, users, etc. related to the project
+ * A stack is a space that stores the content of a project (a web or mobile
+ * property). Within a stack, you can create content structures, content
+ * entries, users, etc. related to the project
*
* Example
*
@@ -361,10 +357,9 @@ public Stack stack() {
/**
* stack
- * A stack is
- * a space that stores the content of a project (a web or mobile property).
- * Within a stack, you can create content
- * structures, content entries, users, etc. related to the project
+ * A stack is a space that stores the content of a project (a web or mobile
+ * property). Within a stack, you can create content structures, content
+ * entries, users, etc. related to the project
*
* Example
*
@@ -386,10 +381,9 @@ public Stack stack(@NotNull Map header) {
/**
* stack
- * A stack is
- * a space that stores the content of a project (a web or mobile property).
- * Within a stack, you can create content
- * structures, content entries, users, etc. related to the project
+ * A stack is a space that stores the content of a project (a web or mobile
+ * property). Within a stack, you can create content structures, content
+ * entries, users, etc. related to the project
*
* Example
*
@@ -399,7 +393,7 @@ public Stack stack(@NotNull Map header) {
*
*
* @param managementToken the authorization for the stack
- * @param apiKey the apiKey for the stack
+ * @param apiKey the apiKey for the stack
* @return the stack instance
*/
public Stack stack(@NotNull String apiKey, @NotNull String managementToken) {
@@ -412,10 +406,9 @@ public Stack stack(@NotNull String apiKey, @NotNull String managementToken) {
/**
* stack
- * A stack is
- * a space that stores the content of a project (a web or mobile property).
- * Within a stack, you can create content
- * structures, content entries, users, etc. related to the project
+ * A stack is a space that stores the content of a project (a web or mobile
+ * property). Within a stack, you can create content structures, content
+ * entries, users, etc. related to the project
*
* Example
*
@@ -442,10 +435,9 @@ public Stack stack(@NotNull String key) {
/**
* stack
- * A stack is
- * a space that stores the content of a project (a web or mobile property).
- * Within a stack, you can create content
- * structures, content entries, users, etc. related to the project
+ * A stack is a space that stores the content of a project (a web or mobile
+ * property). Within a stack, you can create content structures, content
+ * entries, users, etc. related to the project
*
* Example
*
@@ -455,8 +447,8 @@ public Stack stack(@NotNull String key) {
*
*
* @param managementToken the authorization for the stack
- * @param apiKey the apiKey for the stack
- * @param branch the branch that include branching in the response
+ * @param apiKey the apiKey for the stack
+ * @param branch the branch that include branching in the response
* @return the stack instance
*/
public Stack stack(@NotNull String apiKey, @NotNull String managementToken, @NotNull String branch) {
@@ -469,6 +461,7 @@ public Stack stack(@NotNull String apiKey, @NotNull String managementToken, @Not
/**
* Get the OAuth authorization URL for the user to visit
+ *
* @return Authorization URL string
* @throws IllegalStateException if OAuth is not configured
*/
@@ -481,6 +474,7 @@ public String getOAuthAuthorizationUrl() {
/**
* Exchange OAuth authorization code for tokens
+ *
* @param code Authorization code from OAuth callback
* @return CompletableFuture containing OAuth tokens
* @throws IllegalStateException if OAuth is not configured
@@ -494,8 +488,10 @@ public CompletableFuture exchangeOAuthCode(String code) {
/**
* Refresh the OAuth access token
+ *
* @return CompletableFuture containing new OAuth tokens
- * @throws IllegalStateException if OAuth is not configured or no refresh token available
+ * @throws IllegalStateException if OAuth is not configured or no refresh
+ * token available
*/
public CompletableFuture refreshOAuthToken() {
if (!isOAuthConfigured()) {
@@ -506,6 +502,7 @@ public CompletableFuture refreshOAuthToken() {
/**
* Get the current OAuth tokens
+ *
* @return Current OAuth tokens or null if not available
*/
public OAuthTokens getOAuthTokens() {
@@ -514,6 +511,7 @@ public OAuthTokens getOAuthTokens() {
/**
* Check if we have valid OAuth tokens
+ *
* @return true if we have valid tokens
*/
public boolean hasValidOAuthTokens() {
@@ -522,6 +520,7 @@ public boolean hasValidOAuthTokens() {
/**
* Check if OAuth is configured
+ *
* @return true if OAuth is configured
*/
public boolean isOAuthConfigured() {
@@ -530,6 +529,7 @@ public boolean isOAuthConfigured() {
/**
* Get the OAuth handler instance
+ *
* @return OAuth handler or null if not configured
*/
public OAuthHandler getOAuthHandler() {
@@ -538,6 +538,7 @@ public OAuthHandler getOAuthHandler() {
/**
* Logout from OAuth session and optionally revoke authorization
+ *
* @param revokeAuthorization If true, revokes the OAuth authorization
* @return CompletableFuture that completes when logout is done
*/
@@ -550,6 +551,7 @@ public CompletableFuture oauthLogout(boolean revokeAuthorization) {
/**
* Logout from OAuth session without revoking authorization
+ *
* @return CompletableFuture that completes when logout is done
*/
public CompletableFuture oauthLogout() {
@@ -595,8 +597,8 @@ public static class Builder {
private Boolean retry = Util.RETRY_ON_FAILURE;// Default base url for contentstack
/**
- * Default ConnectionPool holds up to 5 idle connections which
- * will be evicted after 5 minutes of inactivity.
+ * Default ConnectionPool holds up to 5 idle connections which will be
+ * evicted after 5 minutes of inactivity.
*/
private ConnectionPool connectionPool = new ConnectionPool(); // Connection
@@ -609,8 +611,7 @@ public Builder() {
/**
* Sets proxy. (Setting proxy to the OkHttpClient) Proxy = new
- * Proxy(Proxy.Type.HTTP, new
- * InetSocketAddress(proxyHost, proxyPort));
+ * Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
*
*
* {@code
@@ -687,26 +688,20 @@ public Builder setTimeout(int timeout) {
/**
* Create a new connection pool with tuning parameters appropriate for a
- * single-user application.
- * The tuning parameters in this pool are subject to change in future OkHttp
- * releases. Currently,
- * this pool holds up to 5 idle connections which will be evicted after 5
- * minutes of inactivity.
+ * single-user application. The tuning parameters in this pool are
+ * subject to change in future OkHttp releases. Currently, this pool
+ * holds up to 5 idle connections which will be evicted after 5 minutes
+ * of inactivity
*
- *
- * public ConnectionPool() {
- * this(5, 5, TimeUnit.MINUTES);
- * }
+ * public ConnectionPool() { this(5, 5, TimeUnit.MINUTES); }
*
* @param maxIdleConnections Maximum number of idle connections
- * @param keepAliveDuration The Keep Alive Duration
- * @param timeUnit A TimeUnit represents time durations at a given
- * unit of granularity and provides utility methods to
- * convert across units
+ * @param keepAliveDuration The Keep Alive Duration
+ * @param timeUnit A TimeUnit represents time durations at a given unit
+ * of granularity and provides utility methods to convert across units
* @return instance of Builder
*
- * Example:
- * {@code
+ * Example: {@code
* Contentstack cs = new Contentstack.Builder()
* .setAuthtoken(AUTHTOKEN)
* .setConnectionPool(5, 400, TimeUnit.MILLISECONDS)
@@ -732,6 +727,7 @@ public Builder setAuthtoken(String authtoken) {
/**
* Sets OAuth configuration for the client
+ *
* @param config OAuth configuration
* @return Builder instance
*/
@@ -744,6 +740,7 @@ public Builder setOAuthConfig(OAuthConfig config) {
/**
* Sets the token callback for OAuth storage
+ *
* @param callback The callback to handle token storage
* @return Builder instance
*/
@@ -751,7 +748,7 @@ public Builder setTokenCallback(TokenCallback callback) {
this.tokenCallback = callback;
return this;
}
-
+
/**
* Configures OAuth authentication with PKCE flow (no client secret)
* @param appId Application ID
@@ -787,10 +784,10 @@ public Builder setOAuth(String appId, String clientId, String redirectUri, Strin
*/
public Builder setOAuth(String appId, String clientId, String redirectUri, String host, String clientSecret) {
OAuthConfig.OAuthConfigBuilder builder = OAuthConfig.builder()
- .appId(appId)
- .clientId(clientId)
- .redirectUri(redirectUri)
- .host(host);
+ .appId(appId)
+ .clientId(clientId)
+ .redirectUri(redirectUri)
+ .host(host);
// Only set clientSecret if provided (otherwise PKCE flow will be used)
if (clientSecret != null && !clientSecret.trim().isEmpty()) {
@@ -828,7 +825,7 @@ private void validateClient(Contentstack contentstack) {
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient(contentstack, this.retry)).build();
contentstack.instance = this.instance;
-
+
// Initialize OAuth if configured
if (this.oauthConfig != null) {
// OAuth handler and interceptor are created in httpClient
@@ -851,12 +848,12 @@ private OkHttpClient httpClient(Contentstack contentstack, Boolean retryOnFailur
OkHttpClient tempClient = builder.build();
this.oauthHandler = new OAuthHandler(tempClient, this.oauthConfig);
this.oauthInterceptor = new OAuthInterceptor(this.oauthHandler);
-
+
// Configure early access if needed
if (this.earlyAccess != null) {
this.oauthInterceptor.setEarlyAccess(this.earlyAccess);
}
-
+
// Add interceptor to handle OAuth, token refresh, and retries
builder.addInterceptor(this.oauthInterceptor);
} else {
diff --git a/src/main/java/com/contentstack/cms/user/User.java b/src/main/java/com/contentstack/cms/user/User.java
index 53b05b8f..fb549976 100644
--- a/src/main/java/com/contentstack/cms/user/User.java
+++ b/src/main/java/com/contentstack/cms/user/User.java
@@ -8,26 +8,24 @@
import org.json.simple.JSONObject;
import retrofit2.Call;
import retrofit2.Retrofit;
+import com.warrenstrange.googleauth.GoogleAuthenticator;
import java.util.HashMap;
+import java.util.Map;
/**
- * All accounts registered with Contentstack are known as Users.
- * A Stack
- * can have many users with varying
- * permissions and roles.
+ * All accounts registered with Contentstack are known as Users.
+ * A stack can have many users with varying permissions and roles.
*
* @author ***REMOVED***
* @version v0.1.0
* @since 2022-10-22
*/
-public class User implements BaseImplementation {
+public class User implements BaseImplementation {
protected final UserService userService;
- protected HashMap headers;
- protected HashMap params;
+ protected final HashMap headers;
+ protected final HashMap params;
/**
* @param client Retrofit adapts a Java interface to HTTP calls by using
@@ -66,8 +64,13 @@ public User(Retrofit client) {
* @return Call
*/
public Call login(@NotNull String email, @NotNull String password) {
+ HashMap credentials = new HashMap<>();
+ credentials.put("email", email);
+ credentials.put("password", password);
+
HashMap> userSession = new HashMap<>();
- userSession.put("user", setCredentials(email, password));
+ userSession.put("user", credentials);
+
JSONObject userDetail = new JSONObject(userSession);
return this.userService.login(loginHeader(), userDetail);
}
@@ -79,28 +82,96 @@ private HashMap loginHeader() {
}
/**
- * Login call.
+ * Login with two-factor authentication. This method provides flexibility to use either:
+ * 1. A direct 2FA token using params.put("tfaToken", "123456"), OR
+ * 2. An MFA secret to generate TOTP using params.put("mfaSecret", "YOUR_SECRET")
+ *
+ * Note: Do not provide both tfaToken and mfaSecret. Choose one authentication method.
*
- * @param email email for user to login
- * @param password password for user to login
- * @param tfaToken the tfa token
- * @return Call
+ * @param email email for user to login
+ * @param password password for user to login
+ * @param params Map containing either tfaToken or mfaSecret
+ * @return Call containing login details
+ * @throws IllegalArgumentException if validation fails or if both tfaToken and mfaSecret are provided
+ *
+ * Example:
+ *
+ * // Login with direct token
+ * {@code Map params = new HashMap<>();}
+ * {@code params.put("tfaToken", "123456");}
+ * {@code Call call = user.login(email, password, params);}
+ *
+ * // OR login with MFA secret
+ * {@code Map params = new HashMap<>();}
+ * {@code params.put("mfaSecret", "YOUR_SECRET");}
+ * {@code Call call = user.login(email, password, params);}
+ *
*/
- public Call login(@NotNull String email, @NotNull String password, @NotNull String tfaToken) {
+ public Call login(@NotNull String email, @NotNull String password, @NotNull Map params) {
+ // Validate basic inputs
+ if (email.trim().isEmpty()) {
+ throw new IllegalArgumentException("Email is required");
+ }
+ if (password.trim().isEmpty()) {
+ throw new IllegalArgumentException("Password is required");
+ }
+ if (params.isEmpty()) {
+ throw new IllegalArgumentException("Authentication parameters are required");
+ }
+
+ String tfaToken = params.get("tfaToken");
+ String mfaSecret = params.get("mfaSecret");
+
+ // Check for mutual exclusivity
+ boolean hasTfaToken = tfaToken != null && !tfaToken.trim().isEmpty();
+ boolean hasMfaSecret = mfaSecret != null && !mfaSecret.trim().isEmpty();
+
+ if (hasTfaToken && hasMfaSecret) {
+ throw new IllegalArgumentException("Cannot provide both tfaToken and mfaSecret. Use either one.");
+ }
+ if (!hasTfaToken && !hasMfaSecret) {
+ throw new IllegalArgumentException("Must provide either tfaToken or mfaSecret");
+ }
+
+ // Generate TOTP if needed
+ String finalTfaToken = tfaToken;
+ if (!hasTfaToken && hasMfaSecret) {
+ finalTfaToken = generateTOTP(mfaSecret);
+ }
+
+ // Perform login
+ HashMap credentials = new HashMap<>();
+ credentials.put("email", email);
+ credentials.put("password", password);
+ credentials.put("tfa_token", finalTfaToken);
+
HashMap> userSession = new HashMap<>();
- userSession.put("user", setCredentials(email, password, tfaToken));
+ userSession.put("user", credentials);
+
JSONObject userDetail = new JSONObject(userSession);
return this.userService.login(loginHeader(), userDetail);
}
- private HashMap setCredentials(@NotNull String... arguments) {
- HashMap credentials = new HashMap<>();
- credentials.put("email", arguments[0]);
- credentials.put("password", arguments[1]);
- if (arguments.length > 2) {
- credentials.put("tfa_token", arguments[2]);
+ private String generateTOTP(String secret) {
+ if (secret == null || secret.trim().isEmpty()) {
+ throw new IllegalArgumentException("MFA secret cannot be null or empty");
+ }
+
+ try {
+ GoogleAuthenticator gAuth = new GoogleAuthenticator();
+ String totp = String.format("%06d", gAuth.getTotpPassword(secret));
+
+ // Validate the generated token
+ if (!totp.matches("\\d{6}")) {
+ throw new IllegalArgumentException("Generated TOTP token is invalid (not 6 digits)");
+ }
+
+ return totp;
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to generate TOTP token: " + e.getMessage(), e);
}
- return credentials;
}
/**
@@ -128,8 +199,7 @@ public Call getUser() {
* @return Call
*/
public Call update(JSONObject body) {
- HashMap headers = new HashMap<>();
- return userService.update(headers, body);
+ return userService.update(new HashMap<>(), body);
}
/**
diff --git a/src/test/java/com/contentstack/cms/ContentstackAPITest.java b/src/test/java/com/contentstack/cms/ContentstackAPITest.java
index 6f1ca163..61ffba90 100644
--- a/src/test/java/com/contentstack/cms/ContentstackAPITest.java
+++ b/src/test/java/com/contentstack/cms/ContentstackAPITest.java
@@ -9,6 +9,8 @@
import retrofit2.Response;
import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
/*
@author ***REMOVED***@gmail.com
@@ -69,8 +71,9 @@ void testContentstackUserLoginWhenAlreadyLoggedIn() throws IOException {
Contentstack contentstack = new Contentstack.Builder()
.setAuthtoken(null)
.build();
- Response response = contentstack.login("invalid@credentials.com", "invalid@password",
- "invalid_tfa_token");
+ Map params = new HashMap<>();
+ params.put("tfaToken", "invalid_tfa_token");
+ Response response = contentstack.login("invalid@credentials.com", "invalid@password", params);
Assertions.assertEquals(422, response.code());
}
diff --git a/src/test/java/com/contentstack/cms/ContentstackUnitTest.java b/src/test/java/com/contentstack/cms/ContentstackUnitTest.java
index 6386cf22..8c9e57dc 100644
--- a/src/test/java/com/contentstack/cms/ContentstackUnitTest.java
+++ b/src/test/java/com/contentstack/cms/ContentstackUnitTest.java
@@ -17,6 +17,8 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
+import java.util.HashMap;
+import java.util.Map;
public class ContentstackUnitTest {
@@ -210,7 +212,9 @@ void testSetAuthtokenLogin() {
void testSetAuthtokenLoginWithTfa() {
Contentstack client = new Contentstack.Builder().setAuthtoken("fake@authtoken").build();
try {
- client.login("fake@email.com", "fake@password", "fake@tfa");
+ Map params = new HashMap<>();
+ params.put("tfaToken", "fake@tfa");
+ client.login("fake@email.com", "fake@password", params);
} catch (Exception e) {
Assertions.assertEquals("User is already loggedIn, Please logout then try to login again", e.getMessage());
}
diff --git a/src/test/java/com/contentstack/cms/user/UserUnitTests.java b/src/test/java/com/contentstack/cms/user/UserUnitTests.java
index 685c4bd9..150eea96 100644
--- a/src/test/java/com/contentstack/cms/user/UserUnitTests.java
+++ b/src/test/java/com/contentstack/cms/user/UserUnitTests.java
@@ -10,6 +10,7 @@
import java.net.URL;
import java.util.HashMap;
+import java.util.Map;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -587,8 +588,7 @@ void testUserBaseParameters() {
HashMap headers = new HashMap<>();
headers.put("header", "something");
Request requestInfo = userInstance.
- addParams(headers).
- addParam("param", "value")
+ addParam("param", (Object)"value")
.addHeader("key", "value")
.addHeaders(headers)
.logoutWithAuthtoken("authtoken")
@@ -596,4 +596,135 @@ void testUserBaseParameters() {
Assertions.assertEquals(0,
requestInfo.url().queryParameterNames().size());
}
+
+ @Test
+ @Order(42)
+ @DisplayName("Test login with TOTP token")
+ void testLoginWithTOTPToken() {
+ // Test basic login with TOTP token
+ Map params = new HashMap<>();
+ params.put("tfaToken", "123456");
+ Request requestInfo = userInstance.login("test@example.com", "password", params).request();
+
+ // Verify request format
+ Assertions.assertEquals("POST", requestInfo.method());
+ Assertions.assertEquals("/v3/user-session", requestInfo.url().encodedPath());
+
+ // Verify request body
+ String requestBody = requestInfo.body() != null ? requestInfo.body().toString() : "";
+ Assertions.assertTrue(requestBody.contains("\"tfa_token\":\"123456\""));
+ }
+
+ @Test
+ @Order(43)
+ @DisplayName("Test login with MFA parameters")
+ void testLoginWithMFAParameters() {
+ // Test login with both token and secret (should throw exception)
+ Map params = new HashMap<>();
+ params.put("tfaToken", "123456");
+ params.put("mfaSecret", "test-secret");
+
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> userInstance.login("test@example.com", "password", params).request()
+ );
+
+ Assertions.assertTrue(exception.getMessage().contains("Cannot provide both tfaToken and mfaSecret"));
+ }
+
+ @Test
+ @Order(44)
+ @DisplayName("Test login validation")
+ void testLoginValidation() {
+ Map params = new HashMap<>();
+ params.put("tfaToken", "123456");
+
+ // Test with missing required parameters
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> userInstance.login("", "password", params).request());
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> userInstance.login("test@example.com", "", params).request());
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> userInstance.login("test@example.com", "password", new HashMap<>()).request());
+ }
+
+ @Test
+ @Order(45)
+ @DisplayName("Test TOTP generation from MFA secret")
+ void testTOTPGenerationFromSecret() {
+ // Test login with MFA secret only
+ Map params = new HashMap<>();
+ params.put("mfaSecret", "test-secret");
+ Request requestInfo = userInstance.login("test@example.com", "password", params).request();
+
+ // Verify request format
+ Assertions.assertEquals("POST", requestInfo.method());
+ Assertions.assertEquals("/v3/user-session", requestInfo.url().encodedPath());
+
+ // Verify generated TOTP format
+ String requestBody = requestInfo.body() != null ? requestInfo.body().toString() : "";
+ Assertions.assertTrue(requestBody.contains("\"tfa_token\":\""));
+
+ // Extract TOTP token and verify it's 6 digits
+ String token = requestBody.split("\"tfa_token\":\"")[1].split("\"")[0];
+ Assertions.assertTrue(token.matches("\\d{6}"), "TOTP should be 6 digits");
+ }
+
+ @Test
+ @Order(46)
+ @DisplayName("Test invalid MFA secret handling")
+ void testInvalidMFASecret() {
+ // Test with invalid MFA secret
+ Map params = new HashMap<>();
+ params.put("mfaSecret", "invalid-secret");
+
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> userInstance.login("test@example.com", "password", params).request()
+ );
+
+ Assertions.assertTrue(exception.getMessage().contains("Invalid MFA secret key"));
+ }
+
+ @Test
+ @Order(47)
+ @DisplayName("Test empty parameters")
+ void testEmptyParameters() {
+ // Test with empty parameters map
+ Map params = new HashMap<>();
+
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> userInstance.login("test@example.com", "password", params).request()
+ );
+
+ Assertions.assertTrue(exception.getMessage().contains("Must provide either tfaToken or mfaSecret"));
+ }
+
+ @Test
+ @Order(48)
+ @DisplayName("Test empty values in parameters")
+ void testEmptyValues() {
+ // Test with empty tfaToken
+ Map params1 = new HashMap<>();
+ params1.put("tfaToken", "");
+
+ IllegalArgumentException exception1 = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> userInstance.login("test@example.com", "password", params1).request()
+ );
+ Assertions.assertTrue(exception1.getMessage().contains("Must provide either tfaToken or mfaSecret"));
+
+ // Test with empty mfaSecret
+ Map params2 = new HashMap<>();
+ params2.put("mfaSecret", "");
+
+ IllegalArgumentException exception2 = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> userInstance.login("test@example.com", "password", params2).request()
+ );
+ Assertions.assertTrue(exception2.getMessage().contains("Must provide either tfaToken or mfaSecret"));
+ }
}