Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,20 @@ - (double)calculateReattemptDelay:(int)reattemptCount {
return REATTEMPT_DELAY * pow(3, reattemptCount);
}

- (NSDictionary<NSString *, NSString *> *)loggableHeadersOfRequest:(OneSignalRequest *)request {
NSDictionary<NSString *, NSString *> *headers = request.additionalHeaders;
if (!headers) {
return nil;
}
NSMutableDictionary<NSString *, NSString *> *loggable = [headers mutableCopy];
for (NSString *name in headers) {
if ([name caseInsensitiveCompare:@"Authorization"] == NSOrderedSame) {
loggable[name] = @"<redacted>";
}
}
return loggable;
}

- (void)prettyPrintDebugStatementWithRequest:(OneSignalRequest *)request {
if (![NSJSONSerialization isValidJSONObject:request.parameters])
return;
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
56 changes: 56 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = \"<redacted>\""), 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 = \"<redacted>\""), entry)
XCTAssertFalse(listener.calls.contains { $0.contains(bearer) }, "\(listener.calls)")
XCTAssertEqual(request.urlRequest().value(forHTTPHeaderField: "Authorization"), "Bearer \(bearer)")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@
// Use this file to import your target's public headers that you would like to expose to Swift.
//

#import <OneSignalCore/OneSignalClient.h>

/// 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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
28 changes: 28 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignalUserTests/UserJwtApiTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down
Loading