Skip to content

Commit

Permalink
EUID Support (#57)
Browse files Browse the repository at this point in the history
* Implement EUID Support

* Add an Info.plist toggle for UID2/EUID

* Use bundleID for EUID requests
  • Loading branch information
dcaunt authored Aug 22, 2024
1 parent 9432399 commit eb745e6
Show file tree
Hide file tree
Showing 19 changed files with 286 additions and 122 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>UID2EnvironmentEUID</key>
<false/>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

"common.nil" = "Nil";

"root.navigation.title" = "UID2 SDK Dev App";
"root.uid2.navigation.title" = "UID2 SDK Dev App";
"root.euid.navigation.title" = "EUID SDK Dev App";
"root.title.identitypackage" = "Current Identity";
"root.label.error" = "Error Occurred";
"root.button.reset" = "Reset";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ internal final class AppUID2Client: Sendable {
func decryptResponse(_ b64Secret: String, _ responseData: Data, _ isRefresh: Bool = false) -> Data? {

// Confirm that responseData is Base64
// swiftlint:disable:next non_optional_string_data_conversion
guard let base64String = String(data: responseData, encoding: .utf8),
let decodedData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
return responseData
Expand Down Expand Up @@ -211,6 +212,7 @@ internal final class AppUID2Client: Sendable {
payload = decryptedData.subdata(in: 16..<decryptedData.count)
}

// swiftlint:disable:next non_optional_string_data_conversion
guard let _ = String(data: payload, encoding: .utf8) else {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct RootView: View {
var body: some View {

VStack {
Text("root.navigation.title")
Text(viewModel.isEUID ? "root.euid.navigation.title" : "root.uid2.navigation.title")
.font(Font.system(size: 28, weight: .bold))
HStack {
TextField("Email Address", text: $email)
Expand Down Expand Up @@ -85,6 +85,7 @@ extension TokenGenerationError: LocalizedError {
if let message,
let jsonObject = try? JSONSerialization.jsonObject(with: Data(message.utf8)),
let jsonString = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) {
// swiftlint:disable:next non_optional_string_data_conversion
formattedMessage = String(data: jsonString, encoding: .utf8)
} else {
formattedMessage = message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,41 @@

import Combine
import Foundation
import OSLog
import SwiftUI
import UID2

extension RootViewModel {
struct Configuration {
let subscriptionID: String
let appName: String
let serverPublicKeyString: String

static func uid2() -> Self {
self.init(
subscriptionID: "toPh8vgJgt",
appName: Bundle.main.bundleIdentifier!,
// swiftlint:disable:next line_length
serverPublicKeyString: "UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKAbPfOz7u25g1fL6riU7p2eeqhjmpALPeYoyjvZmZ1xM2NM8UeOmDZmCIBnKyRZ97pz5bMCjrs38WM22O7LJuw=="
)
}

static func euid() -> Self {
self.init(
subscriptionID: "w6yPQzN4dA",
appName: Bundle.main.bundleIdentifier!,
// swiftlint:disable:next line_length
serverPublicKeyString: "EUID-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEH/k7HYGuWhjhCo8nXgj/ypClo5kek7uRKvzCGwj04Y1eXOWmHDOLAQVCPquZdfVVezIpABNAl9zvsSEC7g+ZGg=="
)
}
}
}

@MainActor
class RootViewModel: ObservableObject {

final class RootViewModel: ObservableObject {

let isEUID: Bool

@Published private(set) var uid2Identity: UID2Identity? {
didSet {
error = nil
Expand All @@ -25,19 +54,33 @@ class RootViewModel: ObservableObject {

/// `UID2Settings` must be configured prior to accessing the `UID2Manager` instance.
/// Configuring them here makes it less likely that an access occurs before configuration.
private let manager: UID2Manager = {
private let manager: UID2Manager

private let configuration: Configuration

private let log = OSLog(subsystem: "com.uid2.UID2SDKDevelopmentApp", category: "RootViewModel")

init() {
isEUID = Bundle.main.object(forInfoDictionaryKey: "UID2EnvironmentEUID") as? Bool ?? false

UID2Settings.shared.isLoggingEnabled = true
// Only the development app should use the integration environment.
// If you have copied the dev app for testing, you probably want to use the default
// environment, which is production.
if Bundle.main.bundleIdentifier == "com.uid2.UID2SDKDevelopmentApp" {
UID2Settings.shared.environment = .custom(url: URL(string: "https://operator-integ.uidapi.com")!)
UID2Settings.shared.euidEnvironment = .custom(url: URL(string: "https://integ.euid.eu/v2")!)
UID2Settings.shared.uid2Environment = .custom(url: URL(string: "https://operator-integ.uidapi.com")!)
}

return UID2Manager.shared
}()

init() {
if isEUID {
os_log("Configured for EUID", log: log, type: .info)
configuration = .euid()
manager = EUIDManager.shared
} else {
os_log("Configured for UID2", log: log, type: .info)
configuration = .uid2()
manager = UID2Manager.shared
}
Task {
for await state in await manager.stateValues() {
self.uid2Identity = state?.identity
Expand Down Expand Up @@ -130,17 +173,13 @@ class RootViewModel: ObservableObject {
}

func clientSideGenerate(identity: IdentityType) {
let subscriptionID = "toPh8vgJgt"
// swiftlint:disable:next line_length
let serverPublicKeyString = "UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKAbPfOz7u25g1fL6riU7p2eeqhjmpALPeYoyjvZmZ1xM2NM8UeOmDZmCIBnKyRZ97pz5bMCjrs38WM22O7LJuw=="

Task<Void, Never> {
do {
try await manager.generateIdentity(
identity,
subscriptionID: subscriptionID,
serverPublicKey: serverPublicKeyString,
appName: Bundle.main.bundleIdentifier!
subscriptionID: configuration.subscriptionID,
serverPublicKey: configuration.serverPublicKeyString,
appName: configuration.appName
)
} catch {
self.error = error
Expand Down
17 changes: 17 additions & 0 deletions Sources/UID2/EUIDManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// EUIDManager.swift
//

import Foundation

public final class EUIDManager {

/// Singleton access point for EUID Manager
/// Returns a manager configured for use with EUID.
public static let shared: UID2Manager = {
UID2Manager(
environment: Environment(UID2Settings.shared.euidEnvironment),
account: .euid
)
}()
}
88 changes: 67 additions & 21 deletions Sources/UID2/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,74 @@

import Foundation

/// For more information, see https://unifiedid.com/docs/getting-started/gs-environments
public struct Environment: Hashable, Sendable {
/// Internal Environment representation
struct Environment: Hashable, Sendable {

/// API base URL
var endpoint: URL

/// Equivalent to `ohio`
public static let production = ohio

/// AWS US East (Ohio)
public static let ohio = Self(endpoint: URL(string: "https://prod.uidapi.com")!)
/// AWS US West (Oregon)
public static let oregon = Self(endpoint: URL(string: "https://usw.prod.uidapi.com")!)
/// AWS Asia Pacific (Singapore)
public static let singapore = Self(endpoint: URL(string: "https://sg.prod.uidapi.com")!)
/// AWS Asia Pacific (Sydney)
public static let sydney = Self(endpoint: URL(string: "https://au.prod.uidapi.com")!)
/// AWS Asia Pacific (Tokyo)
public static let tokyo = Self(endpoint: URL(string: "https://jp.prod.uidapi.com")!)

/// A custom endpoint
public static func custom(url: URL) -> Self {
Self(endpoint: url)
let endpoint: URL
let isProduction: Bool
}

extension Environment {
init(_ environment: UID2.Environment) {
endpoint = environment.endpoint
isProduction = (environment == .production)
}

init(_ environment: EUID.Environment) {
endpoint = environment.endpoint
isProduction = (environment == .production)
}
}

// Namespaces
public enum EUID {}
public enum UID2 {}

extension UID2 {
/// For more information, see https://unifiedid.com/docs/getting-started/gs-environments
public struct Environment: Hashable, Sendable {

/// API base URL
var endpoint: URL

/// Equivalent to `ohio`
public static let production = ohio

/// AWS US East (Ohio)
public static let ohio = Self(endpoint: URL(string: "https://prod.uidapi.com")!)
/// AWS US West (Oregon)
public static let oregon = Self(endpoint: URL(string: "https://usw.prod.uidapi.com")!)
/// AWS Asia Pacific (Singapore)
public static let singapore = Self(endpoint: URL(string: "https://sg.prod.uidapi.com")!)
/// AWS Asia Pacific (Sydney)
public static let sydney = Self(endpoint: URL(string: "https://au.prod.uidapi.com")!)
/// AWS Asia Pacific (Tokyo)
public static let tokyo = Self(endpoint: URL(string: "https://jp.prod.uidapi.com")!)

/// A custom endpoint
public static func custom(url: URL) -> Self {
Self(endpoint: url)
}
}
}

extension EUID {
/// See https://euid.eu/docs/getting-started/gs-environments
public struct Environment: Hashable, Sendable {

/// API base URL
var endpoint: URL

/// Equivalent to `london`
public static let production = london

/// AWS EU West 2 (London)
public static let london = Self(endpoint: URL(string: "https://prod.euid.eu/v2")!)

/// A custom endpoint
public static func custom(url: URL) -> Self {
Self(endpoint: url)
}
}
}
18 changes: 14 additions & 4 deletions Sources/UID2/KeychainManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import Foundation
import Security

extension Storage {
static func keychainStorage() -> Storage {
let storage = KeychainManager()
static func keychainStorage(account: Account) -> Storage {
let storage = KeychainManager(account: account)
return .init(
loadIdentity: { await storage.loadIdentity() },
saveIdentity: { await storage.saveIdentity($0) },
Expand All @@ -16,13 +16,23 @@ extension Storage {
}
}

/// These RawValue are used as persistence keys and must not be renamed
enum Account: String {
case uid2 = "uid2" // swiftlint:disable:this redundant_string_enum_value
case euid = "euid" // swiftlint:disable:this redundant_string_enum_value
}

/// Securely manages data in the Keychain
actor KeychainManager {

private let attrAccount = "uid2"
private let attrAccount: Account

private static let attrService = "auth-state"

init(account: Account = .uid2) {
attrAccount = account
}

func loadIdentity() -> IdentityPackage? {
let query = query(with: [
String(kSecReturnData): true
Expand Down Expand Up @@ -77,7 +87,7 @@ actor KeychainManager {
private func query(with queryElements: [String: Any]) -> CFDictionary {
let commonElements = [
String(kSecClass): kSecClassGenericPassword,
String(kSecAttrAccount): attrAccount,
String(kSecAttrAccount): attrAccount.rawValue,
String(kSecAttrService): Self.attrService
] as [String: Any]

Expand Down
2 changes: 2 additions & 0 deletions Sources/UID2/Networking/DataEnvelope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ internal enum DataEnvelope {
extension Data {
/// A convenience initializer for converting from a Data representation of a base64 encoded string to its decoded Data.
init?(base64EncodedData: Data, options: Data.Base64DecodingOptions = []) {
// https://github.com/realm/SwiftLint/issues/5263#issuecomment-2115182747
// swiftlint:disable:next non_optional_string_data_conversion
guard let base64String = String(data: base64EncodedData, encoding: .utf8) else {
return nil
}
Expand Down
6 changes: 4 additions & 2 deletions Sources/UID2/UID2Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal final class UID2Client: Sendable {
init(
sdkVersion: String,
isLoggingEnabled: Bool = false,
environment: Environment = .production,
environment: Environment,
session: NetworkSession = URLSession.shared,
cryptoUtil: CryptoUtil = .liveValue
) {
Expand Down Expand Up @@ -120,9 +120,11 @@ internal final class UID2Client: Sendable {
let decoder = JSONDecoder.apiDecoder()
guard response.statusCode == 200 else {
let statusCode = response.statusCode
// https://github.com/realm/SwiftLint/issues/5263#issuecomment-2115182747
// swiftlint:disable:next non_optional_string_data_conversion
let responseText = String(data: data, encoding: .utf8) ?? "<none>"
os_log("Request failure (%d) %@", log: log, type: .error, statusCode, responseText)
if environment != .production {
if !environment.isProduction {
os_log("Failed request is using non-production API endpoint %@, is this intentional?", log: log, type: .error, baseURL.description)
}
throw TokenGenerationError.requestFailure(
Expand Down
Loading

0 comments on commit eb745e6

Please sign in to comment.