-
Notifications
You must be signed in to change notification settings - Fork 40
[SDK-136] Add Jwt Generator to Example app #767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2dcb969
1bfa9d5
d471800
6a9972a
ad48782
5df923f
40c4bad
fea6232
fd76f08
b663dce
a58be2a
13012d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,6 +85,7 @@ ios/generated | |
| android/generated | ||
|
|
||
| # Iterable | ||
| .env.local | ||
| .env | ||
| .xcode.env.local | ||
| coverage/ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package com.iterable; | ||
|
|
||
| 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 | ||
| */ | ||
| 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); | ||
qltysh[bot] marked this conversation as resolved.
Show resolved
Hide resolved
qltysh[bot] marked this conversation as resolved.
Show resolved
Hide resolved
qltysh[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+77
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package iterable.reactnativesdk.example | ||
|
|
||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.bridge.ReactContextBaseJavaModule | ||
| import com.facebook.react.bridge.ReactMethod | ||
| import com.facebook.react.bridge.Promise | ||
| import com.iterable.IterableJwtGenerator | ||
| import java.time.Duration | ||
|
|
||
| class JwtTokenModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { | ||
|
|
||
| override fun getName(): String { | ||
| return NAME | ||
| } | ||
|
|
||
| @ReactMethod | ||
| fun generateJwtToken( | ||
| secret: String, | ||
| durationMs: Double, | ||
| email: String?, | ||
| userId: String?, | ||
| promise: Promise | ||
qltysh[bot] marked this conversation as resolved.
Show resolved
Hide resolved
qltysh[bot] marked this conversation as resolved.
Show resolved
Hide resolved
qltysh[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+17
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| ) { | ||
| try { | ||
| val duration = Duration.ofMillis(durationMs.toLong()) | ||
| val token = IterableJwtGenerator.generateToken(secret, duration, email, userId) | ||
| promise.resolve(token) | ||
| } catch (e: Exception) { | ||
| promise.reject("JWT_GENERATION_ERROR", e.message, e) | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| const val NAME = "JwtTokenModule" | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package iterable.reactnativesdk.example | ||
|
|
||
| import com.facebook.react.BaseReactPackage | ||
| import com.facebook.react.bridge.NativeModule | ||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.module.model.ReactModuleInfo | ||
| import com.facebook.react.module.model.ReactModuleInfoProvider | ||
|
|
||
| class JwtTokenPackage : BaseReactPackage() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Package creation example! Great for team to understand how its done! |
||
|
|
||
| override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { | ||
| return if (name == JwtTokenModule.NAME) { | ||
| JwtTokenModule(reactContext) | ||
| } else { | ||
| null | ||
| } | ||
| } | ||
|
|
||
| override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { | ||
| return ReactModuleInfoProvider { | ||
| val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap() | ||
| moduleInfos[JwtTokenModule.NAME] = ReactModuleInfo( | ||
| JwtTokenModule.NAME, | ||
| JwtTokenModule.NAME, | ||
| false, // canOverrideExistingModule | ||
| false, // needsEagerInit | ||
| true, // hasConstants | ||
| false // isCxxModule | ||
| ) | ||
| moduleInfos | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ class MainApplication : Application(), ReactApplication { | |
| PackageList(this).packages.apply { | ||
| // Packages that cannot be autolinked yet can be added manually here, for example: | ||
| // add(MyReactNativePackage()) | ||
| add(JwtTokenPackage()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good to know this is how packages are added |
||
| } | ||
|
|
||
| override fun getJSMainModuleName(): String = "index" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // | ||
| // IterableJwtGenerator.swift | ||
| // ReactNativeSdkExample | ||
| // | ||
| // Utility class to generate JWTs for use with the Iterable API | ||
| // | ||
|
|
||
| import CryptoKit | ||
| import Foundation | ||
|
|
||
| @objcMembers public final class IterableJwtGenerator: NSObject { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Until I saw this, I was under the impression it will be using native layer's sample app code. Then I realized how! This makes sense. So now sample app in RN has its own native layer implementation! End to end native to TS layer work is implemented here! Great one Loren!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YAY! So glad you approve 😁 |
||
|
|
||
| private struct Header: Encodable { | ||
| let alg = "HS256" | ||
| let typ = "JWT" | ||
| } | ||
|
|
||
| /// Base64 URL encode without padding (URL-safe base64 encoding for JWT) | ||
| private static func urlEncodedBase64(_ data: Data) -> String { | ||
| let base64 = data.base64EncodedString() | ||
| return | ||
| base64 | ||
| .replacingOccurrences(of: "+", with: "-") | ||
| .replacingOccurrences(of: "/", with: "_") | ||
| .replacingOccurrences(of: "=", with: "") | ||
| } | ||
|
|
||
| /// Generic JWT generation helper that works with any Encodable payload | ||
| private static func generateJwt<T: Encodable>(secret: String, payload: T) -> String { | ||
| let headerJsonData = try! JSONEncoder().encode(Header()) | ||
| let headerBase64 = urlEncodedBase64(headerJsonData) | ||
|
|
||
| let payloadJsonData = try! JSONEncoder().encode(payload) | ||
| let payloadBase64 = urlEncodedBase64(payloadJsonData) | ||
|
|
||
| let toSign = Data((headerBase64 + "." + payloadBase64).utf8) | ||
|
|
||
| if #available(iOS 13.0, *) { | ||
| let privateKey = SymmetricKey(data: Data(secret.utf8)) | ||
| let signature = HMAC<SHA256>.authenticationCode(for: toSign, using: privateKey) | ||
| let signatureBase64 = urlEncodedBase64(Data(signature)) | ||
|
|
||
| let token = [headerBase64, payloadBase64, signatureBase64].joined(separator: ".") | ||
|
|
||
| return token | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| public static func generateJwtForEmail(secret: String, iat: Int, exp: Int, email: String) | ||
| -> String | ||
| { | ||
| struct Payload: Encodable { | ||
| var email: String | ||
| var iat: Int | ||
| var exp: Int | ||
| } | ||
|
|
||
| return generateJwt(secret: secret, payload: Payload(email: email, iat: iat, exp: exp)) | ||
| } | ||
|
|
||
| public static func generateJwtForUserId(secret: String, iat: Int, exp: Int, userId: String) | ||
| -> String | ||
| { | ||
| struct Payload: Encodable { | ||
| var userId: String | ||
| var iat: Int | ||
| var exp: Int | ||
| } | ||
|
|
||
| return generateJwt(secret: secret, payload: Payload(userId: userId, iat: iat, exp: exp)) | ||
| } | ||
|
|
||
| public static func generateToken( | ||
| secret: String, durationMs: Int64, email: String?, userId: String? | ||
| ) throws -> String { | ||
| // Convert durationMs from milliseconds to seconds | ||
| let durationSeconds = Double(durationMs) / 1000.0 | ||
| let currentTime = Date().timeIntervalSince1970 | ||
|
|
||
| if userId != nil { | ||
| return generateJwtForUserId( | ||
| secret: secret, iat: Int(currentTime), | ||
| exp: Int(currentTime + durationSeconds), userId: userId!) | ||
| } else if email != nil { | ||
| return generateJwtForEmail( | ||
| secret: secret, iat: Int(currentTime), | ||
| exp: Int(currentTime + durationSeconds), email: email!) | ||
| } else { | ||
| throw NSError( | ||
| domain: "JWTGenerator", code: 6, | ||
| userInfo: [NSLocalizedDescriptionKey: "No email or userId provided"]) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // | ||
| // JwtTokenModule.m | ||
| // ReactNativeSdkExample | ||
| // | ||
| // React Native module bridge for JWT token generation | ||
| // | ||
|
|
||
| #import <React/RCTBridgeModule.h> | ||
|
|
||
| @interface RCT_EXTERN_MODULE(JwtTokenModule, NSObject) | ||
|
|
||
| RCT_EXTERN_METHOD(generateJwtToken:(NSString *)secret | ||
| durationMs:(double)durationMs | ||
| email:(NSString *)email | ||
| userId:(NSString *)userId | ||
| resolver:(RCTPromiseResolveBlock)resolve | ||
| rejecter:(RCTPromiseRejectBlock)reject) | ||
|
|
||
| + (BOOL)requiresMainQueueSetup | ||
| { | ||
| return NO; | ||
| } | ||
|
|
||
| @end | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.