Skip to content
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

feat: remove API secret in favor of explicitly passing user HMAC #31

Merged
merged 8 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
59EF9A9B2744700700FB2378 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 59EF9A992744700700FB2378 /* Main.storyboard */; };
59EF9A9D2744700800FB2378 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 59EF9A9C2744700800FB2378 /* Assets.xcassets */; };
59EF9AA02744700800FB2378 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 59EF9A9E2744700800FB2378 /* LaunchScreen.storyboard */; };
94E508EF2BC71087002995F3 /* String+Empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E508EE2BC71087002995F3 /* String+Empty.swift */; };
D2DC15F62758C68000282C27 /* UIColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DC15F52758C68000282C27 /* UIColorExtensions.swift */; };
D2DC15F82758CC0B00282C27 /* MagicBellStoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DC15F72758CC0B00282C27 /* MagicBellStoreCell.swift */; };
D2E4B088277B5EA500E9E98F /* MagicBellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E4B087277B5E8A00E9E98F /* MagicBellView.swift */; };
Expand All @@ -35,6 +36,7 @@
59EF9A9F2744700800FB2378 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
59EF9AA12744700800FB2378 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
911386F4690B2C7C97254425 /* Pods-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.release.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.release.xcconfig"; sourceTree = "<group>"; };
94E508EE2BC71087002995F3 /* String+Empty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Empty.swift"; sourceTree = "<group>"; };
D2DC15F52758C68000282C27 /* UIColorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorExtensions.swift; sourceTree = "<group>"; };
D2DC15F72758CC0B00282C27 /* MagicBellStoreCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicBellStoreCell.swift; sourceTree = "<group>"; };
D2E4B087277B5E8A00E9E98F /* MagicBellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicBellView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -120,6 +122,7 @@
D2DC15F52758C68000282C27 /* UIColorExtensions.swift */,
D2E4B08A277B5EDF00E9E98F /* Appearance.swift */,
D2E4B08C277B619F00E9E98F /* UIHostingViewController.swift */,
94E508EE2BC71087002995F3 /* String+Empty.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -267,6 +270,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
94E508EF2BC71087002995F3 /* String+Empty.swift in Sources */,
59EF9A982744700700FB2378 /* MagicBellStoreViewController.swift in Sources */,
D2DC15F82758CC0B00282C27 /* MagicBellStoreCell.swift in Sources */,
59EF9A942744700700FB2378 /* AppDelegate.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,12 @@
//

import Foundation
import CommonCrypto

extension String {
func hmac(key: String) -> String {
let cKey = key.cString(using: String.Encoding.utf8)
let cData = cString(using: String.Encoding.utf8)
var result = [CUnsignedChar](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
if let cKey = cKey,
let cData = cData {
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), cKey, strlen(cKey), cData, strlen(cData), &result)
let hmacData = NSData(bytes: result, length: Int(CC_SHA256_DIGEST_LENGTH))
let hmacBase64 = hmacData.base64EncodedString(options: NSData.Base64EncodingOptions.lineLength76Characters)
return String(hmacBase64)
} else {
return ""
extension Optional where Wrapped == String {
var nilIfEmpty: String? {
guard let strongSelf = self else {
return nil
}
return strongSelf.isEmpty ? nil : strongSelf
}
}
14 changes: 12 additions & 2 deletions Example/Example/UIKit/MagicBellStoreViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,22 @@ class MagicBellStoreViewController: UIViewController,
alert.addTextField { textField in
textField.placeholder = "john@doe.com"
}
alert.addTextField { textField in
textField.placeholder = "User HMAC (optional)"
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: "Login", style: .default) { _ in
guard let email = alert.textFields?.first?.text else {
guard let textFields = alert.textFields,
let emailField = textFields.first,
let hmacField = textFields.last,
let email = emailField.text.nilIfEmpty
else {
return
}
self.user = MagicBellClient.shared.connectUser(email: email)

let hmac = hmacField.text.nilIfEmpty // optional

self.user = MagicBellClient.shared.connectUser(email: email, hmac: hmac)
self.configureStore(predicate: StorePredicate())
})
self.present(alert, animated: true, completion: nil)
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@ You can provide additional options when initializing a client:
```swift
let magicbell = MagicBellClient(
apiKey: "[MAGICBELL_API_KEY]"
apiSecret: "[MAGICBELL_API_SECRET]",
enableHMAC: true,
logLevel: .debug
)
```
Expand All @@ -126,7 +124,6 @@ let magicbell = MagicBellClient(
| ------------ | ------------- | -------------------------------------------------------------------------------------------- |
| `apiKey` | - | Your MagicBell's API key |
| `apiSecret` | `nil` | Your MagicBell's API secret |
| `enableHMAC` | `false` | Set it to `true` if you want HMAC enabled. Note the `apiSecret` is required if set to `true` |
| `logLevel` | `.none` | Set it to `.debug` to enable logs |

Though the API key is meant to be published, you should not distribute the API secret. Rather, enable HMAC for your
Expand Down Expand Up @@ -170,6 +167,8 @@ let user = magicbell.connectUser(externalId: "001")
let user = magicbell.connectUser(email: "richard@example.com", externalId: "001")
```

Each variant of `connectUser` supports an optional `hmac` parameter that should be send when HMAC Security was enabled for the project.

You can connect as [many users as you need](#multi-user-support).

**IMPORTANT:** `User` instances are singletons. Therefore, calls to the `connectUser` method with the same arguments will
Expand Down
2 changes: 0 additions & 2 deletions Source/Common/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,5 @@ import Foundation

struct Environment {
let apiKey: String
let apiSecret: String?
let baseUrl: URL
let isHMACEnabled: Bool
}
27 changes: 6 additions & 21 deletions Source/Common/Network/HttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import Foundation
import Harmony

protocol HttpClient {
func prepareURLRequest(path: String, externalId: String?, email: String?, additionalHTTPHeaders: [String: String]?) -> URLRequest
func prepareURLRequest(path: String, externalId: String?, email: String?, hmac: String?, additionalHTTPHeaders: [String: String]?) -> URLRequest
func performRequest(_ urlRequest: URLRequest) -> Future<Data>
}
extension HttpClient {
func prepareURLRequest(path: String, externalId: String?, email: String?) -> URLRequest {
prepareURLRequest(path: path, externalId: externalId, email: email, additionalHTTPHeaders: [:])
func prepareURLRequest(path: String, externalId: String?, email: String?, hmac: String?) -> URLRequest {
prepareURLRequest(path: path, externalId: externalId, email: email, hmac: hmac, additionalHTTPHeaders: [:])
}
}

Expand All @@ -34,14 +34,13 @@ class DefaultHttpClient: HttpClient {
self.environment = environment
}

func prepareURLRequest(path: String, externalId: String?, email: String?, additionalHTTPHeaders: [String: String]?) -> URLRequest {
func prepareURLRequest(path: String, externalId: String?, email: String?, hmac: String?, additionalHTTPHeaders: [String: String]?) -> URLRequest {
var urlRequest = URLRequest(url: environment.baseUrl.appendingPathComponent(path))

urlRequest.addValue(environment.apiKey, forHTTPHeaderField: "X-MAGICBELL-API-KEY")

if environment.isHMACEnabled,
let apiSecret = environment.apiSecret {
addHMACHeader(apiSecret, externalId, email, &urlRequest)
if let hmac = hmac {
urlRequest.addValue(hmac, forHTTPHeaderField: "X-MAGICBELL-USER-HMAC")
}
addIdAndOrEmailHeader(externalId, email, &urlRequest)

Expand Down Expand Up @@ -81,20 +80,6 @@ class DefaultHttpClient: HttpClient {
}
}

private func addHMACHeader(_ apiSecret: String,
_ externalId: String?,
_ email: String?,
_ urlRequest: inout URLRequest) {

if let externalId = externalId {
let hmac: String = externalId.hmac(key: apiSecret)
urlRequest.addValue(hmac, forHTTPHeaderField: "X-MAGICBELL-USER-HMAC")
} else if let email = email {
let hmac: String = email.hmac(key: apiSecret)
urlRequest.addValue(hmac, forHTTPHeaderField: "X-MAGICBELL-USER-HMAC")
}
}

private func addIdAndOrEmailHeader(_ externalId: String?,
_ email: String?,
_ urlRequest: inout URLRequest) {
Expand Down
42 changes: 30 additions & 12 deletions Source/Common/Query/UserQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,41 @@ import Harmony
class UserQuery: KeyQuery {
let externalId: String?
let email: String?
let hmac: String?
let key: String

init(externalId: String, email: String) {
self.externalId = externalId
self.email = email
self.key = externalId
// Mark: - Initializers

// private initializer
private init(maybeExternalId: String?, maybeEmail: String?, maybeHmac: String?) {
self.externalId = maybeExternalId
self.email = maybeEmail
self.hmac = maybeHmac
self.key = UserQuery.preferedKey(email: self.email, externalId: self.externalId)
}

convenience init(externalId: String, email: String, hmac: String?) {
self.init(maybeExternalId: externalId, maybeEmail: email, maybeHmac: hmac)
}

init(externalId: String) {
self.externalId = externalId
self.email = nil
self.key = externalId
convenience init(externalId: String, hmac: String?) {
self.init(maybeExternalId: externalId, maybeEmail: nil, maybeHmac: hmac)
}

init(email: String) {
self.externalId = nil
self.email = email
self.key = email
convenience init(email: String, hmac: String?) {
self.init(maybeExternalId: nil, maybeEmail: email, maybeHmac: hmac)
}

// Mark: - Helper

// externalID is prefered over email for key
static func preferedKey(email: String?, externalId: String?) -> String {
if let externalId = externalId {
return externalId
} else if let email = email {
return email
} else {
Swift.fatalError("Either a users email, or an external Id is required")
}
}
}
3 changes: 2 additions & 1 deletion Source/Features/Config/Data/ConfigNetworkDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class ConfigNetworkDataSource: GetDataSource {
let urlRequest = httpClient.prepareURLRequest(
path: "/config",
externalId: userQuery.externalId,
email: userQuery.email
email: userQuery.email,
hmac: userQuery.hmac
)
return httpClient
.performRequest(urlRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class ActionNotificationNetworkDataSource: PutDataSource, DeleteDataSource {
var urlRequest = self.httpClient.prepareURLRequest(
path: path,
externalId: notificationActionQuery.user.externalId,
email: notificationActionQuery.user.email
email: notificationActionQuery.user.email,
hmac: notificationActionQuery.user.hmac
)

urlRequest.httpMethod = httpMethod
Expand All @@ -71,7 +72,8 @@ class ActionNotificationNetworkDataSource: PutDataSource, DeleteDataSource {
var urlRequest = self.httpClient.prepareURLRequest(
path: "/notifications/\(notificationQuery.notificationId)",
externalId: notificationQuery.user.externalId,
email: notificationQuery.user.email
email: notificationQuery.user.email,
hmac: notificationQuery.user.hmac
)
urlRequest.httpMethod = "DELETE"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class NotificationNetworkDataSource: GetDataSource {
let urlRequest = self.httpClient.prepareURLRequest(
path: "/notifications/\(notificationQuery.notificationId)",
externalId: notificationQuery.user.externalId,
email: notificationQuery.user.email
email: notificationQuery.user.email,
hmac: notificationQuery.user.hmac
)
return self.httpClient
.performRequest(urlRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class NotificationPreferencesNetworkDataSource: GetDataSource, PutDataSource {
path: "/notification_preferences",
externalId: userQuery.externalId,
email: userQuery.email,
hmac: userQuery.hmac,
additionalHTTPHeaders: ["accept-version": "v2"]
)
return self.httpClient
Expand Down Expand Up @@ -60,6 +61,7 @@ class NotificationPreferencesNetworkDataSource: GetDataSource, PutDataSource {
path: "/notification_preferences",
externalId: userQuery.externalId,
email: userQuery.email,
hmac: userQuery.hmac,
additionalHTTPHeaders: ["accept-version": "v2"]
)
urlRequest.httpMethod = "PUT"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class PushSubscriptionNetworkDataSource: PutDataSource, DeleteDataSource {
var urlRequest = httpClient.prepareURLRequest(
path: "/push_subscriptions",
externalId: pushSubscriptionQuery.user.externalId,
email: pushSubscriptionQuery.user.email
email: pushSubscriptionQuery.user.email,
hmac: pushSubscriptionQuery.user.hmac
)
urlRequest.httpMethod = "POST"
do {
Expand Down Expand Up @@ -66,7 +67,8 @@ class PushSubscriptionNetworkDataSource: PutDataSource, DeleteDataSource {
var urlRequest = self.httpClient.prepareURLRequest(
path: "/push_subscriptions/\(deletePushSubscriptionQuery.deviceToken)",
externalId: deletePushSubscriptionQuery.user.externalId,
email: deletePushSubscriptionQuery.user.email
email: deletePushSubscriptionQuery.user.email,
hmac: deletePushSubscriptionQuery.user.hmac
)
urlRequest.httpMethod = "DELETE"

Expand Down
3 changes: 2 additions & 1 deletion Source/Features/Store/Data/StoresGraphQLDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class StoresGraphQLDataSource: GetDataSource {
var urlRequest = httpClient.prepareURLRequest(
path: "/graphql",
externalId: query.userQuery.externalId,
email: query.userQuery.email
email: query.userQuery.email,
hmac: query.userQuery.hmac
)
urlRequest.allHTTPHeaderFields = ["content-type": "application/json"]
urlRequest.httpMethod = "POST"
Expand Down
21 changes: 6 additions & 15 deletions Source/Features/StoreRealTime/AblyConnector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,9 @@ class AblyConnector: StoreRealTime {
options.authMethod = "POST"
let headers = self.generateAblyHeaders(
apiKey: self.environment.apiKey,
apiSecret: self.environment.apiSecret,
isHMACEnabled: self.environment.isHMACEnabled,
externalId: self.userQuery.externalId,
email: self.userQuery.email
email: self.userQuery.email,
hmac: self.userQuery.hmac
)
options.authHeaders = headers

Expand All @@ -97,22 +96,14 @@ class AblyConnector: StoreRealTime {

private func generateAblyHeaders(
apiKey: String,
apiSecret: String?,
isHMACEnabled: Bool,
externalId: String?,
email: String?
email: String?,
hmac: String?
) -> [String: String] {

var headers = ["X-MAGICBELL-API-KEY": apiKey]
if let apiSecret = apiSecret,
isHMACEnabled {
if let externalId = externalId {
let hmac = externalId.hmac(key: apiSecret)
headers["X-MAGICBELL-USER-HMAC"] = hmac
} else if let email = email {
let hmac = email.hmac(key: apiSecret)
headers["X-MAGICBELL-USER-HMAC"] = hmac
}
if let hmac = hmac {
headers["X-MAGICBELL-USER-HMAC"] = hmac
}

if let externalId = externalId {
Expand Down
Loading