You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
407 lines
13 KiB
407 lines
13 KiB
/**
|
|
* DailyNotificationJWTManager.java
|
|
*
|
|
* Android JWT Manager for TimeSafari authentication enhancement
|
|
* Extends existing ETagManager infrastructure with DID-based JWT authentication
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
* @created 2025-10-03 06:53:30 UTC
|
|
*/
|
|
|
|
package com.timesafari.dailynotification;
|
|
|
|
import android.util.Log;
|
|
import android.content.Context;
|
|
|
|
import java.net.HttpURLConnection;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Base64;
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
import java.security.MessageDigest;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
/**
|
|
* Manages JWT authentication for TimeSafari integration
|
|
*
|
|
* This class extends the existing ETagManager infrastructure by adding:
|
|
* - DID-based JWT token generation
|
|
* - Automatic JWT header injection into HTTP requests
|
|
* - JWT token expiration management
|
|
* - Integration with existing DailyNotificationETagManager
|
|
*
|
|
* Phase 1 Implementation: Extends existing DailyNotificationETagManager.java
|
|
*/
|
|
public class DailyNotificationJWTManager {
|
|
|
|
// MARK: - Constants
|
|
|
|
private static final String TAG = "DailyNotificationJWTManager";
|
|
|
|
// JWT Headers
|
|
private static final String HEADER_AUTHORIZATION = "Authorization";
|
|
private static final String HEADER_CONTENT_TYPE = "Content-Type";
|
|
|
|
// JWT Configuration
|
|
private static final int DEFAULT_JWT_EXPIRATION_SECONDS = 60;
|
|
|
|
// JWT Algorithm (simplified for Phase 1)
|
|
private static final String ALGORITHM = "HS256";
|
|
|
|
// MARK: - Properties
|
|
|
|
private final DailyNotificationStorage storage;
|
|
private final DailyNotificationETagManager eTagManager;
|
|
|
|
// Current authentication state
|
|
private String currentActiveDid;
|
|
private String currentJWTToken;
|
|
private long jwtExpirationTime;
|
|
|
|
// Configuration
|
|
private int jwtExpirationSeconds;
|
|
|
|
// MARK: - Initialization
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param storage Storage instance for persistence
|
|
* @param eTagManager ETagManager instance for HTTP enhancements
|
|
*/
|
|
public DailyNotificationJWTManager(DailyNotificationStorage storage, DailyNotificationETagManager eTagManager) {
|
|
this.storage = storage;
|
|
this.eTagManager = eTagManager;
|
|
this.jwtExpirationSeconds = DEFAULT_JWT_EXPIRATION_SECONDS;
|
|
|
|
Log.d(TAG, "JWTManager initialized with ETagManager integration");
|
|
}
|
|
|
|
// MARK: - ActiveDid Management
|
|
|
|
/**
|
|
* Set the active DID for authentication
|
|
*
|
|
* @param activeDid The DID to use for JWT generation
|
|
*/
|
|
public void setActiveDid(String activeDid) {
|
|
setActiveDid(activeDid, DEFAULT_JWT_EXPIRATION_SECONDS);
|
|
}
|
|
|
|
/**
|
|
* Set the active DID for authentication with custom expiration
|
|
*
|
|
* @param activeDid The DID to use for JWT generation
|
|
* @param expirationSeconds JWT expiration time in seconds
|
|
*/
|
|
public void setActiveDid(String activeDid, int expirationSeconds) {
|
|
try {
|
|
Log.d(TAG, "Setting activeDid: " + activeDid + " with " + expirationSeconds + "s expiration");
|
|
|
|
this.currentActiveDid = activeDid;
|
|
this.jwtExpirationSeconds = expirationSeconds;
|
|
|
|
// Generate new JWT token immediately
|
|
generateAndCacheJWT();
|
|
|
|
Log.i(TAG, "ActiveDid set successfully");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error setting activeDid", e);
|
|
throw new RuntimeException("Failed to set activeDid", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current active DID
|
|
*
|
|
* @return Current active DID or null if not set
|
|
*/
|
|
public String getCurrentActiveDid() {
|
|
return currentActiveDid;
|
|
}
|
|
|
|
/**
|
|
* Check if we have a valid active DID and JWT token
|
|
*
|
|
* @return true if authentication is ready
|
|
*/
|
|
public boolean isAuthenticated() {
|
|
return currentActiveDid != null &&
|
|
currentJWTToken != null &&
|
|
!isTokenExpired();
|
|
}
|
|
|
|
// MARK: - JWT Token Management
|
|
|
|
/**
|
|
* Generate JWT token for current activeDid
|
|
*
|
|
* @param expiresInSeconds Expiration time in seconds
|
|
* @return Generated JWT token
|
|
*/
|
|
public String generateJWTForActiveDid(String activeDid, int expiresInSeconds) {
|
|
try {
|
|
Log.d(TAG, "Generating JWT for activeDid: " + activeDid);
|
|
|
|
long currentTime = System.currentTimeMillis() / 1000;
|
|
|
|
// Create JWT payload
|
|
Map<String, Object> payload = new HashMap<>();
|
|
payload.put("exp", currentTime + expiresInSeconds);
|
|
payload.put("iat", currentTime);
|
|
payload.put("iss", activeDid);
|
|
payload.put("aud", "timesafari.notifications");
|
|
payload.put("sub", activeDid);
|
|
|
|
// Generate JWT token (simplified implementation for Phase 1)
|
|
String jwt = signWithDID(payload, activeDid);
|
|
|
|
Log.d(TAG, "JWT generated successfully");
|
|
return jwt;
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error generating JWT", e);
|
|
throw new RuntimeException("Failed to generate JWT", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate and cache JWT token for current activeDid
|
|
*/
|
|
private void generateAndCacheJWT() {
|
|
if (currentActiveDid == null) {
|
|
Log.w(TAG, "Cannot generate JWT: no activeDid set");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
currentJWTToken = generateJWTForActiveDid(currentActiveDid, jwtExpirationSeconds);
|
|
jwtExpirationTime = System.currentTimeMillis() + (jwtExpirationSeconds * 1000L);
|
|
|
|
Log.d(TAG, "JWT cached successfully, expires at: " + jwtExpirationTime);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error caching JWT", e);
|
|
throw new RuntimeException("Failed to cache JWT", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if current JWT token is expired
|
|
*
|
|
* @return true if token is expired
|
|
*/
|
|
private boolean isTokenExpired() {
|
|
return currentJWTToken == null || System.currentTimeMillis() >= jwtExpirationTime;
|
|
}
|
|
|
|
/**
|
|
* Refresh JWT token if needed
|
|
*/
|
|
public void refreshJWTIfNeeded() {
|
|
if (isTokenExpired()) {
|
|
Log.d(TAG, "JWT token expired, refreshing");
|
|
generateAndCacheJWT();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current valid JWT token (refreshes if needed)
|
|
*
|
|
* @return Current JWT token
|
|
*/
|
|
public String getCurrentJWTToken() {
|
|
refreshJWTIfNeeded();
|
|
return currentJWTToken;
|
|
}
|
|
|
|
// MARK: - HTTP Client Enhancement
|
|
|
|
/**
|
|
* Enhance HTTP client with JWT authentication headers
|
|
*
|
|
* Extends existing DailyNotificationETagManager connection creation
|
|
*
|
|
* @param connection HTTP connection to enhance
|
|
* @param activeDid DID for authentication (optional, uses current if null)
|
|
*/
|
|
public void enhanceHttpClientWithJWT(HttpURLConnection connection, String activeDid) {
|
|
try {
|
|
// Set activeDid if provided
|
|
if (activeDid != null && !activeDid.equals(currentActiveDid)) {
|
|
setActiveDid(activeDid);
|
|
}
|
|
|
|
// Ensure we have a valid token
|
|
if (!isAuthenticated()) {
|
|
throw new IllegalStateException("No valid authentication available");
|
|
}
|
|
|
|
// Add JWT Authorization header
|
|
String jwt = getCurrentJWTToken();
|
|
connection.setRequestProperty(HEADER_AUTHORIZATION, "Bearer " + jwt);
|
|
|
|
// Set JSON content type for API requests
|
|
connection.setRequestProperty(HEADER_CONTENT_TYPE, "application/json");
|
|
|
|
Log.d(TAG, "HTTP client enhanced with JWT authentication");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error enhancing HTTP client with JWT", e);
|
|
throw new RuntimeException("Failed to enhance HTTP client", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enhance HTTP client with JWT authentication for current activeDid
|
|
*
|
|
* @param connection HTTP connection to enhance
|
|
*/
|
|
public void enhanceHttpClientWithJWT(HttpURLConnection connection) {
|
|
enhanceHttpClientWithJWT(connection, null);
|
|
}
|
|
|
|
// MARK: - JWT Signing (Simplified for Phase 1)
|
|
|
|
/**
|
|
* Sign JWT payload with DID (simplified implementation)
|
|
*
|
|
* Phase 1: Basic implementation using DID-based signing
|
|
* Later phases: Integrate with proper DID cryptography
|
|
*
|
|
* @param payload JWT payload
|
|
* @param did DID for signing
|
|
* @return Signed JWT token
|
|
*/
|
|
private String signWithDID(Map<String, Object> payload, String did) {
|
|
try {
|
|
// Phase 1: Simplified JWT implementation
|
|
// In production, this would use proper DID + cryptography libraries
|
|
|
|
// Create JWT header
|
|
Map<String, Object> header = new HashMap<>();
|
|
header.put("alg", ALGORITHM);
|
|
header.put("typ", "JWT");
|
|
|
|
// Encode header and payload
|
|
StringBuilder jwtBuilder = new StringBuilder();
|
|
|
|
// Header
|
|
jwtBuilder.append(base64UrlEncode(mapToJson(header)));
|
|
jwtBuilder.append(".");
|
|
|
|
// Payload
|
|
jwtBuilder.append(base64UrlEncode(mapToJson(payload)));
|
|
jwtBuilder.append(".");
|
|
|
|
// Signature (simplified - would use proper DID signing)
|
|
String signature = createSignature(jwtBuilder.toString(), did);
|
|
jwtBuilder.append(signature);
|
|
|
|
String jwt = jwtBuilder.toString();
|
|
Log.d(TAG, "JWT signed successfully (length: " + jwt.length() + ")");
|
|
|
|
return jwt;
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error signing JWT", e);
|
|
throw new RuntimeException("Failed to sign JWT", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create JWT signature (simplified for Phase 1)
|
|
*
|
|
* @param data Data to sign
|
|
* @param did DID for signature
|
|
* @return Base64-encoded signature
|
|
*/
|
|
private String createSignature(String data, String did) throws Exception {
|
|
// Phase 1: Simplified signature using DID hash
|
|
// Production would use proper DID cryptographic signing
|
|
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
byte[] hash = digest.digest((data + ":" + did).getBytes(StandardCharsets.UTF_8));
|
|
|
|
return base64UrlEncode(hash);
|
|
}
|
|
|
|
/**
|
|
* Convert map to JSON string (simplified)
|
|
*/
|
|
private String mapToJson(Map<String, Object> map) {
|
|
StringBuilder json = new StringBuilder("{");
|
|
boolean first = true;
|
|
|
|
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
|
if (!first) json.append(",");
|
|
json.append("\"").append(entry.getKey()).append("\":");
|
|
|
|
Object value = entry.getValue();
|
|
if (value instanceof String) {
|
|
json.append("\"").append(value).append("\"");
|
|
} else {
|
|
json.append(value);
|
|
}
|
|
|
|
first = false;
|
|
}
|
|
|
|
json.append("}");
|
|
return json.toString();
|
|
}
|
|
|
|
/**
|
|
* Base64 URL-safe encoding
|
|
*/
|
|
private String base64UrlEncode(byte[] data) {
|
|
return Base64.getUrlEncoder()
|
|
.withoutPadding()
|
|
.encodeToString(data);
|
|
}
|
|
|
|
/**
|
|
* Base64 URL-safe encoding for strings
|
|
*/
|
|
private String base64UrlEncode(String data) {
|
|
return base64UrlEncode(data.getBytes(StandardCharsets.UTF_8));
|
|
}
|
|
|
|
// MARK: - Testing and Debugging
|
|
|
|
/**
|
|
* Get current JWT token info for debugging
|
|
*
|
|
* @return Token information
|
|
*/
|
|
public String getTokenDebugInfo() {
|
|
return String.format(
|
|
"JWT Token Info - ActiveDID: %s, HasToken: %s, Expired: %s, ExpiresAt: %d",
|
|
currentActiveDid,
|
|
currentJWTToken != null,
|
|
isTokenExpired(),
|
|
jwtExpirationTime
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clear authentication state
|
|
*/
|
|
public void clearAuthentication() {
|
|
try {
|
|
Log.d(TAG, "Clearing authentication state");
|
|
|
|
currentActiveDid = null;
|
|
currentJWTToken = null;
|
|
jwtExpirationTime = 0;
|
|
|
|
Log.i(TAG, "Authentication state cleared");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error clearing authentication", e);
|
|
}
|
|
}
|
|
}
|
|
|