Skip to content

Commit 9038579

Browse files
authored
Merge pull request #767 from Iterable/jwt/SDK-136-new-jwt-token-generator
[SDK-136] Add Jwt Generator to Example app
2 parents d695a8b + 13012d9 commit 9038579

File tree

17 files changed

+482
-77
lines changed

17 files changed

+482
-77
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ ios/generated
8585
android/generated
8686

8787
# Iterable
88+
.env.local
8889
.env
8990
.xcode.env.local
9091
coverage/

example/.env.example

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@
99
# 4. Fill in the following fields:
1010
# - Name: A descriptive name for the API key
1111
# - Type: Mobile
12-
# - JWT authentication: Leave **unchecked** (IMPORTANT)
12+
# - JWT authentication: Whether or not you want to use JWT
1313
# 5. Click "Create API Key"
14-
# 6. Copy the generated API key
15-
# 7. Replace the placeholder text next to `ITBL_API_KEY=` with the copied API key
14+
# 6. Copy the generated API key and replace the placeholder text next to
15+
# `ITBL_API_KEY=` with the copied API key
16+
# 7. If you chose to enable JWT authentication, copy the JWT secret and and
17+
# replace the placeholder text next to `ITBL_JWT_SECRET=` with the copied
18+
# JWT secret
1619
ITBL_API_KEY=replace_this_with_your_iterable_api_key
20+
# Your JWT Secret, created when making your API key (see above)
21+
ITBL_JWT_SECRET=replace_this_with_your_jwt_secret
22+
# Is your api token JWT Enabled?
23+
# Must be set to 'true' to enable JWT authentication
24+
ITBL_IS_JWT_ENABLED=true
1725

1826
# Your Iterable user ID or email address
19-
ITBL_ID=replace_this_with_your_user_id_or_email
27+
ITBL_ID=replace_this_with_your_user_id_or_email

example/README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ _example app directory_. To do so, run the following:
2323

2424
```bash
2525
cd ios
26-
pod install
26+
bundle install
27+
bundle exec pod install
2728
```
2829

2930
Once this is done, `cd` back into the _example app directory_:
@@ -40,12 +41,18 @@ In it, you will find:
4041

4142
```shell
4243
ITBL_API_KEY=replace_this_with_your_iterable_api_key
44+
ITBL_JWT_SECRET=replace_this_with_your_jwt_secret
45+
ITBL_IS_JWT_ENABLED=true
4346
ITBL_ID=replace_this_with_your_user_id_or_email
4447
```
4548

46-
Replace `replace_this_with_your_iterable_api_key` with your _mobile_ Iterable API key,
47-
and replace `replace_this_with_your_user_id_or_email` with the email or user id
48-
that you use to log into Iterable.
49+
- Replace `replace_this_with_your_iterable_api_key` with your **_mobile_
50+
Iterable API key**
51+
- Replace `replace_this_with_your_jwt_secret` with your **JWT Secret** (if you
52+
have a JWT-enabled API key)
53+
- Set `ITBL_IS_JWT_ENABLED` to true if you have a JWT-enabled key, and false if you do not.
54+
- Replace `replace_this_with_your_user_id_or_email` with the **email or user
55+
id** that you use to log into Iterable.
4956

5057
Follow the steps below if you do not have a mobile Iterable API key.
5158

@@ -54,12 +61,12 @@ To add an API key, do the following:
5461
1. Sign into your Iterable account
5562
2. Go to [Integrations > API Keys](https://app.iterable.com/settings/apiKeys)
5663
3. Click "New API Key" in the top right corner
57-
4. Fill in the followsing fields:
64+
4. Fill in the following fields:
5865
- Name: A descriptive name for the API key
5966
- Type: Mobile
60-
- JWT authentication: Leave **unchecked** (IMPORTANT)
67+
- JWT authentication: Check to enable JWT authentication. If enabled, will need to create a [JWT generator](https://support.iterable.com/hc/en-us/articles/360050801231-JWT-Enabled-API-Keys#sample-python-code-for-jwt-generation) to generate the JWT token.
6168
5. Click "Create API Key"
62-
6. Copy the generated API key
69+
6. Copy the generated API key and JWT secret into your _.env_ file
6370

6471

6572
## Step 3: Start the Metro Server
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.iterable;
2+
3+
import javax.crypto.Mac;
4+
import javax.crypto.spec.SecretKeySpec;
5+
import java.nio.charset.StandardCharsets;
6+
import java.time.Duration;
7+
import java.util.Base64;
8+
import java.util.Base64.Encoder;
9+
10+
/**
11+
* Utility class to generate JWTs for use with the Iterable API
12+
*
13+
* @author engineering@iterable.com
14+
*/
15+
public class IterableJwtGenerator {
16+
static Encoder encoder = Base64.getUrlEncoder().withoutPadding();
17+
18+
private static final String algorithm = "HmacSHA256";
19+
20+
// Iterable enforces a 1-year maximum token lifetime
21+
private static final Duration maxTokenLifetime = Duration.ofDays(365);
22+
23+
private static long millisToSeconds(long millis) {
24+
return millis / 1000;
25+
}
26+
27+
private static final String encodedHeader = encoder.encodeToString(
28+
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8)
29+
);
30+
31+
/**
32+
* Generates a JWT from the provided secret, header, and payload. Does not
33+
* validate the header or payload.
34+
*
35+
* @param secret Your organization's shared secret with Iterable
36+
* @param payload The JSON payload
37+
*
38+
* @return a signed JWT
39+
*/
40+
public static String generateToken(String secret, String payload) {
41+
try {
42+
String encodedPayload = encoder.encodeToString(
43+
payload.getBytes(StandardCharsets.UTF_8)
44+
);
45+
String encodedHeaderAndPayload = encodedHeader + "." + encodedPayload;
46+
47+
// HMAC setup
48+
Mac hmac = Mac.getInstance(algorithm);
49+
SecretKeySpec keySpec = new SecretKeySpec(
50+
secret.getBytes(StandardCharsets.UTF_8), algorithm
51+
);
52+
hmac.init(keySpec);
53+
54+
String signature = encoder.encodeToString(
55+
hmac.doFinal(
56+
encodedHeaderAndPayload.getBytes(StandardCharsets.UTF_8)
57+
)
58+
);
59+
60+
return encodedHeaderAndPayload + "." + signature;
61+
62+
} catch (Exception e) {
63+
throw new RuntimeException(e.getMessage());
64+
}
65+
}
66+
67+
/**
68+
* Generates a JWT (issued now, expires after the provided duration).
69+
*
70+
* @param secret Your organization's shared secret with Iterable.
71+
* @param duration The token's expiration time. Up to one year.
72+
* @param email The email to included in the token, or null.
73+
* @param userId The userId to include in the token, or null.
74+
*
75+
* @return A JWT string
76+
*/
77+
public static String generateToken(
78+
String secret, Duration duration, String email, String userId) {
79+
80+
if (duration.compareTo(maxTokenLifetime) > 0)
81+
throw new IllegalArgumentException(
82+
"Duration must be one year or less."
83+
);
84+
85+
if ((userId != null && email != null) || (userId == null && email == null))
86+
throw new IllegalArgumentException(
87+
"The token must include a userId or email, but not both."
88+
);
89+
90+
long now = millisToSeconds(System.currentTimeMillis());
91+
92+
String payload;
93+
if (userId != null)
94+
payload = String.format(
95+
"{ \"userId\": \"%s\", \"iat\": %d, \"exp\": %d }",
96+
userId, now, now + millisToSeconds(duration.toMillis())
97+
);
98+
else
99+
payload = String.format(
100+
"{ \"email\": \"%s\", \"iat\": %d, \"exp\": %d }",
101+
email, now, now + millisToSeconds(duration.toMillis())
102+
);
103+
104+
return generateToken(secret, payload);
105+
}
106+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package iterable.reactnativesdk.example
2+
3+
import com.facebook.react.bridge.ReactApplicationContext
4+
import com.facebook.react.bridge.ReactContextBaseJavaModule
5+
import com.facebook.react.bridge.ReactMethod
6+
import com.facebook.react.bridge.Promise
7+
import com.iterable.IterableJwtGenerator
8+
import java.time.Duration
9+
10+
class JwtTokenModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
11+
12+
override fun getName(): String {
13+
return NAME
14+
}
15+
16+
@ReactMethod
17+
fun generateJwtToken(
18+
secret: String,
19+
durationMs: Double,
20+
email: String?,
21+
userId: String?,
22+
promise: Promise
23+
) {
24+
try {
25+
val duration = Duration.ofMillis(durationMs.toLong())
26+
val token = IterableJwtGenerator.generateToken(secret, duration, email, userId)
27+
promise.resolve(token)
28+
} catch (e: Exception) {
29+
promise.reject("JWT_GENERATION_ERROR", e.message, e)
30+
}
31+
}
32+
33+
companion object {
34+
const val NAME = "JwtTokenModule"
35+
}
36+
}
37+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package iterable.reactnativesdk.example
2+
3+
import com.facebook.react.BaseReactPackage
4+
import com.facebook.react.bridge.NativeModule
5+
import com.facebook.react.bridge.ReactApplicationContext
6+
import com.facebook.react.module.model.ReactModuleInfo
7+
import com.facebook.react.module.model.ReactModuleInfoProvider
8+
9+
class JwtTokenPackage : BaseReactPackage() {
10+
11+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12+
return if (name == JwtTokenModule.NAME) {
13+
JwtTokenModule(reactContext)
14+
} else {
15+
null
16+
}
17+
}
18+
19+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
20+
return ReactModuleInfoProvider {
21+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
22+
moduleInfos[JwtTokenModule.NAME] = ReactModuleInfo(
23+
JwtTokenModule.NAME,
24+
JwtTokenModule.NAME,
25+
false, // canOverrideExistingModule
26+
false, // needsEagerInit
27+
true, // hasConstants
28+
false // isCxxModule
29+
)
30+
moduleInfos
31+
}
32+
}
33+
}
34+

example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class MainApplication : Application(), ReactApplication {
2020
PackageList(this).packages.apply {
2121
// Packages that cannot be autolinked yet can be added manually here, for example:
2222
// add(MyReactNativePackage())
23+
add(JwtTokenPackage())
2324
}
2425

2526
override fun getJSMainModuleName(): String = "index"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// IterableJwtGenerator.swift
3+
// ReactNativeSdkExample
4+
//
5+
// Utility class to generate JWTs for use with the Iterable API
6+
//
7+
8+
import CryptoKit
9+
import Foundation
10+
11+
@objcMembers public final class IterableJwtGenerator: NSObject {
12+
13+
private struct Header: Encodable {
14+
let alg = "HS256"
15+
let typ = "JWT"
16+
}
17+
18+
/// Base64 URL encode without padding (URL-safe base64 encoding for JWT)
19+
private static func urlEncodedBase64(_ data: Data) -> String {
20+
let base64 = data.base64EncodedString()
21+
return
22+
base64
23+
.replacingOccurrences(of: "+", with: "-")
24+
.replacingOccurrences(of: "/", with: "_")
25+
.replacingOccurrences(of: "=", with: "")
26+
}
27+
28+
/// Generic JWT generation helper that works with any Encodable payload
29+
private static func generateJwt<T: Encodable>(secret: String, payload: T) -> String {
30+
let headerJsonData = try! JSONEncoder().encode(Header())
31+
let headerBase64 = urlEncodedBase64(headerJsonData)
32+
33+
let payloadJsonData = try! JSONEncoder().encode(payload)
34+
let payloadBase64 = urlEncodedBase64(payloadJsonData)
35+
36+
let toSign = Data((headerBase64 + "." + payloadBase64).utf8)
37+
38+
if #available(iOS 13.0, *) {
39+
let privateKey = SymmetricKey(data: Data(secret.utf8))
40+
let signature = HMAC<SHA256>.authenticationCode(for: toSign, using: privateKey)
41+
let signatureBase64 = urlEncodedBase64(Data(signature))
42+
43+
let token = [headerBase64, payloadBase64, signatureBase64].joined(separator: ".")
44+
45+
return token
46+
}
47+
return ""
48+
}
49+
50+
public static func generateJwtForEmail(secret: String, iat: Int, exp: Int, email: String)
51+
-> String
52+
{
53+
struct Payload: Encodable {
54+
var email: String
55+
var iat: Int
56+
var exp: Int
57+
}
58+
59+
return generateJwt(secret: secret, payload: Payload(email: email, iat: iat, exp: exp))
60+
}
61+
62+
public static func generateJwtForUserId(secret: String, iat: Int, exp: Int, userId: String)
63+
-> String
64+
{
65+
struct Payload: Encodable {
66+
var userId: String
67+
var iat: Int
68+
var exp: Int
69+
}
70+
71+
return generateJwt(secret: secret, payload: Payload(userId: userId, iat: iat, exp: exp))
72+
}
73+
74+
public static func generateToken(
75+
secret: String, durationMs: Int64, email: String?, userId: String?
76+
) throws -> String {
77+
// Convert durationMs from milliseconds to seconds
78+
let durationSeconds = Double(durationMs) / 1000.0
79+
let currentTime = Date().timeIntervalSince1970
80+
81+
if userId != nil {
82+
return generateJwtForUserId(
83+
secret: secret, iat: Int(currentTime),
84+
exp: Int(currentTime + durationSeconds), userId: userId!)
85+
} else if email != nil {
86+
return generateJwtForEmail(
87+
secret: secret, iat: Int(currentTime),
88+
exp: Int(currentTime + durationSeconds), email: email!)
89+
} else {
90+
throw NSError(
91+
domain: "JWTGenerator", code: 6,
92+
userInfo: [NSLocalizedDescriptionKey: "No email or userId provided"])
93+
}
94+
}
95+
}

example/ios/JwtTokenModule.mm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// JwtTokenModule.m
3+
// ReactNativeSdkExample
4+
//
5+
// React Native module bridge for JWT token generation
6+
//
7+
8+
#import <React/RCTBridgeModule.h>
9+
10+
@interface RCT_EXTERN_MODULE(JwtTokenModule, NSObject)
11+
12+
RCT_EXTERN_METHOD(generateJwtToken:(NSString *)secret
13+
durationMs:(double)durationMs
14+
email:(NSString *)email
15+
userId:(NSString *)userId
16+
resolver:(RCTPromiseResolveBlock)resolve
17+
rejecter:(RCTPromiseRejectBlock)reject)
18+
19+
+ (BOOL)requiresMainQueueSetup
20+
{
21+
return NO;
22+
}
23+
24+
@end
25+

0 commit comments

Comments
 (0)