diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m index 1733ecc3f6..5cfee3305d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m @@ -175,6 +175,20 @@ - (double)calculateReattemptDelay:(int)reattemptCount { return REATTEMPT_DELAY * pow(3, reattemptCount); } +- (NSDictionary *)loggableHeadersOfRequest:(OneSignalRequest *)request { + NSDictionary *headers = request.additionalHeaders; + if (!headers) { + return nil; + } + NSMutableDictionary *loggable = [headers mutableCopy]; + for (NSString *name in headers) { + if ([name caseInsensitiveCompare:@"Authorization"] == NSOrderedSame) { + loggable[name] = @""; + } + } + return loggable; +} + - (void)prettyPrintDebugStatementWithRequest:(OneSignalRequest *)request { if (![NSJSONSerialization isValidJSONObject:request.parameters]) return; @@ -190,7 +204,7 @@ - (void)prettyPrintDebugStatementWithRequest:(OneSignalRequest *)request { NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with parameters: %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, jsonString, request.additionalHeaders]]; + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with parameters: %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, jsonString, [self loggableHeadersOfRequest:request]]]; } - (void)handleJSONNSURLResponse:(NSURLResponse*)response data:(NSData*)data error:(NSError*)error isAsync:(BOOL)async withRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSClientFailureBlock)failureBlock { @@ -206,7 +220,7 @@ - (void)handleJSONNSURLResponse:(NSURLResponse*)response data:(NSData*)data erro innerJson[@"httpStatusCode"] = [NSNumber numberWithLong:statusCode]; innerJson[@"headers"] = headers; - [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network request (%@) with URL %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, request.additionalHeaders]]; + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network request (%@) with URL %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, [self loggableHeadersOfRequest:request]]]; [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network response (%@) with URL %@: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, innerJson]]; if (jsonError) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift index b0a2cf8fe3..6a63f245aa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift @@ -169,4 +169,60 @@ final class LoggingTests: XCTestCase { XCTAssertEqual(sink.levels, [.LL_WARN]) XCTAssertEqual(sink.messages, ["raw message"]) } + + // MARK: - Credential redaction + + private let bearer = "jwt-a1b2c3" + + /// Headers as `OSRequestAuth` and the User module set them on a signed request. + private func signedRequest() -> OneSignalRequest { + let request = OneSignalRequest() + request.method = GET + request.path = "apps/test-app-id/users/by/external_id/user-a" + request.parameters = ["app_id": "test-app-id"] + request.additionalHeaders = ["Authorization": "Bearer \(bearer)", "OneSignal-Subscription-Id": "sub-a"] + return request + } + + /// The send log masks the bearer and nothing else. + func testSendingASignedRequestLogsTheHeadersWithTheBearerMasked() throws { + let listener = TestLogListener() + OneSignalLog.debug().__add(listener) + defer { OneSignalLog.debug().__remove(listener) } + let request = signedRequest() + + XCTAssertTrue(OneSignalClient.shared().validRequest(request)) + + let entry = try XCTUnwrap(listener.calls.first { $0.contains("HTTP Request") }) + XCTAssertFalse(entry.contains(bearer), entry) + XCTAssertTrue(entry.contains("Authorization = \"\""), entry) + XCTAssertTrue(entry.contains("sub-a"), entry) + XCTAssertFalse(listener.calls.contains { $0.contains(bearer) }, "\(listener.calls)") + XCTAssertEqual(request.urlRequest().value(forHTTPHeaderField: "Authorization"), "Bearer \(bearer)") + } + + /// The response log masks the bearer too. + func testHandlingAResponseLogsTheHeadersWithTheBearerMasked() throws { + let listener = TestLogListener() + OneSignalLog.debug().__add(listener) + defer { OneSignalLog.debug().__remove(listener) } + let request = signedRequest() + let url = try XCTUnwrap(request.urlRequest().url) + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) + let body = try JSONSerialization.data(withJSONObject: ["onesignal_id": "osid-a"]) + var result: [AnyHashable: Any]? + + OneSignalClient.shared().handleJSONNSURLResponse( + response, data: body, error: nil, isAsync: false, with: request, + onSuccess: { result = $0 }, + onFailure: { XCTFail("\($0)") } + ) + + XCTAssertEqual(result?["onesignal_id"] as? String, "osid-a") + let entry = try XCTUnwrap(listener.calls.first { $0.contains("network request") }) + XCTAssertFalse(entry.contains(bearer), entry) + XCTAssertTrue(entry.contains("Authorization = \"\""), entry) + XCTAssertFalse(listener.calls.contains { $0.contains(bearer) }, "\(listener.calls)") + XCTAssertEqual(request.urlRequest().value(forHTTPHeaderField: "Authorization"), "Bearer \(bearer)") + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests-Bridging-Header.h b/iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests-Bridging-Header.h index 1b2cb5d6d0..f831466260 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests-Bridging-Header.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests-Bridging-Header.h @@ -2,3 +2,10 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#import + +/// The client's log sites, exposed so tests can read what they print without a network round trip. +@interface OneSignalClient (LoggingTests) +- (BOOL)validRequest:(OneSignalRequest *)request; +- (void)handleJSONNSURLResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *)error isAsync:(BOOL)async withRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSClientFailureBlock)failureBlock; +@end diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+Jwt.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+Jwt.swift index 5a424932f9..b99b855d53 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+Jwt.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+Jwt.swift @@ -103,8 +103,10 @@ extension OneSignalUserManagerImpl { OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignal.updateUserJwt called with empty externalId or an unusable token. This is not allowed.") return } - // TODO: omit the token from this log before shipping — keep for testing. - OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignal.updateUserJwt called for externalId: \(externalId) with token: \(token)") + OneSignalLog.onesignalLog( + .LL_VERBOSE, + message: "OneSignal.updateUserJwt called for externalId: \(externalId) with token: ...\(token.suffix(8))" + ) storeJwt(externalId: externalId, token: token) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserJwtApiTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserJwtApiTests.swift index f24b446671..6755b732d6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserJwtApiTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserJwtApiTests.swift @@ -32,6 +32,19 @@ import OneSignalUserMocks @testable import OneSignalOSCore @testable import OneSignalUser +private final class CapturingLogListener: NSObject, OSLogListener { + private let lock = NSLock() + private var captured: [String] = [] + + var entries: [String] { + lock.withLock { captured } + } + + func onLogEvent(_ event: OneSignalLogEvent) { + lock.withLock { captured.append(event.entry) } + } +} + /** Public JWT surface on the User Manager: store a token, and tell the app when it stopped being accepted. Goes through `sharedInstance` because the JWT config and IV service are shared. @@ -113,6 +126,21 @@ final class UserJwtApiTests: XCTestCase { XCTAssertNil(user.identityModel.jwtBearerToken) } + /// The entry names the user and the token's last 8 characters, never more. + func testUpdateUserJwtLogsTheCallWithOnlyTheTokensTail() { + _ = OneSignalUserMocks.setUserManagerInternalUser(externalId: "user-a", onesignalId: "osid-a") + let token = "eyJhbGciOi.eyJzdWIiOi.signature-a1b2c3d4" + let log = CapturingLogListener() + OneSignalLog.debug().__add(log) + defer { OneSignalLog.debug().__remove(log) } + + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: "user-a", token: token) + + let entry = log.entries.first { $0.contains("updateUserJwt") } + XCTAssertEqual(entry, "VERBOSE: OneSignal.updateUserJwt called for externalId: user-a with token: ...a1b2c3d4") + XCTAssertFalse(log.entries.contains { $0.contains("signature-a1b2c3d4") }, "\(log.entries)") + } + // MARK: - Invalidated listener func testInvalidatingAJwtParksTheTokenAndNotifiesTheApp() {