From 8b95b5cc0577d2708445562dad5c6e0ae3c22b0d Mon Sep 17 00:00:00 2001 From: Hardik Mashru Date: Wed, 3 Apr 2024 19:04:38 +0530 Subject: [PATCH 01/27] JWT improvements 1. Provide a method to stop JWT retries (regardless of retry policy) 2. Make it possible to specify a retry policy (on IterableConfig) --- app/build.gradle | 2 +- .../androidsdk/IterableJWTGenerator.java | 111 ++++++++++++++++++ .../com/iterable/androidsdk/MainActivity.java | 73 +++++++++++- app/src/main/res/layout/content_main.xml | 41 ++++++- .../com/iterable/iterableapi/IterableApi.java | 7 +- .../iterableapi/IterableAuthManager.java | 48 ++++++-- .../iterable/iterableapi/IterableConfig.java | 51 ++++++++ .../iterableapi/IterableConstants.java | 2 + .../iterableapi/IterableRequestTask.java | 5 +- .../com/iterable/iterableapi/RetryPolicy.java | 6 + 10 files changed, 329 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/iterable/androidsdk/IterableJWTGenerator.java create mode 100644 iterableapi/src/main/java/com/iterable/iterableapi/RetryPolicy.java diff --git a/app/build.gradle b/app/build.gradle index 8fe296c7e..b002f2493 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,7 +15,7 @@ android { testNamespace 'iterable.com.iterableapi' defaultConfig { - applicationId "com.iterable.iterableapi.testapp" + applicationId "com.iterable.androidsdk" minSdkVersion 16 targetSdkVersion 27 versionCode 1 diff --git a/app/src/main/java/com/iterable/androidsdk/IterableJWTGenerator.java b/app/src/main/java/com/iterable/androidsdk/IterableJWTGenerator.java new file mode 100644 index 000000000..0ef9ed5fd --- /dev/null +++ b/app/src/main/java/com/iterable/androidsdk/IterableJWTGenerator.java @@ -0,0 +1,111 @@ +package com.iterable.androidsdk; + +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Base64.Encoder; + +/** + * Utility class to generate JWTs for use with the Iterable API + * + * @author engineering@iterable.com + */ +@RequiresApi(api = Build.VERSION_CODES.O) +public class IterableJWTGenerator { + static Encoder encoder = Base64.getUrlEncoder().withoutPadding(); + + private static final String algorithm = "HmacSHA256"; + + // Iterable enforces a 1-year maximum token lifetime + private static final Duration maxTokenLifetime = Duration.ofDays(365); + + private static long millisToSeconds(long millis) { + return millis / 1000; + } + + private static final String encodedHeader = encoder.encodeToString( + "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8) + ); + + /** + * Generates a JWT from the provided secret, header, and payload. Does not + * validate the header or payload. + * + * @param secret Your organization's shared secret with Iterable + * @param payload The JSON payload + * + * @return a signed JWT + */ + public static String generateToken(String secret, String payload) { + try { + String encodedPayload = encoder.encodeToString( + payload.getBytes(StandardCharsets.UTF_8) + ); + String encodedHeaderAndPayload = encodedHeader + "." + encodedPayload; + + // HMAC setup + Mac hmac = Mac.getInstance(algorithm); + SecretKeySpec keySpec = new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), algorithm + ); + hmac.init(keySpec); + + String signature = encoder.encodeToString( + hmac.doFinal( + encodedHeaderAndPayload.getBytes(StandardCharsets.UTF_8) + ) + ); + + return encodedHeaderAndPayload + "." + signature; + + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Generates a JWT (issued now, expires after the provided duration). + * + * @param secret Your organization's shared secret with Iterable. + * @param duration The token's expiration time. Up to one year. + * @param email The email to included in the token, or null. + * @param userId The userId to include in the token, or null. + * + * @return A JWT string + */ + public static String generateToken( + String secret, Duration duration, String email, String userId) { + + if (duration.compareTo(maxTokenLifetime) > 0) + /*throw new IllegalArgumentException( + "Duration must be one year or less." + );*/ + + if ((userId != null && email != null) || (userId == null && email == null)) + throw new IllegalArgumentException( + "The token must include a userId or email, but not both." + ); + + long now = millisToSeconds(System.currentTimeMillis()); + + String payload; + if (userId != null) + payload = String.format( + "{ \"userId\": \"%s\", \"iat\": %d, \"exp\": %d }", + userId, now, now + millisToSeconds(duration.toMillis()) + ); + else + payload = String.format( + "{ \"email\": \"%s\", \"iat\": %d, \"exp\": %d }", + email, now, now + millisToSeconds(duration.toMillis()) + ); + + return generateToken(secret, payload); + } +} diff --git a/app/src/main/java/com/iterable/androidsdk/MainActivity.java b/app/src/main/java/com/iterable/androidsdk/MainActivity.java index c5f0725b8..cc0130778 100644 --- a/app/src/main/java/com/iterable/androidsdk/MainActivity.java +++ b/app/src/main/java/com/iterable/androidsdk/MainActivity.java @@ -1,17 +1,47 @@ package com.iterable.androidsdk; +import android.os.Build; import android.os.Bundle; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; + +import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; + +import android.util.Log; import android.view.View; import android.view.Menu; import android.view.MenuItem; +import com.iterable.iterableapi.IterableApi; +import com.iterable.iterableapi.IterableAuthHandler; +import com.iterable.iterableapi.IterableConfig; +import com.iterable.iterableapi.RetryPolicy; import com.iterable.iterableapi.testapp.R; -public class MainActivity extends AppCompatActivity { +import org.json.JSONException; +import org.json.JSONObject; + +import java.time.Duration; +import java.util.Objects; + +@RequiresApi(api = Build.VERSION_CODES.O) +public class MainActivity extends AppCompatActivity implements IterableAuthHandler { + + private static final String secret = "34992609011249b410db9e1a568db9b65063c73e618bdb0229a674aeed7db7fba1bdc06e9b42d021120b9c88f795a734c18ab88ff7b6ecbccc50a945899d3666"; + private static final String email = "harrymash2006@gmail.com"; + private static final String userId = "harrymash2006"; + private static final String apiKey = "4236278428294f04be8443007d3daf89"; + private static final Duration days7 = Duration.ofDays(7); + private static final Duration days366 = Duration.ofDays(366); + private static final int issuedAt = 1516239022; + private static final int expiration = 1516239023; + private static final String payload = String.format( + "{ \"email\": \"%s\", \"iat\": %d, \"exp\": %d }", + email, issuedAt, expiration + ); + @Override protected void onCreate(Bundle savedInstanceState) { @@ -28,6 +58,30 @@ public void onClick(View view) { .setAction("Action", null).show(); } }); + IterableConfig config = new IterableConfig.Builder() + .setAuthHandler(this) + .setMaxRetries(4) + .setRetryBackoff(RetryPolicy.LINEAR) + .setRetryInterval(2L) + .build(); + + IterableApi.initialize(this, apiKey, config); + + findViewById(R.id.btn_set_user).setOnClickListener(v -> IterableApi.getInstance().setUserId("hani7")); + + findViewById(R.id.btn_update_user).setOnClickListener(v -> { + try { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("firstName", "Hani"); + IterableApi.getInstance().updateUser(jsonObject); + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + + findViewById(R.id.btn_logout).setOnClickListener(view -> IterableApi.getInstance().setUserId(null)); + findViewById(R.id.btn_pauseAuthRetry).setOnClickListener(view -> IterableApi.getInstance().pauseAuthRetries(true)); + findViewById(R.id.btn_resumeAuthRetry).setOnClickListener(view -> IterableApi.getInstance().pauseAuthRetries(false)); } @Override @@ -51,4 +105,21 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } + + @Override + public String onAuthTokenRequested() { + String token = IterableJWTGenerator.generateToken(secret, days366, null, "hani7"); + Log.i("jwt token::", token); + return token; + } + + @Override + public void onTokenRegistrationSuccessful(String authToken) { + Log.i("success:", "token registration successful."); + } + + @Override + public void onTokenRegistrationFailed(Throwable object) { + Log.e("failed:", Objects.requireNonNull(object.getMessage())); + } } diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 5646874e1..4f6cb30fd 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -1,5 +1,5 @@ - + tools:showIn="@layout/activity_main" + android:gravity="center" + android:orientation="vertical"> - + +