Skip to content

Commit

Permalink
Add support for DeviceToken cookie, and workaround for the idx cookie (
Browse files Browse the repository at this point in the history
  • Loading branch information
mikenachbaur-okta authored Nov 18, 2022
1 parent 569f6fa commit bd98eae
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 8 deletions.
4 changes: 2 additions & 2 deletions OktaIdx.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'OktaIdx'
spec.version = '3.0.1'
spec.version = '3.0.2'
spec.summary = 'SDK to easily integrate the Okta Identity Engine'
spec.description = <<-DESC
Integrate your native app with Okta using the Okta Identity Engine library.
Expand All @@ -24,5 +24,5 @@ Integrate your native app with Okta using the Okta Identity Engine library.
spec.source_files = 'Sources/OktaIdx/**/*.swift'
spec.swift_version = "5.5"

spec.dependency "OktaAuthFoundation", "1.0.0"
spec.dependency "OktaAuthFoundation", "1.1.2"
end
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var package = Package(
dependencies: [
.package(name: "AuthFoundation",
url: "https://github.com/okta/okta-mobile-swift",
from: "1.0.0")
from: "1.1.2")
],
targets: [
.target(name: "OktaIdx",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This library uses semantic versioning and follows Okta's [Library Version Policy
| ------- | ---------------------------------- |
| 1.0.0 | |
| 2.0.1 | |
| 3.0.0 | ✔️ Stable |
| 3.0.2 | ✔️ Stable |

The latest release can always be found on the [releases page][github-releases].

Expand Down
2 changes: 1 addition & 1 deletion Sources/OktaIdx/Extensions/EnumExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ extension Capability.SocialIDP.Service {
case "OIDC": self = .oidc
case "OKTA": self = .okta
case "IWA": self = .iwa
case "AgentlessDSSO":self = .agentlessIwa
case "AgentlessDSSO": self = .agentlessIwa
case "X509": self = .x509
case "APPLE": self = .apple
case "OIN_SOCIAL": self = .oinSocial
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// Copyright (c) 2022-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
//
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and limitations under the License.
//

import Foundation
import AuthFoundation

#if canImport(UIKit)
import UIKit
#endif

#if os(watchOS)
import WatchKit
#endif

extension Keychain {
static let deviceIdentifierKey = "com.okta.mobile.deviceIdentifier"
}

extension InteractionCodeFlow {
static var systemDeviceIdentifier: UUID? {
#if canImport(UIKit) && (os(iOS) || os(macOS) || os(tvOS))
if let uuid = UIDevice.current.identifierForVendor {
return uuid
}
#elseif os(watchOS)
if let uuid = WKInterfaceDevice.current().identifierForVendor {
return uuid
}
#endif

return nil
}

static var keychainDeviceIdentifier: UUID? {
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
if let data = try? Keychain.Search(account: Keychain.deviceIdentifierKey).get().value,
let string = String(data: data, encoding: .utf8),
let uuid = UUID(uuidString: string)
{
return uuid
}

let uuid = UUID()
guard let uuidData = uuid.uuidString.data(using: .utf8) else {
return nil
}

do {
_ = try Keychain.Item(account: Keychain.deviceIdentifierKey,
value: uuidData).save()
return uuid
} catch {
return nil
}
#else
return nil
#endif
}

/// Unique identifier for this device, encoded to limit the character count. This is used within
/// an outgoing Cookie named `dt` to enable "Remember this device" trust options within OIE.
static var deviceIdentifier: String? {
guard var identifier = systemDeviceIdentifier ?? keychainDeviceIdentifier
else {
return nil
}

let data = Data(bytes: &identifier, count: 16)
let deviceToken = data.base64EncodedString()

return deviceToken
}
}
1 change: 1 addition & 0 deletions Sources/OktaIdx/IDXResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public class Response: NSObject {
switch result {
case .success(let token):
self.flow.send(response: token.result, completion: completion)
self.flow.reset()
case .failure(let error):
self.flow.send(error: .apiError(error), completion: completion)
}
Expand Down
44 changes: 42 additions & 2 deletions Sources/OktaIdx/InteractionCodeFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public final class InteractionCodeFlow: AuthenticationFlow {
self.client = client
self.redirectUri = redirectUri
self.additionalParameters = additionalParameters

client.add(delegate: self)
}

Expand Down Expand Up @@ -289,6 +289,7 @@ public final class InteractionCodeFlow: AuthenticationFlow {
switch result {
case .success(let token):
self.send(response: token.result, completion: completion)
self.reset()
case .failure(let error):
self.send(error: .apiError(error), completion: completion)
}
Expand All @@ -308,9 +309,32 @@ public final class InteractionCodeFlow: AuthenticationFlow {
public func reset() {
context = nil
isAuthenticating = false

// Remove any previous `idx` cookies so it won't leak into other sessions.
let storage = client.session.configuration.httpCookieStorage ?? HTTPCookieStorage.shared
storage.cookies(for: client.baseURL)?
.filter({ $0.name == "idx" })
.forEach({ storage.deleteCookie($0) })
}

// MARK: Private properties / methods
private(set) lazy var deviceTokenCookie: HTTPCookie? = {
guard let deviceToken = InteractionCodeFlow.deviceIdentifier,
let host = client.baseURL.host
else {
return nil
}

return HTTPCookie(properties: [
.name: "DT",
.value: deviceToken,
.domain: host,
.path: "/",
.secure: "TRUE",
.expires: Date.distantFuture,
])
}()

public let delegateCollection = DelegateCollection<InteractionCodeFlowDelegate>()
}

Expand Down Expand Up @@ -362,7 +386,23 @@ extension InteractionCodeFlow: UsesDelegateCollection {
}

extension InteractionCodeFlow: OAuth2ClientDelegate {

public func api(client: APIClient, willSend request: inout URLRequest) {
guard let url = request.url,
let deviceTokenCookie = deviceTokenCookie
else {
return
}

let storage = client.session.configuration.httpCookieStorage ?? HTTPCookieStorage.shared
var cookies = storage.cookies(for: url) ?? []
cookies.insert(deviceTokenCookie, at: 0)

var headers = request.allHTTPHeaderFields ?? [:]
headers.merge(HTTPCookie.requestHeaderFields(with: cookies)) { old, new in
new
}
request.allHTTPHeaderFields = headers
}
}

extension OAuth2Client {
Expand Down
2 changes: 1 addition & 1 deletion Sources/OktaIdx/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@

import Foundation

public let Version = SDKVersion(sdk: "okta-idx-swift", version: "3.0.1")
public let Version = SDKVersion(sdk: "okta-idx-swift", version: "3.0.2")
56 changes: 56 additions & 0 deletions Tests/OktaIdxTests/DeviceIdentifierTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// Copyright (c) 2022-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
//
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and limitations under the License.
//

import XCTest
@testable import OktaIdx

#if canImport(UIKit)
import UIKit
#endif

#if canImport(WatchKit)
import WatchKit
#endif

final class DeviceIdentifierTests: XCTestCase {
#if canImport(UIKit) && (os(iOS) || os(macOS) || os(tvOS))
func testSystemDeviceIdentifier() throws {
XCTAssertEqual(InteractionCodeFlow.systemDeviceIdentifier, UIDevice.current.identifierForVendor)
}
#elseif canImport(WatchKit)
func testSystemDeviceIdentifier() throws {
XCTAssertEqual(InteractionCodeFlow.systemDeviceIdentifier, WKInterfaceDevice.current().identifierForVendor)
}
#endif

#if canImport(UIKit) && (os(iOS) || os(macOS) || os(tvOS) || canImport(WatchKit))
func testDeviceIdentifier() throws {
let identifier = try XCTUnwrap(InteractionCodeFlow.deviceIdentifier)

// Device Token string _must_ be 32 characters or less.
XCTAssertLessThanOrEqual(identifier.count, 32)

let data = try XCTUnwrap(Data(base64Encoded: identifier, options: .ignoreUnknownCharacters))

var nsuuid: NSUUID?
data.withUnsafeBytes { (unsafeBytes) in
let bytes = unsafeBytes.bindMemory(to: UInt8.self).baseAddress!
nsuuid = NSUUID(uuidBytes: bytes)
}

let uuidString = try XCTUnwrap(nsuuid?.uuidString)
let uuid = try XCTUnwrap(UUID(uuidString: uuidString))

XCTAssertEqual(uuid, InteractionCodeFlow.systemDeviceIdentifier)
}
#endif
}
1 change: 1 addition & 0 deletions Tests/TestCommon/Mocks/URLSessionMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class URLSessionMock: URLSessionProtocol {
let error: Error?
}

var configuration: URLSessionConfiguration = .ephemeral
var requestDelay: TimeInterval?

private(set) var requests: [URLRequest] = []
Expand Down

0 comments on commit bd98eae

Please sign in to comment.